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:
- Your backend creates a terminal session — send a
POST /terminal-sessionsrequest with the charge amount and the target terminal identifier. - Stitch delivers the session to the terminal — the device wakes up and displays the payment amount. No manual action is needed on the terminal.
- The customer presents their card — the terminal handles the EMV card acceptance flow (tap, insert, or swipe).
- Stitch processes the payment — the transaction is routed to the issuer through the card network for authorisation.
- The terminal displays the result — the customer sees whether the payment was approved or declined.
- Your backend receives a webhook — a
terminal-session.successorterminal-session.failureevent is sent to your registered endpoint. - (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
| Field | Type | Required | Description |
|---|---|---|---|
intent.charge.amount | number | Yes | Charge amount in major units (e.g. 350.5 for R350.50). |
intent.charge.currency | string | Yes | ISO 4217 currency code. Only ZAR is currently supported. |
intent.charge.cashback.amount | number | No | Cashback amount in major units, dispensed to customer on top of the charge. |
intent.charge.cashback.currency | string | No | Cashback currency. Only ZAR. |
terminal | string | Yes | The Stitch-assigned terminal identifier where the payment should be processed. |
nonce | string | Yes | A unique identifier for idempotency. Reusing a nonce returns an error. |
externalReference | string | No | Your own reference for this session. Not enforced as unique. |
metadata | object | No | Arbitrary key-value pairs to attach to the session. |
customer.name | string | No | Customer name. |
customer.email | string | No | Customer email. |
customer.phone | string | No | Customer phone including country code (e.g. +27612345678). |
customer.identifyingDocument | object | No | Government-issued identity or travel document. Required sub-fields below. |
customer.identifyingDocument.type | string | Yes | One of identity_document, passport, or temporary_residence. |
customer.identifyingDocument.country | string | Yes | ISO 3166-1 alpha-2 country code (e.g. ZA). |
customer.identifyingDocument.number | string | Yes | The document number. |
customer.externalReference | string | No | Your identifier for this customer. |
The terminal identifier is assigned by Stitch on registration of terminal devices. Following registration, identifiers may be obtained in one of two ways:
- Directly from Stitch, or
- Programmatically over API, listing terminals via the Weave Terminal Retrieval endpoint.
Map these identifiers to the respective device serial numbers, to be able to initiate terminal sessions for the respective devices.
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
- Basic Charge
- Charge with Cashback
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"
}'
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",
"cashback": {
"amount": 50,
"currency": "ZAR"
}
}
},
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"nonce": "9599a853-4333-4359-89de-658bfc86773a"
}'
The total amount charged to the customer will be R400.50 (R350.50 charge + R50.00 cashback).
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
| Status | Description |
|---|---|
pending | Session is awaiting completion on the terminal. |
success | Payment was processed successfully. The outcome field contains the charge details. |
failure | Session did not complete. The failureReason field explains why. |
Failure Reasons
| Reason | Description |
|---|---|
attempt_limit_exceeded | The maximum number of failed attempts for this terminal session has been reached. |
expired | Session timed out before the terminal could complete. |
cancelled_by_terminal | Operator cancelled the payment on the device. |
offline_terminal | Terminal was not connected when the session was created. |
busy_terminal | Terminal 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.
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": "ad8e3872-cd17-41fe-88ea-605b9d441ec7",
"type": "in_person_card",
"card": {
"bin": "42424242",
"last4": "4242",
"expiry": {
"month": "12",
"year": "29"
},
"network": "visa",
"fundingType": "debit",
"issuer": {
"name": "standard bank",
"country": "ZA"
}
},
"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>"
- Pending
- Success
- Failure
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"
}
The payment was processed successfully. The outcome.charge field contains the full charge details.
{
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "success",
"outcome": {
"charge": {
"id": "ad8e3872-cd17-41fe-88ea-605b9d441ec7",
"type": "in_person_card",
"card": {
"bin": "42424242",
"last4": "4242",
"expiry": {
"month": "12",
"year": "29"
},
"network": "visa",
"fundingType": "debit",
"issuer": {
"name": "standard bank",
"country": "ZA"
}
},
"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"
}
The session failed. The failureReason field explains why. There is no outcome field since no charge was created.
{
"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"
}
Listing Terminal Sessions
GET https://api.stitch.money/v2/terminal-sessions
Use this endpoint to look up terminal sessions you've previously created — most commonly to find a session by the externalReference you assigned at creation time, or by its nonce. This is useful when reconciling a payment against an order in your own system without needing to track Stitch-generated session IDs.
Both charge and disbursement terminal sessions are returned by this endpoint; filter or inspect each result's intent field to distinguish them.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
externalReference | string | No | Filter by the externalReference you supplied when creating the session. The same value may have been used on multiple sessions, so the response is a list. |
nonce | string | No | Filter by the nonce used at creation time. Nonces are unique per client, so at most one session is returned. |
limit | integer | No | Page size, between 1 and 10. Defaults to 10. |
offset | integer | No | Number of items to skip for pagination. Defaults to 0. |
Filters are mutually exclusive. If both externalReference and nonce are supplied, externalReference takes precedence. Without a filter, all sessions for the authenticated client are returned, ordered most recent first.
Example
curl "https://api.stitch.money/v2/terminal-sessions?externalReference=order-42198&limit=10" \
-H "Authorization: Bearer <access_token>"
Response
The response contains an array of terminal sessions (each with the same structure as a single fetched session) plus a page object for pagination.
{
"data": [
{
"id": "ts_i0sspe2eZvKYDS2Cd31jOCgZ",
"terminal": "dGVybWluYWwvMmNiN2NhMTQtNGQwYi00ODg3LTk2MWItOTRmYWYzMzJkMGYw",
"intent": {
"charge": {
"amount": 350.5,
"currency": "ZAR"
}
},
"status": "success",
"outcome": {
"charge": {
"id": "ad8e3872-cd17-41fe-88ea-605b9d441ec7",
"type": "in_person_card",
"amount": 350.5,
"currency": "ZAR",
"status": "success",
"retrievalReferenceNumber": "1234567890",
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}
},
"nonce": "9599a853-4333-4359-89de-658bfc86773a",
"externalReference": "order-42198",
"metadata": {},
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}
],
"page": {
"limit": "10",
"offset": "0",
"hasNext": false
}
}
Disbursements (standalone refunds) are modelled as terminal sessions, so the same list endpoint is used to query them. Set the externalReference you supplied on the disbursement session, then inspect each result's intent.disbursement (and, when settled, outcome.disbursement) field. See Disbursements (Standalone Refunds) for the disbursement object structure.
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
| Field | Type | Required | Description |
|---|---|---|---|
charge | string | No | The charge ID to refund (from outcome.charge.id). Either charge or terminalSession must be provided. |
terminalSession | string | No | The terminal session ID to refund. Either charge or terminalSession must be provided. |
amount | number | Yes | Refund amount in major units (e.g. 30.0 for R30.00). Can be less than the original charge for a partial refund. |
currency | string | Yes | ISO 4217 currency code. Only ZAR. |
reason | string | Yes | One of fraud, requested_by_customer, duplicate_charge. |
nonce | string | Yes | Unique identifier for idempotency. |
metadata | object | No | Arbitrary key-value pairs. |
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_customer",
"nonce": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}'
Response
{
"id": "refund_1234567890",
"type": "card",
"charge": "charge_1Q0PsIJvEtkwdCNYMSaVuRz6",
"amount": 30.0,
"currency": "ZAR",
"reason": "requested_by_customer",
"status": "success",
"nonce": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}
Refund Statuses
| Status | Description |
|---|---|
success | Refund completed successfully. |
failure | Refund did not complete successfully. |
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>"
Listing Refunds
GET https://api.stitch.money/v2/refunds
Use this endpoint to look up refunds previously created against a charge or terminal session. To find every refund issued for a given order, first list the terminal session by your externalReference, then use the resulting terminalSession (or outcome.charge.id) to list its refunds.
Like the create endpoint, listing refunds requires the client_refund scope.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
terminalSession | string | No | Filter refunds by the terminal session ID they were issued against. |
charge | string | No | Filter refunds by the charge ID they were issued against. |
nonce | string | No | Filter refunds by the nonce supplied at refund creation. Nonces are unique per client, so at most one refund is returned. |
limit | integer | No | Page size, between 1 and 50. Defaults to 20. |
offset | integer | No | Number of items to skip for pagination. Defaults to 0. |
The refund object does not itself carry an externalReference field. To find refunds for a payment by your own reference, look up the matching terminal session first via Listing Terminal Sessions, then pass the returned session's id as the terminalSession filter on this endpoint (or pass outcome.charge.id as the charge filter).
Example
curl "https://api.stitch.money/v2/refunds?terminalSession=dGVybWluYWxTZXNzaW9uLzg2MjE3Mzg2LTA3M2QtNDQ5Yi04NjA3LWFiOWY4MTNlMGQ4MA==&limit=20" \
-H "Authorization: Bearer <access_token>"
Response
{
"data": [
{
"id": "refund_1234567890",
"type": "card",
"charge": "charge_1Q0PsIJvEtkwdCNYMSaVuRz6",
"amount": 30.0,
"currency": "ZAR",
"reason": "requested_by_customer",
"status": "success",
"nonce": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"createdAt": "2024-01-15T09:30:00Z",
"updatedAt": "2024-01-15T09:35:00Z"
}
],
"page": {
"limit": 20,
"offset": 0,
"hasNext": false
}
}
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
| Field | Type | Required | Description |
|---|---|---|---|
intent.disbursement.amount | number | Yes | Disbursement amount in major units (e.g. 150.0 for R150.00). |
intent.disbursement.currency | string | Yes | ISO 4217 currency code. Only ZAR is currently supported. |
terminal | string | Yes | The terminal identifier where the disbursement should be processed. |
nonce | string | Yes | A unique identifier for idempotency. Reusing a nonce returns an error. |
externalReference | string | No | Your own reference for this session. Not enforced as unique. |
metadata | object | No | Arbitrary 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"
}
},
"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.
Fetching and Listing Disbursements
Disbursements are terminal sessions, so the same query endpoints apply:
- Fetch by ID —
GET /v2/terminal-sessions/{id}returns the disbursement session. Theoutcome.disbursementfield is populated once the disbursement completes. See Fetching a Terminal Session. - List by external reference —
GET /v2/terminal-sessions?externalReference=...returns sessions matching your reference, including disbursements. Inspect each result'sintent.disbursementfield to identify disbursement sessions. See Listing Terminal Sessions.
There is no separate /disbursements endpoint for in-person card disbursements.