Skip to main content

Over the Air API

The Over the Air (OTA) API lets you initiate and manage in-person card payments on Stitch terminals from your backend — no interaction with the terminal itself is required. The core concept is the terminal session: a server-initiated request that tells a specific terminal to collect a card payment for a given amount. Your backend creates the session via the API, Stitch delivers it to the terminal over the air, and the terminal guides the customer through the card acceptance flow. Once the payment completes (or fails), you receive the result via webhook.

The payment flow works as follows:

  1. Your backend creates a terminal session — send a POST /terminal-sessions request with the charge amount and the target terminal identifier.
  2. Stitch delivers the session to the terminal — the device wakes up and displays the payment amount. No manual action is needed on the terminal.
  3. The customer presents their card — the terminal handles the EMV card acceptance flow (tap, insert, or swipe).
  4. Stitch processes the payment — the transaction is routed to the issuer through the card network for authorisation.
  5. The terminal displays the result — the customer sees whether the payment was approved or declined.
  6. Your backend receives a webhook — a terminal-session.success or terminal-session.failure event is sent to your registered endpoint.
  7. (Optional) Poll for the session — if you need to confirm the result or missed the webhook, fetch the session via GET /terminal-sessions/{id}.

Authentication

The OTA API uses OAuth2 client credentials. Before making any API calls, your server must request an access token from the Stitch authorization server. This token authenticates all subsequent requests and is scoped to specific permissions.

Request an access token with the client_terminal_session scope:

curl -X POST https://secure.stitch.money/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=client_terminal_session"

The response includes an access_token and expires_in value (in seconds). Store the token server-side and reuse it until it expires — there's no need to request a new token for every API call.

All subsequent API calls use Authorization: Bearer <access_token>. See the client token docs for full details.

Creating a Terminal Session

POST https://api.stitch.money/v2/terminal-sessions

To initiate a payment, send a request with the charge amount and the target terminal. Stitch delivers the session to the terminal over the air — the device wakes up, displays the payment amount, and prompts the customer to present their card. No action is needed on the terminal itself.

The API responds immediately with a pending session. The actual payment result arrives asynchronously via webhook, or you can poll for it.

Request Body

FieldTypeRequiredDescription
intent.charge.amountnumberYesCharge amount in major units (e.g. 350.5 for R350.50).
intent.charge.currencystringYesISO 4217 currency code. Only ZAR is currently supported.
intent.charge.cashback.amountnumberNoCashback amount in major units, dispensed to customer on top of the charge.
intent.charge.cashback.currencystringNoCashback currency. Only ZAR.
terminalstringYesThe terminal identifier where the payment should be processed.
noncestringYesA unique identifier for idempotency. Reusing a nonce returns an error.
externalReferencestringNoYour own reference for this session. Not enforced as unique.
metadataobjectNoArbitrary key-value pairs to attach to the session.
customer.namestringNoCustomer name.
customer.emailstringNoCustomer email.
customer.phonestringNoCustomer phone including country code (e.g. +27612345678).
customer.identifyingDocumentobjectNoGovernment-issued identity or travel document. Required sub-fields below.
customer.identifyingDocument.typestringYesOne of identity_document, passport, or temporary_residence.
customer.identifyingDocument.countrystringYesISO 3166-1 alpha-2 country code (e.g. ZA).
customer.identifyingDocument.numberstringYesThe document number.
customer.externalReferencestringNoYour identifier for this customer.
tip

The nonce field prevents duplicate payments. Each terminal session must have a unique nonce — reusing one returns an error. Generate a unique value (e.g. a UUID) for each payment attempt.

Examples

curl -X POST https://api.stitch.money/v2/terminal-sessions \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"nonce": "9599a853-4333-4359-89de-658bfc86773a"
}'

Response

The API returns immediately with the session in pending status. Store the session id — you'll need it to match incoming webhooks or to poll for the result.

{
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "pending",
"nonce": "9599a853-4333-4359-89de-658bfc86773a",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Terminal Session Statuses

StatusDescription
pendingSession is awaiting completion on the terminal.
successPayment was processed successfully. The outcome field contains the charge details.
failureSession did not complete. The failureReason field explains why.

Failure Reasons

ReasonDescription
expiredSession timed out before the terminal could complete.
cancelled_by_terminalOperator cancelled the payment on the device.
offline_terminalTerminal was not connected when the session was created.
busy_terminalTerminal was already processing another session.

Handling Webhooks

Webhooks are the primary way your system learns the outcome of a terminal session. When the customer completes (or fails to complete) the card payment, Stitch sends an HTTP POST to your registered webhook endpoint.

There are two event types:

  • terminal-session.success — the payment was processed successfully.
  • terminal-session.failure — the session failed for one of the reasons listed above.
tip

Always verify the webhook signature before trusting the payload. Use the session id from the webhook to match it back to the payment you initiated. See the webhooks documentation for signature verification details and setup.

Success Webhook

On success, data.outcome.charge contains the full charge details — card information, retrieval reference number, and settlement status. Use the charge.id if you need to issue a refund later. The retrievalReferenceNumber is the bank reference you'll need for reconciliation.

{
"id": "wev_1o1jU6Y8whOrGcmXzfeDq3wQ",
"type": "terminal-session.success",
"data": {
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "success",
"outcome": {
"charge": {
"id": "charge_1Q0PsIJvEtkwdCNYMSaVuRz6",
"type": "in_person_card",
"card": {
"id": "Y2FyZC9mYTY5ZGQ3Yi03OTY1LTQ3MzUtOWZiNy1mZjEwZTY2OTAwMjY=",
"bin": "42424242",
"last4": "4242",
"expiry": {
"month": "12",
"year": "29"
},
"network": "visa",
"fundingType": "debit",
"issuer": {
"name": "standard bank",
"country": "ZA"
},
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
},
"capture": {
"method": "automatic"
},
"retrievalReferenceNumber": "1234567890",
"networkTransactionIdentifier": "1234567890",
"amount": 350.5,
"currency": "ZAR",
"status": "success",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}
},
"nonce": "9599a853-4333-4359-89de-658bfc86773a",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
},
"createdAt": "2024-01-15T09:36:00Z"
}

Failure Webhook

On failure, data.failureReason tells you what went wrong. There is no outcome field since no charge was created. Your system should update the order status accordingly — in most cases you can prompt the customer to retry or create a new session.

{
"id": "wev_3p9xA2mQ6LfHjR3kUb1tW7nP",
"type": "terminal-session.failure",
"data": {
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "failure",
"failureReason": "expired",
"nonce": "9599a853-4333-4359-89de-658bfc86773a",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
},
"createdAt": "2024-01-15T09:36:00Z"
}

Fetching a Terminal Session

GET https://api.stitch.money/v2/terminal-sessions/{id}

While webhooks are the recommended way to receive results, you can also poll this endpoint as a fallback — for example, if your webhook endpoint was temporarily unavailable or you need to confirm the current state of a session.

The response is a terminal session object in its current state — pending, success, or failure — with the same structure as the webhook data field.

curl https://api.stitch.money/v2/terminal-sessions/ts_i0sspe2eZvKYDS2Cd31jOCgZ \
-H "Authorization: Bearer <access_token>"

The session has been created but the terminal hasn't completed the payment yet.

{
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "pending",
"nonce": "9599a853-4333-4359-89de-658bfc86773a",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:30:00Z"
}

Refunds

POST https://api.stitch.money/v2/refunds

To refund a completed payment, send a request with the charge ID (or terminal session ID) and the amount to return. You can issue a full refund for the original amount, or a partial refund for a lesser amount. Multiple partial refunds against the same charge are supported up to the original charge amount.

Refunds require the client_refund scope, which is separate from the terminal session scope. Request an access token with both scopes if you need to create sessions and issue refunds.

Request Body

FieldTypeRequiredDescription
chargestringNoThe charge ID to refund (from outcome.charge.id). Either charge or terminalSession must be provided.
terminalSessionstringNoThe terminal session ID to refund. Either charge or terminalSession must be provided.
amountnumberYesRefund amount in major units (e.g. 30.0 for R30.00). Can be less than the original charge for a partial refund.
currencystringYesISO 4217 currency code. Only ZAR.
reasonstringYesOne of fraud, requested_by_user, duplicate_charge.
noncestringYesUnique identifier for idempotency.
externalReferencestringNoYour own reference for this refund.
metadataobjectNoArbitrary key-value pairs.
tip

You can identify the charge to refund using either the charge ID (from outcome.charge.id in the success webhook) or the terminalSession ID. Using the charge ID is more precise if a session could theoretically produce multiple charges in future.

Example

curl -X POST https://api.stitch.money/v2/refunds \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"charge": "charge_1Q0PsIJvEtkwdCNYMSaVuRz6",
"amount": 30.0,
"currency": "ZAR",
"reason": "requested_by_user",
"nonce": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}'

Response

{
"id": "refund_1234567890",
"type": "card",
"charge": "charge_1Q0PsIJvEtkwdCNYMSaVuRz6",
"amount": 30.0,
"currency": "ZAR",
"reason": "requested_by_user",
"status": "success",
"nonce": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}

Refund Statuses

StatusDescription
successRefund completed successfully.
failureRefund failed. See the failure object for details.

You can check the status of a refund at any time:

curl https://api.stitch.money/v2/refunds/refund_1234567890 \
-H "Authorization: Bearer <access_token>"

Disbursements (Standalone Refunds)

POST https://api.stitch.money/v2/terminal-sessions

Disbursements (Standalone Refunds) let you push funds to a customer's card without referencing a prior charge. Unlike charge-linked refunds, which reverse a specific payment, a disbursement uses a terminal session to collect the customer's card details directly from their physical card. The terminal prompts the customer to present their card, reads the card details, and Stitch pushes the refund funds to that card.

This is useful for scenarios like goodwill refunds, compensation, or returns where the original transaction isn't available in Stitch — for example, if the charge was processed on a different system.

Disbursements use the same POST /v2/terminal-sessions endpoint as charges, but with an intent.disbursement payload instead of intent.charge.

Request Body

FieldTypeRequiredDescription
intent.disbursement.amountnumberYesDisbursement amount in major units (e.g. 150.0 for R150.00).
intent.disbursement.currencystringYesISO 4217 currency code. Only ZAR is currently supported.
terminalstringYesThe terminal identifier where the disbursement should be processed.
noncestringYesA unique identifier for idempotency. Reusing a nonce returns an error.
externalReferencestringNoYour own reference for this session. Not enforced as unique.
metadataobjectNoArbitrary key-value pairs to attach to the session.

Example

curl -X POST https://api.stitch.money/v2/terminal-sessions \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"intent": {
"disbursement": {
"amount": 150.0,
"currency": "ZAR"
}
},
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"nonce": "b7e3f1a2-9c84-4d5e-a6b0-8f2d4e6c0a1b"
}'

Response

The API returns immediately with the session in pending status, just like a charge session. The actual result arrives asynchronously via webhook or by polling.

{
"id": "ts_k3mRvN8pLqWxYZ5Af72jTBcE",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"disbursement": {
"amount": 150.0,
"currency": "ZAR"
}
},
"status": "pending",
"nonce": "b7e3f1a2-9c84-4d5e-a6b0-8f2d4e6c0a1b",
"metadata": {},
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:00:00Z"
}

Successful Session

On success, the session status changes to success and the outcome.disbursement field contains the full disbursement details — card information, retrieval reference number, and the amount pushed to the card.

{
"id": "ts_k3mRvN8pLqWxYZ5Af72jTBcE",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"disbursement": {
"amount": 150.0,
"currency": "ZAR"
}
},
"status": "success",
"outcome": {
"disbursement": {
"id": "disb_2R1QtJKwFulxeD4Bg83kUCdF",
"type": "in_person_card",
"card": {
"id": "Y2FyZC9mYTY5ZGQ3Yi03OTY1LTQ3MzUtOWZiNy1mZjEwZTY2OTAwMjY=",
"bin": "42424242",
"last4": "4242",
"expiry": {
"month": "12",
"year": "29"
},
"network": "visa",
"fundingType": "debit",
"issuer": {
"name": "standard bank",
"country": "ZA"
},
"metadata": {},
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:05:00Z"
},
"retrievalReferenceNumber": "9876543210",
"amount": 150.0,
"currency": "ZAR",
"status": "success",
"metadata": {},
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:05:00Z"
}
},
"nonce": "b7e3f1a2-9c84-4d5e-a6b0-8f2d4e6c0a1b",
"metadata": {},
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-15T10:05:00Z"
}

Webhooks

Disbursement sessions use the same terminal-session.success and terminal-session.failure webhook events as charge sessions. The only difference is that the outcome field contains a disbursement object instead of a charge object.

See Handling Webhooks for details on webhook setup and signature verification.