Developer docs · POS

Send POS transactions to SyncPay

One endpoint. Your POS sends each sale as a JSON snapshot when it completes, and sends it again whenever something changes. SyncPay turns it into an itemised receipt for the merchant and the card issuer.

POSThttps://api.syncpay.au/v1/transactions
Push, self-serviceYou call us. No certification, nothing changes at the terminal.
Integer cents4995 means 49.95. No floating point anywhere.
Test mode firstA spk_test_ key runs every check and stores nothing real.

Push or pull

This API is the push path: your system calls SyncPay. If you already expose an event feed and would rather SyncPay call you, the pull specification on pos-partner.html still applies. Both use the same field names, so moving from one to the other is a transport change, not a data change.

Same API for websites and online orders: developers-websites.html.

Keys and merchant links

You get one key for your company, not one per merchant. Each request names the merchant it belongs to with merchant_ref, which is your own id for that merchant.

KeyUse
spk_test_...Sandbox. Accepts any merchant_ref. Nothing reaches a receipt, a merchant or an issuer.
spk_live_...Production. Only for merchants who have linked your account.

What a link is

A link is the merchant's permission for your POS to send its sales to SyncPay. Without it, anyone holding a vendor key could write receipts into a business that never agreed to it, and those receipts reach card issuers. The link also records your id for the merchant (the merchant_ref), lets the merchant withdraw permission at any time, and is how sales are attributed to your platform.

To link a merchant, email [email protected] with the merchant's business name, ABN and your merchant_ref. We confirm with the merchant and switch the link on. A live call for a merchant without an active link returns 403 merchant_not_linked.

Getting, rotating and revoking keys

  • Keys are issued by SyncPay on request to [email protected]. A key is shown once. SyncPay stores only a hash, so a lost key cannot be recovered, only replaced.
  • Rotation. Ask for a rotation and you get a new key straight away. The old key keeps working for 24 hours so you can deploy the new one without downtime.
  • Revocation is immediate. Use it if a key leaks.
  • Keys are server-side secrets. Keep them out of terminal firmware logs, client apps and source control.

Quickstart

  1. Check the key. GET /v1/auth echoes who you are.
    shell
    curl -sS https://api.syncpay.au/v1/auth \
      -H "Authorization: Bearer spk_test_YOUR_TEST_KEY"
    json
    {
      "object": "auth",
      "livemode": false,
      "partner": {
        "name": "Example POS Pty Ltd",
        "kind": "pos_vendor"
      },
      "key": {
        "prefix": "spk_test_",
        "last4": "a91f"
      },
      "merchant_pinned": false,
      "linked_merchants": 0
    }
  2. Send a sale. Two flat whites, paid by contactless card.
    shell
    curl -sS -X POST https://api.syncpay.au/v1/transactions \
      -H "Authorization: Bearer spk_test_YOUR_TEST_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "merchant_ref": "HALO-88213",
      "channel": "pos",
      "transaction_id": "POS-TXN-0091822",
      "type": "sale",
      "status": "completed",
      "currency": "AUD",
      "subtotal_cents": 1000,
      "gst_cents": 100,
      "total_cents": 1100,
      "created_at": "2026-10-02T01:14:05Z",
      "completed_at": "2026-10-02T01:14:06Z",
      "store_id": "ST-014",
      "device_id": "REG-014-03",
      "employee_id": "EMP-2291",
      "lines": [
        {
          "line_id": "L1",
          "sku": "FW-REG",
          "description": "Flat white",
          "quantity": 2,
          "unit_price_cents": 550,
          "line_total_cents": 1100,
          "gst_cents": 100
        }
      ],
      "payments": [
        {
          "payment_id": "POS-PAY-3320471",
          "amount_cents": 1100,
          "tender_type": "card",
          "status": "captured",
          "paid_at": "2026-10-02T01:14:06Z",
          "gateway": "tyro",
          "gateway_payment_id": "TYR-7781023",
          "terminal_id": "T0142201",
          "rrn": "612345678901",
          "stan": "123456",
          "auth_code": "081224",
          "card": {
            "bin": "424242",
            "last4": "4242",
            "brand": "visa",
            "entry_mode": "contactless",
            "funding": "debit"
          }
        }
      ]
    }'

    First time: 201 Created.

    json
    {
      "id": "txn_6f1c2a0b9d4e4f8a9c6b2e5d8a917f30",
      "object": "transaction",
      "livemode": false,
      "transaction_id": "POS-TXN-0091822",
      "merchant_ref": "HALO-88213",
      "version": 1,
      "created": true,
      "updated": false
    }
  3. Send it again with the ARN. The ARN usually arrives at settlement. Re-send the whole transaction with the new fields; the version goes up.
    json
    {
      "payment_id": "POS-PAY-3320471",
      "amount_cents": 1100,
      "tender_type": "card",
      "status": "captured",
      "paid_at": "2026-10-02T01:14:06Z",
      "gateway": "tyro",
      "gateway_payment_id": "TYR-7781023",
      "terminal_id": "T0142201",
      "rrn": "612345678901",
      "stan": "123456",
      "auth_code": "081224",
      "card": {
        "bin": "424242",
        "last4": "4242",
        "brand": "visa",
        "entry_mode": "contactless",
        "funding": "debit"
      },
      "acquirer_reference_number": "74567890123456789012345",
      "arn_scheme": "visa",
      "settlement_batch_id": "BATCH-20261002-014"
    }

    That is the updated payment object; send it inside the full transaction body. Response: 200 OK.

    json
    {
      "id": "txn_6f1c2a0b9d4e4f8a9c6b2e5d8a917f30",
      "object": "transaction",
      "livemode": false,
      "transaction_id": "POS-TXN-0091822",
      "merchant_ref": "HALO-88213",
      "version": 2,
      "created": false,
      "updated": true
    }

If the gateway route is not reachable from your network yet, the same endpoint answers at https://api.syncpay.au/functions/v1/syncpay-ingest/v1/transactions.

Request

Headers: Authorization: Bearer spk_... and Content-Type: application/json. Timestamps are ISO 8601 with an offset or Z. Amounts are integer cents and never negative; a return is marked by type: "return", not by a minus sign.

Transaction

FieldTypeRequiredNotes
merchant_refstringyesYour id for the merchant, as registered on the link. Also accepted in test mode before any link exists.
channelstringyespos for anything taken at a counter or terminal.
transaction_idstring, max 128yesYour id for the transaction. The idempotency key: unique per merchant and per mode.
typestringsale, order or return. Default sale.
statusstringopen, completed, voided, refunded or partially_refunded. Default completed.
original_transaction_idstringOn a return, the transaction_id of the sale it reverses.
currencystringyesISO 4217, for example AUD.
total_centsintegeryesWhat the customer paid.
subtotal_centsintegerAmount before GST.
gst_centsintegerGST included in the total.
discount_cents, tip_cents, refunded_centsintegerTransaction-level discount, tip and amount refunded so far.
created_attimestampyesWhen the sale was opened.
completed_attimestampWhen it was paid or finalised.
store_idstringYour id for the store or location.
device_idstringRegister or device that took the sale.
employee_idstringStaff member who processed it.
customerobjectcustomer_id, email, phone, name, consent. See Customer below.
linesarrayLine items. Up to 500.
paymentsarrayPayments against the transaction. Up to 20, so deposits and split tender fit on one transaction.
metadataobjectUp to 20 keys, string values up to 500 characters.

Line

FieldTypeRequiredNotes
line_idstringYour id for the line.
skustringYour stock keeping unit.
gtinstringBarcode number (GTIN, EAN, UPC).
descriptionstringyesWhat the customer sees on the receipt.
quantitynumberyesMay be fractional, for example 1.25 kg.
unit_price_centsintegeryesPrice per unit in cents.
line_total_centsintegeryesTotal for the line in cents.
gst_centsintegerGST included in the line.
discount_centsintegerDiscount applied to the line.
categorystringYour product category.

Payment

Send every identifier the terminal or gateway gives you. They are what tie the receipt to the cardholder's statement; without them, matching falls back to amount and time. Null or omitted where a value genuinely does not exist, never a placeholder.

FieldTypeRequiredNotes
payment_idstringyesYour id for this payment. Unique within the transaction.
amount_centsintegeryesAmount of this payment in cents.
tender_typestringyescard, eftpos, wallet, bnpl, bank_transfer, cash, account or other.
statusstringauthorized, captured, refunded, partially_refunded, voided or failed. Default captured.
paid_attimestampWhen the payment was taken.
gatewaystringProcessor or acquirer, for example stripe, tyro, square.
gateway_payment_idstringThe processor's id for the payment.
gateway_order_idstringThe processor's order id, if it has an order layer.
payment_referencestringAny other reference that identifies the payment, for example a charge id or a bank transfer reference.
acquirer_reference_numberstringThe ARN. Usually only known at settlement: send it later on a re-send.
arn_schemestringCard scheme the ARN belongs to, for example visa.
rrnstringRetrieval reference number from the terminal.
stanstringSystem trace audit number. Only unique per terminal per day, so send terminal_id and paid_at with it.
terminal_idstringPayment terminal id.
settlement_batch_idstringSettlement batch the payment closed in.
auth_codestringIssuer authorisation code.
avs_resultstringAddress verification result, as your gateway reports it.
cvv_resultstringCVV check result, as your gateway reports it. Never the CVV itself.
cardobjectbin (6 or 8 digits; 6 for American Express and Diners Club), last4 (exactly 4), brand, entry_mode (chip, contactless, swipe, keyed, on_file, ecommerce), funding, wallet.

Customer

customer_id, email, phone and name are all optional. Set consent to explicit_optin or tos_acceptance only when the customer actually agreed; without it, the details sit on the receipt but no customer profile is created.

Returns

A return is its own transaction with type: "return" and original_transaction_id pointing at the sale.

json
{
  "merchant_ref": "HALO-88213",
  "channel": "pos",
  "transaction_id": "POS-TXN-0091907",
  "type": "return",
  "original_transaction_id": "POS-TXN-0091822",
  "status": "completed",
  "currency": "AUD",
  "subtotal_cents": 500,
  "gst_cents": 50,
  "total_cents": 550,
  "created_at": "2026-10-03T22:41:10Z",
  "store_id": "ST-014",
  "device_id": "REG-014-01",
  "employee_id": "EMP-1180",
  "lines": [
    {
      "line_id": "L1",
      "sku": "FW-REG",
      "description": "Flat white",
      "quantity": 1,
      "unit_price_cents": 550,
      "line_total_cents": 550,
      "gst_cents": 50
    }
  ],
  "payments": [
    {
      "payment_id": "POS-REF-0042",
      "amount_cents": 550,
      "tender_type": "card",
      "status": "refunded",
      "gateway": "tyro",
      "terminal_id": "T0142199",
      "card": {
        "last4": "4242",
        "brand": "visa",
        "entry_mode": "contactless"
      }
    }
  ]
}

Updates and retries

  • transaction_id is the idempotency key. Always send the full, current state of the transaction, not a diff.
  • Same body again: 200, same version, updated: false. Retrying after a timeout or a 5xx is always safe.
  • Changed body: 200, version goes up by one, updated: true. Use this for an ARN at settlement, a void (status: "voided"), or a refund (status: "refunded" plus refunded_cents).
json
{
  "id": "txn_6f1c2a0b9d4e4f8a9c6b2e5d8a917f30",
  "object": "transaction",
  "livemode": false,
  "transaction_id": "POS-TXN-0091822",
  "merchant_ref": "HALO-88213",
  "version": 2,
  "created": false,
  "updated": false
}

Sandbox

Same URL, spk_test_ key. Test mode runs the full check: authentication, validation, card-data refusal, idempotency and versioning. The transaction is stored for your testing and goes no further: no receipt, no merchant, no issuer, no charge. Any merchant_ref is accepted, so you can build before your first merchant links. Responses carry "livemode": false. Test transactions are deleted after 30 days.

Card data

Never send

The full card number (PAN), the CVV or CVC value, the expiry date, magnetic stripe track data, PIN blocks or any key material. Not in a field, not in metadata (whatever the key is called), not in a line description or a payment reference.

Fine to send

card.bin (first 6 or 8 digits), card.last4, the brand, the entry mode, the auth code and the AVS and CVV result codes.

SyncPay refuses card data rather than quietly dropping it. A field named like a card number, CVV, expiry, track or PIN, or a 13 to 19 digit number that passes the Luhn check in a free-text field, gets 400 card_data_rejected. The body is not stored and not logged. Keeping card data out keeps your PCI scope and ours unchanged.

Responses and errors

Success is 201 the first time a transaction_id is seen and 200 after that. The response is the same whether or not the transaction ends up on a receipt.

Errors share one shape:

json
{
  "error": {
    "type": "invalid_request",
    "message": "2 fields are invalid",
    "fields": [
      {
        "field": "lines[0].quantity",
        "message": "must be a number greater than 0"
      },
      {
        "field": "currency",
        "message": "is required"
      }
    ]
  }
}
StatusTypeMeaning
400invalid_jsonThe body is not valid JSON.
400invalid_requestOne or more fields failed validation. fields lists each one with its path, for example lines[0].quantity.
400card_data_rejectedThe body carried card data SyncPay does not accept. Nothing was stored. See Card data.
401invalid_api_keyMissing, unknown, revoked or expired key, or a suspended account. Always the same response, so it cannot be used to tell these apart.
403merchant_not_linkedLive key, and no active link between your account and that merchant_ref.
403merchant_ref_requiredA vendor key was used without merchant_ref.
404not_foundUnknown path.
405method_not_allowedWrong HTTP method for the path.
413payload_too_largeBody over 256 KB. (More than 500 lines or 20 payments is a 400 invalid_request.)
415unsupported_media_typeContent-Type is not application/json.
409conflictAnother request for the same transaction_id is being processed right now. Wait for Retry-After (a couple of seconds) and resend the same body.
429rate_limitedOver 300 requests a minute on this key, or too many failed authentication attempts from your IP. Wait for the number of seconds in Retry-After, then resend the same body.
500internal_errorOur fault. Resend the same body; it is safe.

Limits

LimitValue
Request body256 KB
Lines per transaction500
Payments per transaction20
transaction_id128 characters
metadata20 keys, values up to 500 characters
Rate300 requests a minute per key

Versioning: the path carries /v1. New optional fields can appear in responses at any time, so ignore fields you do not know. Breaking changes get /v2.

Questions, keys, links: [email protected].