diff --git a/restapi/restendpoints.md b/restapi/restendpoints.md index 163cdf3..03c7d6d 100644 --- a/restapi/restendpoints.md +++ b/restapi/restendpoints.md @@ -61,12 +61,8 @@ RESPONSE: Reminder that SSK (Shared Secret Key) is a value unique to a Merchant, and the same Merchant (SSK) can have assigned multiple devices from different manufacturers. This includes PAX and Datecs (eg. HiLite) ::: - - ## /transactions - - `Transactions` POST endpoint used to send a financial operation to the payment terminal. The transaction type to be executed (sale, refund etc.) is defined in the `operation` field of the request body. @@ -743,4 +739,362 @@ curl -X POST \ "hour": 22 }' \ "https://cloud.handpoint.io/devices/PAXIM30/0000000000/set-reboot-time" +``` + +## MOTO Operations (no reader) + +MOTO (Mail Order / Telephone Order) operations can also be processed **without a payment reader**, using information +that is already stored in the gateway (tokens and references to previous transactions). + +These endpoints are intended for MOTO scenarios where the merchant does **not** need to collect or handle sensitive +card data (PAN, expiry date, CVV) in their own systems: + +- `/moto/sale` performs a sale using a **previously generated card token** (`cardToken`) that represents card details stored in the gateway. +- `/moto/refund` performs a refund of a **previous operation**, using the card associated with that original operation (`originalGuid`). +- `/moto/reversal` performs a reversal (void) of a **previous operation**, passing only its identifier (`originalGuid`). + +:::tip +Unlike the standard MOTO operations that use `/transactions` and a physical terminal, these endpoints: + +- Do **not** require `serial_number` or `terminal_type`. +- Do **not** receive raw card data in the request. +- Rely on `cardToken` and `originalGuid` to reference card information already stored in the gateway. +::: + +All request and response payloads are defined in the corresponding [Moto objects](restobjects#moto). + +--- + +### /moto/sale + +`MotoSale` + +`POST /moto/sale` is used to perform a **MOTO sale without a payment reader**, using a **card token** (`cardToken`) +that was generated previously (for example, by a `saleAndTokenizeCard` operation). + +The card details are *not* sent in the request; they are resolved by the gateway using the `cardToken`. + +Typical flow: + +1. A card is captured securely in a previous flow, for example through the [`/transactions`](restendpoints#transactions) endpoint using a + `saleAndTokenizeCard` operation. +2. The gateway returns a `cardToken` (e.g. `665630867`). +3. The integrator can later perform one or more MOTO sales using that `cardToken`, without handling PAN/CVV again. + +#### Parameters + +| Parameter | Notes | +| --------- | ----- | +| `Header: ApiKeyCloud` Required | Cloud API key used to authenticate the merchant. | +| `Request Body: MotoSaleRequest` Required | [MotoSaleRequest](restobjects#motoSaleRequest) object containing `cardToken`, `amount`, `currency` and optional merchant references. | + +Typical fields in the request body (see [MotoSaleRequest](restobjects#motoSaleRequest) for full details): + +- `cardToken` Required – Token representing the card stored in the gateway (e.g. `"665630867"`). +- `amount` Required – String amount with dot as decimal separator (e.g. `"20.00"`), matching `^\d+(\.\d+)?$`. +- `currency` Required – 3-character ISO 4217 currency code (e.g. `"EUR"`). +- Optional references for reconciliation: `customerReference`, `transactionReference`, etc. + +#### Returns + +| Result | Notes | +| ------ | ----- | +| `200` | Sale successfully processed. The response body is a `motoSaleResponse` [Moto Transaction Response](restobjects#motoTransactionResponse) with the authorization result (approved/declined), authorization code, masked card details, acquirer TID, timestamps, etc. | +| `400` | Business rule error from the payment gateway (for example, CVV required, card token failure). Returned as `BadRequestError`, with `error.code` and `error.details` containing the gateway error code and description. | +| `422` | Payload validation error (`VALIDATION_FAILED`) when required fields are missing or do not match the schema (invalid amount pattern, currency length, etc.). | +| `5xx` | Internal error or gateway unavailability. The final outcome may be unknown and may require reconciliation. | + +#### Behaviour examples + +- **CVV required (3107)** — merchant configured with “CVV/CV2 input mandatory” for Card Not Present: + +```shell +http POST https://cloud.handpoint.io/moto/sale \ + ApiKeyCloud:SX34S16-WRZMY6C-JNJMP9J-7B0P0TH \ + amount='20.00' \ + currency='EUR' \ + cardToken='665630867' +``` + +Example response: + +```json +{ + "error": { + "statusCode": 400, + "name": "BadRequestError", + "message": "CVV required", + "code": "3107", + "details": { + "errorGuid": "dae59b20-cf71-11f0-b588-a122fae316de", + "errorCode": "3107", + "description": "CVV required", + "httpStatus": 400 + } + } +} +``` + +A very common cause of **3107** is having “CVV/CV2 input mandatory” enabled for Card Not Present. + +* **Invalid / unknown card token (5252)**: + + ```json + { + "error": { + "code": "5252", + "details": { + "description": "Card token failure", + "errorCode": "5252", + "errorGuid": "10a3d120-cf75-11f0-95b2-770b7d1d8e67", + "httpStatus": 404 + }, + "message": "Card token failure", + "name": "BadRequestError", + "statusCode": 400 + } + } + ``` + +* **Validation errors (422 `VALIDATION_FAILED`)** – for example, missing `amount`: + + ```json + { + "error": { + "code": "VALIDATION_FAILED", + "details": [ + { + "code": "required", + "info": { + "missingProperty": "amount" + }, + "message": "must have required property 'amount'", + "path": "" + } + ], + "message": "The request body is invalid. See error object `details` property for more info.", + "name": "UnprocessableEntityError", + "statusCode": 422 + } + } + ``` + +#### Code example – MOTO sale with `cardToken` + +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -H "ApiKeyCloud: SX34S16-WRZMY6C-JNJMP9J-7B0P0TH" \ + -d '{ + "amount": "20.00", + "currency": "EUR", + "cardToken": "665630867", + "customerReference": "order-12345", + "transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01" + }' \ + "https://cloud.handpoint.io/moto/sale" +``` + +--- + +### /moto/refund + +`MotoRefund` + +`POST /moto/refund` is used to perform a **MOTO refund without a payment reader**. + +The refund is **linked** to a previous MOTO sale via `originalGuid`, and the gateway reuses the card associated with that +original transaction. No card data is passed in the refund request. + +#### Parameters + +| Parameter | Notes | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `Header: ApiKeyCloud` Required | Cloud API key used to authenticate the merchant. | +| `Request Body: MotoRefundRequest` Required | [MotoRefundRequest](restobjects#motoRefundRequest) object containing `originalGuid`, `amount`, `currency` and optional merchant references. | + +Typical fields (see [MotoRefundRequest](restobjects#motoRefundRequest) for full details): + +* `originalGuid` Required – GUID of the original sale to be refunded (e.g. `"1a41d9f0-cf72-11f0-95b2-770b7d1d8e67"`). +* `amount` Required – String amount to be refunded (e.g. `"5.00"`), matching `^\d+(\.\d+)?$`. +* `currency` Required – 3-character ISO 4217 code (e.g. `"EUR"`, `"USD"`). +* Optional: `customerReference`, `transactionReference`. + +Supports full and partial refunds, depending on acquirer configuration. + +#### Returns + +| Result | Notes | +| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `200` | Refund successfully processed. The response body is a `motoRefundResponse` [Moto Transaction Response](restobjects#motoTransactionResponse) including `guid`, `originalGuid`, `amount`, `currency`, `maskedCardNumber`, `approvalCode`, `issuerResponseText`, etc. | +| `400` | Business rule error from the payment gateway (for example, currency mismatch, refund amount greater than the original sale). Returned as `BadRequestError`, with `error.code` and `error.details` describing the gateway error. | +| `422` | Payload validation error (`VALIDATION_FAILED`) when required fields are missing or invalid (missing `originalGuid`, invalid amount, invalid currency format, etc.). | + +#### Behaviour examples + +* **Partial refund – happy path** (EUR 5.00 of a 20.00 EUR sale): + + ```shell + http POST https://cloud.handpoint.io/moto/refund \ + ApiKeyCloud:SX34S16-WRZMY6C-JNJMP9J-7B0P0TH \ + originalGuid='1a41d9f0-cf72-11f0-95b2-770b7d1d8e67' \ + amount='5.00' \ + currency='EUR' + ``` + +* **Currency mismatch (3210)** + + ```json + { + "error": { + "code": "3210", + "details": { + "description": "Original and linked currency do not match", + "errorCode": "3210", + "errorGuid": "35a2b220-cf7a-11f0-b588-a122fae316de", + "httpStatus": 400 + }, + "message": "Original and linked currency do not match", + "name": "BadRequestError", + "statusCode": 400 + } + } + ``` + +* **Refund amount greater than original (3209)** + + ```json + { + "error": { + "code": "3209", + "details": { + "description": "The requested refund amount is greater than the initial sale amount", + "errorCode": "3209", + "errorGuid": "e72ea940-cf7a-11f0-b588-a122fae316de", + "httpStatus": 400 + }, + "message": "The requested refund amount is greater than the initial sale amount", + "name": "BadRequestError", + "statusCode": 400 + } + } + ``` + +* **Validation errors** – missing `originalGuid`, missing `amount`, invalid `currency` length, invalid `amount` pattern — are returned as `422 VALIDATION_FAILED` with one or more entries in `error.details`. + +#### Code example – partial MOTO refund by `originalGuid` + +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -H "ApiKeyCloud: SX34S16-WRZMY6C-JNJMP9J-7B0P0TH" \ + -d '{ + "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67", + "amount": "5.00", + "currency": "EUR", + "customerReference": "refund-98765", + "transactionReference": "a1fe8db5-69a4-4b4d-a704-94ac2570f9b0" + }' \ + "https://cloud.handpoint.io/moto/refund" +``` + +--- + +### /moto/reversal + +`MotoReversal` + +`POST /moto/reversal` is used to **reverse (void)** a previous MOTO operation processed via the no-reader MOTO endpoints. + +The request is linked to the original sale via `originalGuid`. No card data is sent; the gateway uses the original +transaction information. + +Reversals are typically used to cancel a MOTO sale shortly after authorization, subject to acquirer rules. + +#### Parameters + +| Parameter | Notes | +| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `Header: ApiKeyCloud` Required | Cloud API key used to authenticate the merchant. | +| `Request Body: MotoReversalRequest` Required | [MotoReversalRequest](restobjects#motoReversalRequest) object referencing the original MOTO transaction (`originalGuid`) and including the reversal `amount` and `currency`. | + +Typical fields (see [MotoReversalRequest](restobjects#motoReversalRequest) for full details): + +* `originalGuid` Required – GUID of the original sale to be reversed. +* `amount` Required – String amount to reverse (e.g. `"20.00"`), `^\d+(\.\d+)?$`. +* `currency` Optional – 3-character ISO 4217 code; if provided, must respect `minLength = 3`, `maxLength = 3` and may need to match the original transaction’s currency. +* Optional merchant references: `customerReference`, `transactionReference`. + +#### Returns + +| Result | Notes | +| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `200` | Reversal accepted and processed. Response body is a `motoReversalResponse` [Moto Transaction Response](restobjects#motoTransactionResponse) indicating whether the reversal was approved, with fields such as `guid`, `originalGuid`, `issuerResponseCode`, `issuerResponseText`, `f25`, etc. | +| `400` | Business rule error from the payment gateway (for example, unknown `originalGuid`, reversal not allowed). Returned as `BadRequestError`, with `error.code` and `error.details` describing the gateway error (for example, code `3153`). | +| `422` | Payload validation error (`VALIDATION_FAILED`) when required fields are missing or invalid (missing `originalGuid`, missing `amount`, invalid `currency`, invalid `amount` pattern). | + +#### Behaviour examples + +* **Happy path – full MOTO reversal** + + ```shell + http POST https://cloud.handpoint.io/moto/reversal \ + ApiKeyCloud:RCZNP67-CB6M5VN-J4QBVGZ-9JWX765 \ + originalGuid='b28bdb10-cf87-11f0-b588-a122fae316de' \ + amount='20.00' \ + currency='EUR' + ``` + + Typical success response (simplified): + + ```json + { + "type": "motoReversalResponse", + "httpStatus": 200, + "amount": "20.00", + "currency": "EUR", + "guid": "3f7772a0-cf88-11f0-b588-a122fae316de", + "originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de", + "issuerResponseCode": "00", + "issuerResponseText": "Successful", + "maskedCardNumber": "************3555", + "f25": "4000" + } + ``` + +* **Unknown / invalid `originalGuid` (3153)** + + ```json + { + "error": { + "code": "3153", + "details": { + "description": "Unable to find message to reverse.", + "errorCode": "3153", + "errorGuid": "633a4370-cf88-11f0-b588-a122fae316de", + "httpStatus": 404 + }, + "message": "Unable to find message to reverse.", + "name": "BadRequestError", + "statusCode": 400 + } + } + ``` + +* **Validation errors** – missing `originalGuid`, missing `amount`, invalid `currency` length, invalid `amount` pattern — result in `422 VALIDATION_FAILED` with one or more entries in `error.details`. + +#### Code example – MOTO reversal by `originalGuid` + +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -H "ApiKeyCloud: RCZNP67-CB6M5VN-J4QBVGZ-9JWX765" \ + -d '{ + "originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de", + "amount": "20.00", + "currency": "EUR", + "customerReference": "void-001", + "transactionReference": "4d7b1a2c-5bfd-4a30-9b6f-123456789abc" + }' \ + "https://cloud.handpoint.io/moto/reversal" ``` \ No newline at end of file diff --git a/restapi/restobjects.md b/restapi/restobjects.md index c6942dd..b5b9206 100644 --- a/restapi/restobjects.md +++ b/restapi/restobjects.md @@ -824,3 +824,191 @@ An enum representing the possible verification methods used during the transacti Possible values: `UNDEFINED` `SIGNATURE` `PIN` `PIN_SIGNATURE` `FAILED` `NOT_REQUIRED` `MOBILE_PASS_CODE` + +## Moto + +### MotoSaleRequest {#motoSaleRequest} + +`MotoSaleRequest` Object + +Object used by the [`POST /moto/sale`](restendpoints#moto-operations-no-reader) endpoint to process a MOTO sale **without a reader**, using a previously generated `cardToken` (for example, obtained via a `saleAndTokenizeCard` operation sent to [`/transactions`](restendpoints#transactions)). + +**Properties** + +| Property | Description | +| -------- | ----------- | +| `amount` Required
*String* | Amount of the operation in major units, using a dot (`.`) as decimal separator. Valid examples: `"20"`, `"20.0"`, `"20.00"`. Must match the pattern `^\d+(\.\d+)?$`. | +| `currency` Required
[*Currency*](#currency) | ISO 4217 3-character currency code (for example, `"EUR"`, `"USD"`). | +| `cardToken` Required
*String* | Token that represents the card stored securely in the gateway. This token is obtained in a previous operation (for example, `saleAndTokenizeCard`) and allows the integrator to avoid handling PAN/CVV directly. | +| `customerReference`
*String* | Merchant-defined reference for the operation. Useful for back-office reconciliation and reporting. | +| `transactionReference`
*String* | Unique identifier for the transaction (for example, a UUID v4) generated by the integrator for traceability and reconciliation. | + +**Code example** + +```json +{ + "amount": "20.00", + "currency": "EUR", + "cardToken": "665630867", + "customerReference": "order-12345", + "transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01" +} +``` + +--- + +### MotoRefundRequest {#motoRefundRequest} + +`MotoRefundRequest` Object + +Object used by the [`POST /moto/refund`](restendpoints#moto-operations-no-reader) endpoint to process a **MOTO refund without a reader**, linked to a previous operation via `originalGuid`. The card is retrieved from the original transaction, so **no card data** is sent in the refund request. + +**Properties** + +| Property | Description | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `originalGuid` Required
*String* | GUID of the original MOTO sale to be refunded (for example, the `guid` returned by a previous MOTO sale). | +| `amount` Required
*String* | Amount to be refunded, in major units, using a dot (`.`) as decimal separator. Must match `^\d+(\.\d+)?$`. Can be a **partial** or full refund, depending on acquirer configuration. | +| `currency` Required
[*Currency*](#currency) | ISO 4217 3-character currency code (for example, `"EUR"`, `"USD"`). In most flows this is expected to match the currency of the original transaction; otherwise a business error (for example, code `3210`) can be returned. | +| `customerReference`
*String* | Merchant-defined reference for the refund, useful for internal reporting and reconciliation. | +| `transactionReference`
*String* | Unique identifier for this refund operation generated by the integrator. | + +**Code example** + +```json +{ + "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67", + "amount": "5.00", + "currency": "EUR", + "customerReference": "refund-98765", + "transactionReference": "a1fe8db5-69a4-4b4d-a704-94ac2570f9b0" +} +``` + +--- + +### MotoReversalRequest {#motoReversalRequest} + +`MotoReversalRequest` Object + +Object used by the [`POST /moto/reversal`](restendpoints#moto-operations-no-reader) endpoint to **reverse (void)** a previous MOTO operation without using a reader. The operation is linked via `originalGuid`. + +**Properties** + +| Property | Description | +| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `originalGuid` Required
*String* | GUID of the original MOTO sale to be reversed (voided). Must correspond to a transaction that can be reversed according to acquirer rules. | +| `amount` Required
*String* | Amount to reverse, in major units, using a dot (`.`) as decimal separator. Must match `^\d+(\.\d+)?$`. In many cases this will be the full original amount, though acquirer-level rules may apply. | +| `currency` Optional
[*Currency*](#currency) | ISO 4217 3-character currency code. If provided, it must respect `minLength = 3` and `maxLength = 3` and may be expected to match the original transaction currency. | +| `customerReference`
*String* | Merchant-defined reference to identify the reversal in internal systems. | +| `transactionReference`
*String* | Unique identifier for this reversal generated by the integrator. | + +**Code example** + +```json +{ + "originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de", + "amount": "20.00", + "currency": "EUR", + "customerReference": "void-001", + "transactionReference": "4d7b1a2c-5bfd-4a30-9b6f-123456789abc" +} +``` + +--- + +### Moto Transaction Response Object {#motoTransactionResponse} + +`MotoTransactionResponse` Object + +Generic object representing the response of the MOTO no-reader endpoints: + +* `/moto/sale` → `type = "motoSaleResponse"` +* `/moto/refund` → `type = "motoRefundResponse"` +* `/moto/reversal` → `type = "motoReversalResponse"` + +The exact shape is very similar across these operations; some fields (such as `originalGuid` or `f25`) are only present when relevant (for example, in refund or reversal responses). + +**Properties** + +| Property | Description | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `type`
*String* | Response type. Typical values: `"motoSaleResponse"`, `"motoRefundResponse"`, `"motoReversalResponse"`. | +| `httpStatus`
*Number* | HTTP status code returned by Cloud API for the operation (for example, `200`). | +| `guid`
*String* | Unique identifier of the newly processed MOTO operation. | +| `originalGuid`
*String* | GUID of the original operation associated with this response (present in refund and reversal responses). | +| `amount`
*String* | Amount of the operation in major units (for example, `"20.00"`). | +| `currency`
[*Currency*](#currency) | ISO 4217 currency code (for example, `"EUR"`, `"USD"`). | +| `issuerResponseCode`
*String* | Response code returned by the issuer (for example, `"00"` for approved). | +| `issuerResponseText`
*String* | Human-readable description of the issuer response (for example, `"Successful"`). | +| `approvalCode`
*String* | Authorization code for the operation. | +| `maskedCardNumber`
*String* | Masked PAN of the card used (for example, `"************3555"`). | +| `cardTypeName`
*String* | Card brand/type (for example, `"Visa"`, `"Mastercard"`), when available. | +| `expiryDateMMYY`
*String* | Card expiry date in `MMYY` format (for example, `"0927"`), when available. | +| `acquirerTid`
*String* | Acquirer terminal ID associated with the operation. | +| `transactionReference`
*String* | Optional transaction reference echoed back or set for reconciliation. | +| `serverDateTime`
*String* | Server timestamp in gateway internal format (for example, `"20251202112902223"`). | +| `terminalDateTime`
*String* | Payment environment/terminal timestamp in internal format (for example, `"20251202112901000"`). | +| `f25`
*String* | Reason code field used in some flows (for example, `"4000"` for customer cancellation in a reversal). Mainly relevant for `/moto/reversal`. | + +**Code example – `motoSaleResponse`** + +```json +{ + "type": "motoSaleResponse", + "httpStatus": 200, + "amount": "20.00", + "currency": "EUR", + "issuerResponseCode": "00", + "issuerResponseText": "Successful", + "guid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67", + "maskedCardNumber": "************3555", + "approvalCode": "123456", + "acquirerTid": "ACQUIRER_TID", + "serverDateTime": "20251202112902223", + "terminalDateTime": "20251202112901000" +} +``` + +**Code example – `motoRefundResponse` (partial refund)** + +```json +{ + "type": "motoRefundResponse", + "httpStatus": 200, + "amount": "5.00", + "currency": "EUR", + "guid": "e4a29ca0-cf79-11f0-95b2-770b7d1d8e67", + "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67", + "issuerResponseText": "Successful", + "maskedCardNumber": "************3555", + "approvalCode": "123456", + "cardTypeName": "Visa", + "expiryDateMMYY": "0927", + "acquirerTid": "ACQUIRER_TID", + "serverDateTime": "20251202122448234", + "terminalDateTime": "20251202122448000" +} +``` + +**Code example – `motoReversalResponse`** + +```json +{ + "type": "motoReversalResponse", + "httpStatus": 200, + "amount": "20.00", + "currency": "EUR", + "guid": "3f7772a0-cf88-11f0-b588-a122fae316de", + "originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de", + "issuerResponseCode": "00", + "issuerResponseText": "Successful", + "maskedCardNumber": "************3555", + "cardTypeName": "Visa", + "expiryDateMMYY": "0927", + "f25": "4000", + "acquirerTid": "ACQUIRER_TID", + "serverDateTime": "20251202140733578", + "terminalDateTime": "20251202140733000" +} +``` \ No newline at end of file