Appture Pay Documentation

Using this Doc

Getting Started

To get started:

  1. Register an account at https://sandbox.appturepay.com/.
  2. Wait for approval. An administrator will activate your account and load sandbox funds before you can start testing.
  3. Once approved, sign in to the sandbox environment and create an API client for your integration. Enable Sandbox Mode and configure its sandbox callback and redirect URLs as needed.

Once you are done, you should have an API Client ID and Client Secret. Now you can continue with our tutorials.

Before going live, create your production Appture Pay account and complete the FICA processes required for activation.


API URL

Use the API base URL for your environment:

For example, sandbox authentication uses POST https://sandbox.appturepay.com/api/auth/access_token. Use credentials issued in the same environment as your API requests.

For further details, see each Endpoint's document below.


Postman Collections

Download our integration collection to explore authentication, transactions, recurring transactions, deliveries and payouts in Postman. It includes saved response examples where available and is generated as part of each release deployment.

Download Integration Collection

To use a collection:

  1. Import the downloaded JSON file into Postman.
  2. Set the collection's base_url variable to https://sandbox.appturepay.com/api for sandbox testing, without a trailing slash. For production, use https://api.appturepay.com.
  3. Set client_id and client_secret, then authenticate using the appropriate auth/access_token example.
  4. Copy the returned token into access_token and fill in resource variables such as transaction_id before sending requests.

Start with a Sandbox Client as described below. Code samples are also available under each endpoint and method in this guide.


Sandbox Mode

The sandbox environment is available at https://sandbox.appturepay.com/. Register there and wait for an administrator to approve and activate your account and load sandbox funds.

After activation:

  1. Create a new API client, or edit an existing client, in your sandbox account.
  2. Enable its Sandbox Mode checkbox and set the client's sandbox callback and redirect URLs as needed.
  3. Send test API requests to https://sandbox.appturepay.com/api/ using your sandbox client's credentials.

Sandbox Android Apps

Download the sandbox app for your integration and install it on your Android test device. Sign in with your approved sandbox account and use the sandbox API environment for app-based testing.

Download Appture Pay Sandbox APK

Download iKWAZA Sandbox APK

Sandbox Testing Cards

Use the current sandbox testing card details for card payment tests. This reference contains the testing card information; use it when preparing your test cases.


API Testing

You can run tests and preview the response via this documentation. Open the documentation in the sandbox environment when testing against the sandbox API. To make use of this feature you will need to authenticate by entering your sandbox Client ID and Secret, in the Auth Access Token Endpoint's API Testing section, and authenticating. The access token is saved to the session so you can test each endpoint after authenticating.


Need Help?

If you are having trouble using this Doc, or found bugs/errors, please get in touch:

support@appturepay.com

Tutorials and Guides

Payment Gateway

Follow these steps to make use of the Payment Gateway from an eCommerce type website:

  1. Get an access token by authenticating your Client using the auth/access_token endpoint
  2. Create a transaction via the transaction endpoint
  3. Redirect the user to the Payment Gateway with the transaction id as parameter
  4. Appture Pay's API will do a server-to-server call to your set callback URL with the result of the transaction
  5. The Payment Gateway then redirects the user to your set redirect URL
include_once "includes/WebRequest.php";
include_once "includes/AppturePayAPI.php";

// 1. In order to authenticate as a Client, simply instantiate the AppturePayAPI
// class with your client details. The constructor will automatically check if
// we have authenticated previously, and access token details were saved to the
// session, or if it should authenticate. If not authenticated it will do so and
// save the access token to the PHP session.

$clientId = "{your client id}";
$clientSecret = "{your client secret}";

// get an instance of the AppturePayAPI class
$api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

// make sure we are successfully authenticated
if( $api->getSession() !== null ) {

    // 2. After authenticating we can create the transaction.

    // variable for keeping the response
    $transactionPostResponse = null;

    // values to pass as request body
    $data = array(
        "client_id" => $clientId,
        "transaction_type" => "DB",
        "recurring_term" => "1",
        "reference" => "TRANS01",
        "description" => "Test transaction",
        "total" => "999.99"
    );

    // then make the call to the transaction/{id} endpoint using the POST method
    $transactionPostResponse = $api->transactionPost($data);

    // make sure it was a success
    if($transactionPostResponse !== null && $transactionPostResponse["success"]) {

        // 3. Now we can redirect the user to the Payment Gateway.

        header("Location: https://www.appturepay.com/gateway/?id={$transactionPostResponse['data']['identifier']}");

    }

}

...

// 4. Receiving the server-to-server callback request from Appture Pay, after successful payment.

$transactionId = filter_input(INPUT_GET, "id");
$transactionReference = filter_input(INPUT_GET, "reference");

if($transactionId && $transactionReference) {

    // Get your local order against which to check the transaction.
    $order = array();

    // Get the transaction from AppturePay.

    // get an instance of the AppturePayAPI class
    $api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

    // make sure we are successfully authenticated
    if( $api->getSession() !== null ) {

        // then make the call to the transaction/{id} endpoint using the GET method
        $transactionGetResponse = $api->transactionGetSpecific($transactionId);

        // make sure it was a success
        if($transactionGetResponse !== null && $transactionGetResponse["success"]) {

            // Check the transaction's status and also that its total matches
            // with your local order.

            if($transactionGetResponse["data"]["status"] === "SUCCESS") {

                // Do something specific on success, ie update redeemed voucher
                // codes, send success email etc.

            }

            // Update your local order to indicate the status of the payment.

        }

    }

}

Idempotent Transaction Creation

If a transaction request times out, the transaction may already have been created. The optional Idempotency-Key header on POST /transaction lets you retry the same creation request without creating another transaction.

  1. Authenticate your API client using auth/access_token.
  2. Create and save one payment-attempt record containing a unique key and the original transaction details before making the first request.
  3. Send the saved key as the Idempotency-Key header. Reuse the same attempt for concurrent requests, timeouts and retries.
  4. Save the returned data.identifier against the attempt and order.
  5. Confirm payment separately with GET /transaction/{identifier} and your callback handling.

Choosing and reusing a key

Keys are case-sensitive and must contain 1–128 visible ASCII characters without spaces. Send a single header value. A UUID saved with the attempt or an identifier such as sale-10482-payment-1 is suitable. Use a new key only when intentionally creating another transaction.

Keep the original request details unchanged on retries. JSON property order does not matter, and ordinary decimal encodings such as 125, 125.0 and "125.00" compare equally. Do not assume that omitting a field is equivalent to explicitly sending its default value.

POST https://api.appturepay.com/transaction
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Idempotency-Key: sale-10482-payment-1

{
  "client_id": "YOUR_CLIENT_ID",
  "reference": "SALE-10482",
  "description": "In-store purchase",
  "transaction_type": "DB",
  "day_of_debit": 1,
  "total": "125.00",
  "gateway": "DEFAULT"
}
// Run on your backend, using an access token from auth/access_token.
$accessToken = "{access token}";

// Save this attempt in your database BEFORE sending its first request.
// For retries, load the SAME key and details instead of creating a new attempt.
$attempt = array(
    "key" => "sale-10482-payment-1",
    "data" => array(
        "client_id" => "{your client id}",
        "reference" => "SALE-10482",
        "description" => "In-store purchase",
        "transaction_type" => "DB",
        "day_of_debit" => 1,
        "total" => "125.00",
        "gateway" => "DEFAULT",
    ),
);

$request = curl_init("https://api.appturepay.com/transaction");
curl_setopt_array($request, array(
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => array(
        "Authorization: Bearer " . $accessToken,
        "Content-Type: application/json",
        "Idempotency-Key: " . $attempt["key"],
    ),
    CURLOPT_POSTFIELDS => json_encode($attempt["data"]),
));
$rawResponse = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
$transportError = curl_error($request);
curl_close($request);

if ($rawResponse === false) {
    // Creation may have committed. Keep the saved attempt for a retry.
    throw new RuntimeException("Creation result unknown: " . $transportError);
}
$result = json_decode($rawResponse, true);
if ($status !== 200 || ($result["success"] ?? false) !== true) {
    // idempotency_request_in_progress: respect Retry-After, then retry unchanged.
    // idempotency_key_conflict: reconcile the saved attempt and original details.
    // Do not automatically generate a new key to bypass either conflict.
    throw new RuntimeException("Check the saved payment attempt: " . $rawResponse);
}

$identifier = $result["data"]["identifier"];
// Save the identifier against the attempt and order before continuing checkout.
// Use GET /transaction/{identifier} for current status before marking an order paid.

Responses and retry handling

ResultWhat to do
HTTP 200, Idempotency-Replayed: falseThe transaction was created. Save its identifier.
HTTP 200, Idempotency-Replayed: trueThe original successful creation response was returned. Reuse its identifier; another transaction was not created.
HTTP 409, idempotency_key_conflictThe key was already used with different accepted details. Check the original attempt instead of automatically generating a new key.
HTTP 409, idempotency_request_in_progressRespect Retry-After (currently 2 seconds), then retry with the same key and details.
HTTP 400, invalid_idempotency_keySend one valid, nonempty header value.
Timeout, lost response or uncertain server failureKeep the saved attempt. Retry unchanged in the same client, merchant, mode and environment context.
A replay is the original creation snapshot, even if the transaction has since been paid or deleted. Use GET /transaction/{identifier} to check its current status. Replaying creation does not collect payment again or confirm that payment succeeded.

Scope and compatibility

Keys are scoped to the authenticated OAuth client, actor/user context, mode and sandbox/live environment. POS-mode keys also belong to their POS session. Refreshing a token within the same context keeps its key scope; switching clients, environments or POS sessions can create a separate scope. Reconcile an existing attempt before retrying from another context.

Authentication, permissions and creation validation still apply to retries. A validation or creation failure before commit does not save a successful result, so a corrected request can reuse its key. Successful keys have no automatic expiry.

Requests without Idempotency-Key retain the existing creation behaviour without duplicate prevention. This header applies to transaction creation; payment preparation, collection and callback processing retain their existing rules.


Callbacks and Transaction Statuses

Configure your API client's callback_uri for live payments and sandbox_callback_uri for sandbox payments. Use a publicly reachable HTTPS endpoint that accepts server-to-server requests without a browser login. The browser's redirect URL is a separate setting.

HTTP method and callback fields

Transaction result callbacks use HTTP GET, with URL-encoded query parameters and no JSON request body. Existing query parameters on your callback URL are preserved. For example:

GET /payments/callback?type=DB&action=DEBIT&id=0123456789abcdef0123456789abcdef&reference=SALE-10482&status=SUCCESS&message=Payment+completed HTTP/1.1
Host: merchant.example.com
Query parameterMeaning
typeTransaction type, such as DB (debit), PA (preauthorization), or EFT. Recurring debit notifications use RECURRING_DB. This is separate from the selected gateway.
actionDEBIT on card/app result callbacks; EFT on the EFT result path. Recurring configuration notifications use a different event format.
idThe transaction's public identifier, used for GET /transaction/{identifier}.
referenceYour reference saved when creating the transaction.
statusThe result reported when the notification was created. Fetch the transaction for its current status.
messageHuman-readable status information. URL-decode it; do not use its wording as a machine-readable status.
recurring_transaction_idIncluded by recurring/card/app paths when applicable; may be absent for a once-off payment or EFT callback.
activeRecurring notifications additionally include the recurring instruction's active flag.

The transaction callback does not carry a verified amount or a complete transaction object. Recurring configuration updates are distinct: type=RECURRING_DB, action=UPDATED, with recurring_transaction_id, reference, description and active; a transaction id may be absent.

Authentication and verification

Merchant callbacks currently have no HMAC signature, signing secret, or Authorization header. Treat every callback parameter as untrusted input. Payment-provider webhook verification performed inside Appture Pay does not add a signature to the callback delivered to your server.

  1. Validate the supplied id as a public identifier and find the saved payment attempt in your backend. Reject identifiers unrelated to your orders.
  2. Use your backend's owning-client credentials to obtain an access token, then call GET /transaction/{identifier} with Authorization: Bearer {access_token} in the same environment.
  3. Require a successful lookup and compare data.identifier, data.client_id, data.reference and data.total with your saved attempt. Compare amounts in integer cents.
  4. For a once-off debit sale, fulfil only when the verified transaction status is SUCCESS. A successful preauthorization (PA) still requires capture; do not treat it as a captured debit.
  5. Persist the verified result and fulfil the order exactly once using a database transaction or equivalent duplicate protection. Callback retries and later status changes must be safe.

Keep client secrets and access tokens on your server. A redirect, app handoff, callback query string, or HTTP 200 from a lookup alone does not prove payment. Check the API response's success flag and transaction fields. The POS payment confirmation example describes the same reconciliation flow.

Acknowledgement, retries and missing notifications

Return HTTP 200 OK once the notification has been handled or durably accepted for processing. The delivery worker requires exactly 200; redirects and other status codes count as failures. It does not follow redirects. If you acknowledge before verification, persist the job and retry failed lookups internally.

Failed deliveries retry at one-minute intervals for the first ten retries, ten-minute intervals for the next six, and hourly for the final twenty-four: one initial attempt plus up to forty retries. Handle duplicate and delayed notifications. Do not assume every cancellation, failure, or abandoned checkout generates a callback; reconcile unresolved attempts through authenticated lookup or polling.

Exact transaction status values

ValueMeaning and handling
OPENCreated and unresolved. Payment is not confirmed; it may still be in progress.
SUCCESSSuccessful transaction result. Verify ownership, reference, amount and transaction type before fulfilment.
FAILEDPayment failed, for example a declined or unverified payment. Do not fulfil.
ERRORGateway or processing error. Do not fulfil; reconcile before starting another payment.
USER_CANCELLEDUser cancellation. This is the exact cancellation value, rather than CANCELLED. It is not a refund or proof that an external payment attempt was voided.
CLOSEDDeclared legacy status for a transaction closed and replaced. Do not treat it as success or as an expiry status.
ExpiryThere is currently no EXPIRED transaction status or automatic transaction expiry contract. An abandoned checkout may remain OPEN. Provider checkout/session expiry does not define a separate transaction status.

These values describe the transaction's status. POS collection_state (for example READY, DISPATCHED or CANCELLED) and webhook delivery status are separate fields. Always re-read the transaction when reconciling; a callback is a snapshot, and a cancellation can be followed by an explicitly resumed payment flow.


Transaction Management

Save each order's payment attempt, reference, public transaction identifier, client ID, expected amount and idempotency key on your backend. Examples below show paths relative to your API base URL: https://sandbox.appturepay.com/api/ for sandbox and https://api.appturepay.com/ for production. Use an access token issued in that environment.

Lookup by identifier or reference

The direct lookup is GET /transaction/{identifier}, authenticated with Authorization: Bearer {access_token}. A confidential API client can read its own transactions. Use the saved public identifier for payment reconciliation; a merchant reference is not an identifier and is not guaranteed unique.

Reference searches use the transaction list endpoint's exact-match filter, filter[]=reference=SALE-10482. The list endpoint requires owner scope; a confidential token alone does not grant list access. With an authorized owner token, for example:

GET /transaction?filter%5B%5D=reference%3DSALE-10482&limit=25&page=0 HTTP/1.1
Authorization: Bearer {owner_access_token}

Read data.list, inspect all matching attempts and follow zero-based pagination. Owner searches can include multiple clients' transactions: verify each result's client_id, reference, amount and environment. Use filterAnd[] when combining exact conditions with AND; multiple filter[] entries use OR. There is no separate /transaction/reference/{reference} endpoint or direct ?reference=... lookup contract.

For a backend using only client credentials, keep your own reference-to-identifier mapping. If creation times out before you receive an identifier, replay the original creation request with its saved idempotency key.

Cancellation and expiry support

Cancellation uses the transaction update endpoint with a sparse JSON body:

PUT /transaction/{identifier} HTTP/1.1
Authorization: Bearer {authorized_user_or_pos_access_token}
Content-Type: application/json

{"status":"USER_CANCELLED"}

Authorization, current transaction state and collection state still apply. Standard non-POS cancellation requires an authenticated user context; a confidential client-credentials token alone does not grant this update operation. POS cancellation uses the authorized receiving session. Normal updates allow OPEN and USER_CANCELLED; callers cannot mark an order paid by setting SUCCESS. Inspect both the HTTP status and JSON success, then fetch the transaction again to confirm the result.

For POS collection, cancellation is allowed before collection while the transaction is OPEN, the collection is READY, the caller owns the receiving merchant/session and no external payment attempt has started. A dispatched or uncertain payment can return HTTP 409 payment_confirmation_required; reconcile its result before trying to cancel or starting another attempt. Other POS ownership/session restrictions also apply.

USER_CANCELLED records cancellation in Appture Pay; it does not promise a provider void, refund or permanent prevention of later payment. There is no general transaction expiry endpoint, expiry timestamp input, or EXPIRED status. Enforce your order's checkout deadline in your application and reconcile outstanding payments, including late successes, before deciding whether to fulfil or arrange a refund.

DELETE /transaction/{identifier} permanently deletes an eligible OPEN transaction under the applicable owner permissions and collection guards. Deletion is not a cancellation or expiry mechanism. Preauthorization capture/reversal operations also have their own rules and are not a general cancellation API.

Idempotency and safe retries

The existing Idempotent Transaction Creation tutorial covers request examples, key format, response headers and conflicts. Use Idempotency-Key on POST /transaction, save it before sending, and retry with the same accepted details in the same client, merchant, mode and environment context after a timeout.

  • A successful replay returns the original creation response and Idempotency-Replayed: true. Fetch the identifier separately for current status.
  • HTTP 409 idempotency_request_in_progress: respect Retry-After and retry unchanged.
  • HTTP 409 idempotency_key_conflict: reconcile the saved attempt and original details; do not automatically create another key.
  • A reused reference alone does not prevent duplicate transactions. The creation idempotency header does not extend to collection, cancellation, capture, reversal or callback processing.

Transaction amounts by payment method

total is an amount in rand with two decimal places, not cents: 123.45 means R123.45. Transaction creation declares and validates a common minimum of R10.00. The current API defines no separate per-method maximum transaction amount.

Payment methodGatewayAPI creation minimumMaximum
Online cardDEFAULTR10.00No maximum defined by this API; provider/merchant limits may apply.
Instant EFTEFTSECURER10.00No maximum defined by this API; provider/bank limits may apply.
Appture Pay walletAPPTURER10.00No maximum defined by this API; available wallet funds and account rules apply.
Phone tap / QR collectionAPPTAPR10.00No maximum defined by this API; available wallet funds and account rules apply.
Card tapHALOR10.00No maximum defined by this API; device/provider/merchant limits may apply.

An undefined maximum does not mean an unlimited payment is accepted. Confirm the applicable provider and merchant limits before enabling a payment method, and handle preparation and payment failures. The common creation minimum is not a guarantee of provider acceptance. For verification and exact status values, see Callbacks and Transaction Statuses.


Courier Quote

Before requesting a delivery quotation, you will need to add an Address, for the pickup point, to your Appture Pay account.

Follow these steps in order to get a delivery quotation:

  1. Get the delivery address and check it's postal code using the delivery/check_postal_code endpoint
  2. Create a delivery via the delivery endpoint and keep it's id
  3. Request a delivery quote via the delivery/{id}/quote endpoint using the id received previously
  4. Allow your buyer to select a service option returned by the previous request
  5. Update the delivery with the selected service option using the delivery/{id} endpoint
include_once "includes/WebRequest.php";
include_once "includes/AppturePayAPI.php";

$clientId = "{your client id}";
$clientSecret = "{your client secret}";

// get an instance of the AppturePayAPI class
$api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

// First, create a delivery.

// variable for keeping the response
$deliveryPostResponse = null;

// values to pass as request body
$data = array(
    "delivery_courier_checked" => 0,
    "delivery_contact_name" => "Test",
    "delivery_contact_number_1" => "1231231234",
    "delivery_street" => "Street Ave",
    "delivery_suburb" => "Suburb",
    "delivery_city" => "City",
    "delivery_province" => "Gauteng",
    "delivery_country" => "South Africa",
    "delivery_postal_code" => "1234"
);

// make sure we are successfully authenticated
if( $api->getSession() !== null ) {
    // then make the call to the delivery/{id} endpoint using the GET method
    $deliveryPostResponse = $api->deliveryPost($data);

    // make sure the post succeeded
    if($deliveryPostResponse !== null && $deliveryPostResponse["success"]) {

        // Next we can call the quote endpoint for this delivery.

        // variable for keeping the response
        $deliveryPostQuoteResponse = null;

        // delivery id
        $id = $deliveryPostResponse["data"]["id"];

        // values to pass as request body
        $data = array(
            "value" => "199",
            "products" => array(
                array(
                    "quantity" => 1, // integer		  Y	number of pieces
                    "description" => "String with max length of 30", // string[30]	  N	freight description
                    "height" => 100, // integer		  Y	dimension 1 in millimeters
                    "width" => 250, // integer		  Y	dimension 2 in millimeters
                    "length" => 100, // integer		  Y	dimension 3 in millimeters
                    "weight" => 1.0 // float		  Y	mass in kilograms
                )
            ),
            "insurance" => 1
        );

        // then make the call to the delivery/{id}/quote endpoint using the POST method
        $deliveryPostQuoteResponse = $api->deliveryPostQuote($id, $data);

        // make sure the post succeeded
        if($deliveryPostQuoteResponse !== null && $deliveryPostQuoteResponse["success"]) {

            /*
             Here you may give the user a list of rates/services, as quoted, in
             order for them to select their own preferred service.
             */

            // Finally we can select the preferred service for the delivery quote.

            // variable for keeping the response
            $deliveryPutQuoteResponse = null;

            // values to pass as request body
            $data = array(
                // we will select the first service in the list of services/rates received
                "service" => $deliveryPostQuoteResponse["data"]["rates"][0]["service"]
            );

            // then make the call to the delivery/{id}/quote endpoint using the PUT method
            $deliveryPutQuoteResponse = $api->deliveryPutQuote($id, $data);
            var_dump($deliveryPutQuoteResponse);

        }

    }
}

Courier Dispatch

After completing the steps for creating a quotation and selecting a service option, the delivery can be dispatched. On success, the quoted amount for the selected service option will be deducted from your wallet.

  • To dispatch the courier call the delivery/{id}/dispatch endpoint
include_once "includes/WebRequest.php";
include_once "includes/AppturePayAPI.php";

$clientId = "{your client id}";
$clientSecret = "{your client secret}";

// get an instance of the AppturePayAPI class
$api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

// variable for keeping the response
$deliveryPutDispatchResponse = null;

// delivery id
$id = "{delivery id}";

// make sure we are successfully authenticated
if( $api->getSession() !== null ) {
    // make the call to the delivery/{id}/dispatch endpoint using the PUT method
    $deliveryPutDispatchResponse = $api->deliveryPutDispatch($id, null);
    var_dump($deliveryPutDispatchResponse);
}

Get Waybill and Label(s)

After dispatching the delivery, its waybill and label(s) can be downloaded and printed. There is a separate endpoint for each and the waybill or label will be sent back as an inline PDF file which should be saved locally and printed to present it to the courier's official on arrival. In case multiple products were quoted for delivery, multiple labels will exist within the label PDF.

  • To download the waybill call the delivery/{id}/waybill endpoint
  • To download the label(s) call the delivery/{id}/label endpoint
include_once "includes/WebRequest.php";
include_once "includes/AppturePayAPI.php";

$clientId = "{your client id}";
$clientSecret = "{your client secret}";

// get an instance of the AppturePayAPI class
$api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

// variable for keeping the response
$deliveryGetWaybillResponse = null;
$deliveryGetLabelResponse = null;

// delivery id
$id = "{delivery id}";

// make sure we are successfully authenticated
if( $api->getSession() !== null ) {
    // make the call to the delivery/{id}/waybill endpoint using the GET method
    $deliveryGetWaybillResponse = $api->deliveryGetWaybill($id);
    var_dump($deliveryGetWaybillResponse);

    // make the call to the delivery/{id}/label endpoint using the GET method
    $deliveryGetLabelResponse = $api->deliveryGetLabel($id);
    var_dump($deliveryGetLabelResponse);
}

Delivery Tracking

After the courier has picked up the item(s), tracking details will become available.

  • To get tracking info for a delivery call the delivery/{id}/track endpoint
include_once "includes/WebRequest.php";
include_once "includes/AppturePayAPI.php";

$clientId = "{your client id}";
$clientSecret = "{your client secret}";

// get an instance of the AppturePayAPI class
$api = new ApptureLab\AppturePayAPI($clientId, $clientSecret);

// variable for keeping the response
$deliveryGetTrackResponse = null;

// delivery id
$id = "{delivery id}";

// make sure we are successfully authenticated
if( $api->getSession() !== null ) {
    // then make the call to the delivery/{id}/track endpoint using the GET method
    $deliveryGetTrackResponse = $api->deliveryGetTrack($id);
    var_dump($deliveryGetTrackResponse);
}

POS App Integration

iKWAZA / Appture Pay · Cloud POS backend · React UI · Merchant app

Create a merchant transaction through the API, then open it in the installed merchant app using a same-device app link or Bluetooth to a nearby Android device. The merchant chooses Tap or QR Scan or Card tap to receive payment. Your POS receives transaction status notifications at its configured callback URL.

1. Merchant setup

  1. The merchant completes their account setup and creates an active, confidential API client for the POS integration.
  2. They configure your POS backend's live and sandbox callback URLs on that client.
  3. In your POS settings, they enter the Client ID and Client Secret. Your backend tests the credentials and stores them securely against that merchant.
  4. Install the current iKWAZA or Appture Pay app on the receiving device and sign in with an account authorized for the receiving merchant. Use matching app and API environments for testing and live payments.
SettingExamplePurpose
Callback URIhttps://pos.example.com/api/payments/ikwaza/callbackNotifies your backend of transaction status changes.
Sandbox callback URIA publicly reachable test callback URLReceives sandbox transaction notifications.
API base URLhttps://api.appturepay.com/Use the base URL supplied for your integration environment.

Keep client secrets and API access tokens on your backend, outside browser storage and VITE_* variables. The app uses its own merchant session. App links carry the transaction identifier; Bluetooth carries that identifier and the receiving app's connection code.

The API client that creates the transaction must belong to the merchant receiving the payment in the app. A successful Bluetooth handoff does not grant another merchant access to that transaction. If a payment is already in progress, check its current status before attempting another payment.

For testing, register at https://sandbox.appturepay.com/ and wait for an administrator to activate your account and load sandbox funds. Use a sandbox API client and configure its sandbox callback URL. The examples below use the production API base URL; replace it with https://sandbox.appturepay.com/api/ for sandbox requests. Install the iKWAZA sandbox APK or Appture Pay sandbox APK on your receiving Android device.

2. Authenticate the merchant's API client

PurposeEndpoint or app link
AuthenticatePOST /auth/access_token
Create transactionPOST /transaction
Verify current statusGET /transaction/{identifier}
Open Receive in the installed apphttps://app.ikwaza.com/wallet/receive/pos/{identifier}

Your Test connection button asks your backend to send:

POST https://api.appturepay.com/auth/access_token
Authorization: Basic BASE64(clientId:clientSecret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

A successful response supplies access_token, token_type and expires_in. Cache tokens on your backend per merchant and client until shortly before expiry; invalidate them when credentials change. Return only the connection result to the POS browser.

3. Create the transaction on your backend

When the cashier requests iKWAZA payment, your backend authenticates the cashier, checks access to the order, loads the authoritative amount, and selects the merchant's credentials. Create or load one payment-attempt record for the order, persisting its idempotency key and request details before calling the API. Reuse that record for concurrent requests and retries.

The following server-side TypeScript example creates a transaction and constructs its app link:

import { Buffer } from "node:buffer";

// Use the API base URL supplied for your integration environment.
const API_URL = "https://api.appturepay.com/";
const APP_URL = "https://app.ikwaza.com/";

type MerchantCredentials = { clientId: string; clientSecret: string };

// Persist these values on your backend BEFORE the first API request.
// Load and reuse the same attempt for retries, including after a timeout.
type PaymentAttempt = {
  idempotencyKey: string;
  reference: string;
  description: string;
  totalCents: number;
};

export async function createAppPayment(
  credentials: MerchantCredentials,
  attempt: PaymentAttempt,
) {
  if (!Number.isSafeInteger(attempt.totalCents) || attempt.totalCents < 1000) {
    throw new Error("The minimum payment amount is R10.00.");
  }

  const authResponse = await fetch(`${API_URL}auth/access_token`, {
    method: "POST",
    headers: {
      Authorization: `Basic ${Buffer.from(
        `${credentials.clientId}:${credentials.clientSecret}`,
      ).toString("base64")}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({ grant_type: "client_credentials" }),
  });
  const token = await authResponse.json();
  if (!authResponse.ok || !token.access_token) {
    throw new Error("Unable to authenticate the merchant's API client.");
  }

  const response = await fetch(`${API_URL}transaction`, {
    method: "POST",
    headers: {
      Authorization: `${token.token_type || "Bearer"} ${token.access_token}`,
      "Content-Type": "application/json",
      "Idempotency-Key": attempt.idempotencyKey,
    },
    body: JSON.stringify({
      client_id: credentials.clientId,
      reference: attempt.reference,
      description: attempt.description,
      transaction_type: "DB",
      day_of_debit: 1,
      total: attempt.totalCents / 100,
      gateway: "DEFAULT",
    }),
  });
  const result = await response.json();
  if (!response.ok || result.success !== true) {
    // Preserve the key and original details. Do not create another attempt
    // automatically after a timeout, 409, or uncertain server response.
    throw new Error(result.error || "Unable to create the payment transaction.");
  }

  const identifier = result.data?.identifier;
  if (typeof identifier !== "string" || !/^[0-9a-f]{32}$/i.test(identifier)) {
    throw new Error("The API did not return a valid transaction identifier.");
  }
  const appUrl = new URL(`wallet/receive/pos/${identifier}`, APP_URL).toString();

  // Save the identifier against this attempt before returning it to the POS.
  return { identifier, appUrl };
}
  • DB is a once-off debit payment. total is in rand; 123.45 means R123.45. The current minimum is R10.00.
  • Send gateway: "DEFAULT" for a transaction to be collected in the app. The merchant selects the payment method in the app.
  • Use a stable reference, such as POS-112, and save the original amount and description with the attempt.
  • The returned data.identifier is the public transaction identifier. Use it for the app link and subsequent status lookups.
  • Configure the callback URL on the API client.
Idempotency: send the optional Idempotency-Key header to prevent duplicate transactions on retries. Use one stable key per intended transaction. Keys are case-sensitive, 1–128 visible ASCII characters without spaces. Retrying the same key and details returns the original creation response. A changed request with that key returns HTTP 409 (idempotency_key_conflict). For idempotency_request_in_progress, respect Retry-After and retry the same request. A replay is a creation snapshot; use GET for current status.

Implement your own POS endpoint, for example POST /api/orders/112/payments/ikwaza. Save the returned identifier against the merchant, order, expected amount and attempt before returning:

{
  "identifier": "PUBLIC_IDENTIFIER",
  "appUrl": "https://app.ikwaza.com/wallet/receive/pos/PUBLIC_IDENTIFIER"
}

If your POS already has an identifier for the current attempt, reuse it. A network timeout does not prove creation or payment failed. Preserve the key and reconcile the existing transaction before intentionally creating a new attempt.

4. Send the transaction to the installed app

Use an app link when the POS browser and receiving app run on the same device, or Bluetooth when the POS browser runs on a nearby device. Both handoffs use the transaction identifier already saved by your backend.

Open the app on the same device

Open the verified app link returned by your backend on the device running the POS page:

https://app.ikwaza.com/wallet/receive/pos/PUBLIC_IDENTIFIER

After creating the transaction, display an Open iKWAZA link for the merchant to tap. The following React example requests payment through your POS backend and displays that link:

import { useState } from "react";

type AppPayment = { identifier: string; appUrl: string };

export function IkwazaPayment({ orderId }: { orderId: string }) {
  const [payment, setPayment] = useState<AppPayment | null>(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState("");

  async function requestPayment() {
    setBusy(true);
    setError("");
    try {
      const response = await fetch(
        `/api/orders/${encodeURIComponent(orderId)}/payments/ikwaza`,
        { method: "POST" },
      );
      if (!response.ok) throw new Error("Unable to start payment.");
      setPayment(await response.json());
    } catch (error) {
      setError(error instanceof Error ? error.message : "Unable to start payment.");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div>
      <button disabled={busy || payment !== null} onClick={requestPayment}>
        {busy ? "Creating payment..." : "Request iKWAZA payment"}
      </button>
      {error && <p role="alert">{error}</p>}
      {payment && <a href={payment.appUrl}>Open iKWAZA to receive payment</a>}
    </div>
  );
}

Protect this endpoint with your POS authentication and request validation. A repeated request for the same payment attempt must return its saved transaction identifier.

For an Android-only alternative, provide an explicit Open Android app directly link:

// Optional Android-only button, after creating and saving the transaction.
const androidAppUrl =
  `intent://app.ikwaza.com/wallet/receive/pos/${payment.identifier}` +
  "#Intent;scheme=https;package=com.appturelab.ikwaza;" +
  `S.browser_fallback_url=${encodeURIComponent(payment.appUrl)};end`;

// Set this on an <a href={androidAppUrl}>Open Android app directly</a> link.

Enable supported-link handling for iKWAZA in the Android app settings and allow the browser to open links in other apps. If a link stays in the browser, check that iKWAZA is installed and these settings are enabled. The merchant must open the transaction in the app to receive payment.

Keep the cloud POS order screen available and refresh its status through your backend when the merchant returns. The app's Done action returns to its own dashboard; this integration does not automatically navigate back to the browser.

For Appture Pay, use https://app.appturepay.com/wallet/receive/pos/{identifier} and Android package com.appturelab.appturepay.

Send to a nearby Android device over Bluetooth

Serve the sending POS over HTTPS. Both devices need internet access for API requests and payment processing. Choose a transport supported by the receiving phone and sending browser:

ConnectionSending POSReceiving phoneOS pairing
Bluetooth LEChrome/Edge with navigator.bluetooth and a BLE adapterAndroid with BLE advertising supportNot required
Bluetooth ClassicDesktop Chrome 117+ or compatible Edge with navigator.serial and Bluetooth ClassicAndroid 6+ with Bluetooth Classic; BLE advertising is not requiredPair once in phone/computer Bluetooth settings

Check window.isSecureContext and the selected browser API before offering that transport. For Classic, pair the devices in their Bluetooth settings first (keep the phone's Bluetooth settings open while discovering it), confirm the pairing prompts, then return to the receiving app. The app starts a secure RFCOMM service when Bluetooth receiving is enabled on its Bluetooth settings page. Return from Android's system Bluetooth settings to the app before sending.

The Bluetooth settings page displays Bluetooth Classic when BLE receiving is unavailable, including Android 6 phones without BLE advertising. Select Classic in your POS. Phones supporting both transports accept either through the same receiving switch. See the Chrome Bluetooth RFCOMM documentation for browser requirements. The browser must allow access to the selected custom Bluetooth service; a browser exposing Web Serial without Bluetooth RFCOMM support cannot use this connection.

Activate receiving and select the phone

  1. Keep the phone unlocked, sign in to iKWAZA or Appture Pay in POS mode, and open its dashboard.
  2. Tap the Bluetooth icon in the dashboard header to open Bluetooth settings. Turn on receiving on that page. It shows the available modes, device name, six-digit connection code and connection instructions. The dashboard icon is black when receiving is off or unavailable and uses the app's brand color when ready.
  3. Enter that code in the sending POS. Keep it as text so leading zeros are preserved.
  4. Click Send over Bluetooth and select the receiving phone in the browser's device chooser. Select it as soon as it appears; there is no need to wait for the scanning indicator to stop. For Bluetooth LE, OS pairing is not required. For Bluetooth Classic, complete OS pairing first, select Classic in the POS, then choose the paired phone's POS Payments serial service.
  5. The receiving app opens the transaction. The merchant selects the payment method, and your backend confirms the outcome through the callback and transaction lookup.

Send subsequent sales

After each payment, the app's Done action returns the merchant to the POS dashboard. Wait for the Bluetooth icon to show ready before sending the next sale. Receiving stays enabled for that POS session, and the connection code stays the same until receiving is switched off or the POS session ends. Tap the icon to reopen Bluetooth settings and switch receiving off there. Opening settings or returning to the dashboard keeps the same connection and code.

Load the browser helper once per page. For each new sale, create or load that order's payment attempt through your backend and send its saved identifier. Keep the same page open: starting a new sale must not reload the helper or call forgetReceiver(). The helper reuses the selected phone, including after connection failures or a change of connection code.

To choose another phone, offer a Change Bluetooth device action that calls PosBluetoothHandoff.forgetReceiver(). The next send opens the chooser. Reloading the page also starts a new device selection. The browser controls device access permissions; an initial selection is required.

Transaction acceptance pauses while a payment screen is open. Between sales, you can receive on the Bluetooth settings page or POS dashboard while the app remains in the foreground. Receiving stops when the app goes into the background or the phone locks. After returning to the app, wait for the dashboard to show ready before sending.

Browser example

Download the JavaScript Bluetooth handoff helper and serve it with your POS page. Connect the following example to the payment-attempt response from your backend by calling setPaymentAttempt(attempt). This function only updates the selected attempt; it does not create or send a transaction. Neither API credentials nor bearer tokens are sent over Bluetooth.

<label for="bluetooth-transport">Bluetooth connection</label>
            <select id="bluetooth-transport"><option value="ble">Bluetooth LE</option><option value="classic">Bluetooth Classic (paired phone / Android 6)</option></select>
<label for="connection-code">Connection code from the receiving app</label>
<input id="connection-code" inputmode="numeric" maxlength="6" autocomplete="off">
<button id="send-bluetooth" type="button">Send over Bluetooth</button>
<button id="change-device" type="button">Change Bluetooth device</button>
<p id="handoff-status" role="status" aria-live="polite"></p>

<script src="pos_bluetooth.js"></script>
<script>
const sendButton = document.getElementById('send-bluetooth');
const changeDeviceButton = document.getElementById('change-device');
const connectionCode = document.getElementById('connection-code');
const statusMessage = document.getElementById('handoff-status');
let paymentAttempt = null;
let sending = false;

// Call after your backend creates or restores the current order's attempt.
// The backend response contains its saved public transaction identifier.
function setPaymentAttempt(attempt) {
  if (sending) throw new Error('Wait for the current handoff to finish.');
  paymentAttempt = attempt;
}

sendButton.addEventListener('click', async () => {
  if (sending) return;
  if (!paymentAttempt || !paymentAttempt.identifier) {
    statusMessage.textContent = 'Create or load the payment request first.';
    return;
  }
  sending = true;
  sendButton.disabled = changeDeviceButton.disabled = document.getElementById('bluetooth-transport').disabled = true;
  statusMessage.textContent = 'Connecting to the receiving app...';
  try {
    // Call directly from this click; do not await another request first.
    if (document.getElementById('bluetooth-transport').value === 'classic') {
      await PosBluetoothHandoff.sendClassic(paymentAttempt.identifier, connectionCode.value.trim());
    } else {
      await PosBluetoothHandoff.send(paymentAttempt.identifier, connectionCode.value.trim());
    }
    statusMessage.textContent = 'Sent to the app. Waiting for verified payment status.';
  } catch (error) {
    statusMessage.textContent = error.message
      + ' Check this transaction through your backend before retrying the same sale.';
  } finally {
    sending = false;
    sendButton.disabled = changeDeviceButton.disabled = document.getElementById('bluetooth-transport').disabled = false;
  }
});

changeDeviceButton.addEventListener('click', () => {
  if (sending) return;
  PosBluetoothHandoff.forgetReceiver();
  statusMessage.textContent = 'The next send will ask you to select a device.';
});
</script>

Reconnect and recover safely

The helper retains the selected phone after a connection error. For Bluetooth LE, it retries transient connection setup failures up to three times within a 45-second transfer timeout. Classic opens the selected paired service once per send and waits for the app acknowledgement. It does not automatically resend transaction data after a write starts. After a timeout or disconnection, check the same transaction through your backend before retrying, and reuse its identifier and payment attempt.

SymptomWhat to check
The phone does not appearUnlock the phone, open Bluetooth settings from its POS dashboard, turn on receiving and wait for the brand-colored Bluetooth icon. For LE, the chooser displays devices advertising the POS service. For Classic, confirm OS pairing, select Classic, and use a desktop browser with Bluetooth Web Serial support. Allow requested Bluetooth permissions and check that the sending browser supports the selected transport.
The browser asks to select the phone for every saleKeep the POS page and helper loaded between sales. Call forgetReceiver() only when the merchant explicitly changes devices.
Connection timeoutKeep the devices nearby and return the phone to its ready POS dashboard. After the transfer fails, check the existing transaction before retrying it. If the remembered device remains unreachable after restarting the app or Bluetooth, use Change Bluetooth device to select it again.
Invalid connection codeUse the current six-digit code shown in the app, including leading zeros. Switching receiving off or ending the POS session requires a new code.
The app rejects the merchantConfirm that the API client creating the sale and the app's receiving merchant belong to the same account and API environment. Changing the Bluetooth device does not change transaction ownership.

Protocol for native POS clients

Native POS clients can use BLE GATT or a secure paired RFCOMM socket:

FieldValue
Classic RFCOMM service UUID9d421003-8c21-4a6f-9a6a-9bb0d02f8310 (POS Payments). Connect using a secure paired socket.
Primary service UUID (BLE)9d421001-8c21-4a6f-9a6a-9bb0d02f8310
Write characteristic UUID9d421002-8c21-4a6f-9a6a-9bb0d02f8310
FramePOS1 CODE IDENTIFIER\n, where CODE is six digits, IDENTIFIER is the 32-character public transaction identifier, and \n is one newline byte (0x0A).
Encoding and transferBLE: ASCII, sequential chunks of at most 20 bytes. Use writes with response and wait for each acknowledgement before sending the next chunk.

Classic sends the same 45-byte ASCII frame over RFCOMM in one write, then reads a newline-terminated ASCII acknowledgement. Success is exactly OK IDENTIFIER\n with the same identifier; BUSY\n means the app is not ready, and INVALID\n means the code or frame was rejected. The phone accepts one frame per connection and closes it after replying. Send within 15 seconds of connecting. Reopen the remembered port for each subsequent sale; do not request a new chooser each time. No API credentials are sent over this connection.

The helper waits up to 45 seconds for Classic connection/receipt and closes the port afterward. It does not automatically resend Classic writes. A missing acknowledgement leaves the outcome uncertain: check the existing transaction status and the receiving phone before manually retrying the same identifier. OS pairing remains saved unless removed in Bluetooth settings; page reloads can require selecting the paired service again.

A successful Bluetooth transfer confirms receipt of the identifier. It does not confirm merchant authorization or payment. The app checks transaction ownership and status through the API. Keep the cloud order pending until your backend verifies payment through the API; a transport acknowledgement alone must not mark an order paid.

5. Receive payment in POS mode

  1. Opening an app link or receiving a Bluetooth handoff takes the merchant to the Receive screen in POS mode. If sign-in is required, the merchant enters their PIN or password before continuing to the transaction.
  2. The Receive screen displays the transaction reference, description and amount for an eligible transaction belonging to the merchant.
  3. The merchant chooses a payment option available on that device.
  4. Payment is processed against the transaction you created. Its identifier and reference remain the same for status lookup and callback delivery.
Receive optionBehavior
Tap or QR ScanThe customer pays from their wallet by tapping their phone or scanning the displayed QR code. Phone tap requires NFC support with NFC enabled. QR payments work on devices without NFC.
Card tapThe customer taps their bank card on a supported Android 10+ NFC device. Card acceptance must be enabled for the merchant.

Your POS creates the transaction, opens the app link, and confirms the result through the API. The app handles the merchant's payment interaction and authorization.

If the app closes or a request times out, check the transaction's current status before starting another payment. Reopen the same transaction link to resume or check its progress.

6. Confirm payment through the callback

Transaction status notifications are sent to the API client's configured callback URL using GET, with parameters such as:

https://pos.example.com/api/payments/ikwaza/callback
  ?id=PUBLIC_IDENTIFIER&reference=POS-112&status=SUCCESS&...

For each notification:

  1. Locate the saved attempt using id and load the merchant from that saved record.
  2. Authenticate with that merchant's API client and request the current transaction:
GET https://api.appturepay.com/transaction/PUBLIC_IDENTIFIER
Authorization: Bearer ACCESS_TOKEN
  1. Check the lookup succeeded. Match identifier, client_id, reference and total to the saved attempt. Compare amounts in integer cents.
  2. Mark the order paid only when the verified status is SUCCESS. Apply the update atomically so duplicate or concurrent notifications cannot fulfill an order twice.
  3. Save failed or cancelled outcomes appropriately. An OPEN transaction is still awaiting confirmation. An older notification must not revert an already verified payment.
  4. Commit the update and return HTTP 200. Already processed notifications also return 200. Return non-200 for temporary failures so delivery is retried.

Failed callback deliveries retry at increasing intervals of one minute, ten minutes and then one hour. Use publicly reachable HTTPS endpoints that accept server-to-server requests without a browser login.

Your POS can also poll its own backend while displaying Checking payment…. That backend uses the same authenticated transaction lookup and verification routine, covering delayed callbacks and merchants returning from the app before notification arrives.

Opening the app, tapping Done, or receiving a callback query parameter does not prove payment succeeded. Confirm the transaction status through the API before marking the order paid.

7. Test and go live

Test with an approved sandbox account, a sandbox API client using https://sandbox.appturepay.com/api/, and the app configured for that environment. Configure a publicly reachable sandbox callback URL before creating test transactions. For card payment tests, use the current sandbox testing card details.

  • Use an active test merchant and an app configured for the same API environment. Confirm ownership checks reject a different merchant's sale.
  • Test app links and Android intent links, PIN/password sign-in, and resuming an interrupted sale.
  • Test Bluetooth with both receiving apps, including incorrect codes, denied permissions, disconnections, switching receiving off, and expired POS sessions. Confirm transport success alone does not mark an order paid.
  • Complete a Bluetooth payment, return to the dashboard, then create and send a new sale in the same browser tab. Confirm that the helper reuses the selected phone without another chooser.
  • Test a transient connection failure and a failed write separately. Confirm that setup retries do not create another transaction, an uncertain write is not resent automatically, and manual recovery reuses the original identifier.
  • Test Change Bluetooth device, reloading the POS page, locking the phone and returning from the background. Confirm that the POS displays clear recovery instructions and waits for the receiving dashboard to be ready.
  • Test Tap or QR Scan with NFC enabled, disabled, and unavailable.
  • Test Card tap on a supported device with card acceptance, including cancellation and interrupted payments. Confirm your POS checks the current transaction before offering another payment attempt.
  • Retry creation with the same idempotency key and details after a lost response. Confirm it returns the same identifier; changed details must conflict.
  • Verify callback retries, duplicate notifications, delayed notifications, and client/amount mismatches. Each paid order must be fulfilled exactly once.
  • Return to your POS after payment or cancellation and verify that it displays the confirmed transaction status.
  • Before going live, confirm the client's live callback URL, sandbox setting and app environment.
Code examples use TypeScript and React. Adapt them to your POS backend, authentication and order-management system.