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.
https://api.syncpay.au/v1/transactions4995 means 49.95. No floating point anywhere.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.
| Key | Use |
|---|---|
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
- Check the key.
GET /v1/authechoes who you are.shellcurl -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 } - 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 } - Send it again with the ARN. The ARN usually arrives at settlement. Re-send the whole transaction with the new fields; the
versiongoes 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
| Field | Type | Required | Notes |
|---|---|---|---|
merchant_ref | string | yes | Your id for the merchant, as registered on the link. Also accepted in test mode before any link exists. |
channel | string | yes | pos for anything taken at a counter or terminal. |
transaction_id | string, max 128 | yes | Your id for the transaction. The idempotency key: unique per merchant and per mode. |
type | string | sale, order or return. Default sale. | |
status | string | open, completed, voided, refunded or partially_refunded. Default completed. | |
original_transaction_id | string | On a return, the transaction_id of the sale it reverses. | |
currency | string | yes | ISO 4217, for example AUD. |
total_cents | integer | yes | What the customer paid. |
subtotal_cents | integer | Amount before GST. | |
gst_cents | integer | GST included in the total. | |
discount_cents, tip_cents, refunded_cents | integer | Transaction-level discount, tip and amount refunded so far. | |
created_at | timestamp | yes | When the sale was opened. |
completed_at | timestamp | When it was paid or finalised. | |
store_id | string | Your id for the store or location. | |
device_id | string | Register or device that took the sale. | |
employee_id | string | Staff member who processed it. | |
customer | object | customer_id, email, phone, name, consent. See Customer below. | |
lines | array | Line items. Up to 500. | |
payments | array | Payments against the transaction. Up to 20, so deposits and split tender fit on one transaction. | |
metadata | object | Up to 20 keys, string values up to 500 characters. |
Line
| Field | Type | Required | Notes |
|---|---|---|---|
line_id | string | Your id for the line. | |
sku | string | Your stock keeping unit. | |
gtin | string | Barcode number (GTIN, EAN, UPC). | |
description | string | yes | What the customer sees on the receipt. |
quantity | number | yes | May be fractional, for example 1.25 kg. |
unit_price_cents | integer | yes | Price per unit in cents. |
line_total_cents | integer | yes | Total for the line in cents. |
gst_cents | integer | GST included in the line. | |
discount_cents | integer | Discount applied to the line. | |
category | string | Your 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.
| Field | Type | Required | Notes |
|---|---|---|---|
payment_id | string | yes | Your id for this payment. Unique within the transaction. |
amount_cents | integer | yes | Amount of this payment in cents. |
tender_type | string | yes | card, eftpos, wallet, bnpl, bank_transfer, cash, account or other. |
status | string | authorized, captured, refunded, partially_refunded, voided or failed. Default captured. | |
paid_at | timestamp | When the payment was taken. | |
gateway | string | Processor or acquirer, for example stripe, tyro, square. | |
gateway_payment_id | string | The processor's id for the payment. | |
gateway_order_id | string | The processor's order id, if it has an order layer. | |
payment_reference | string | Any other reference that identifies the payment, for example a charge id or a bank transfer reference. | |
acquirer_reference_number | string | The ARN. Usually only known at settlement: send it later on a re-send. | |
arn_scheme | string | Card scheme the ARN belongs to, for example visa. | |
rrn | string | Retrieval reference number from the terminal. | |
stan | string | System trace audit number. Only unique per terminal per day, so send terminal_id and paid_at with it. | |
terminal_id | string | Payment terminal id. | |
settlement_batch_id | string | Settlement batch the payment closed in. | |
auth_code | string | Issuer authorisation code. | |
avs_result | string | Address verification result, as your gateway reports it. | |
cvv_result | string | CVV check result, as your gateway reports it. Never the CVV itself. | |
card | object | bin (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.
{
"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_idis the idempotency key. Always send the full, current state of the transaction, not a diff.- Same body again:
200, sameversion,updated: false. Retrying after a timeout or a5xxis always safe. - Changed body:
200,versiongoes up by one,updated: true. Use this for an ARN at settlement, a void (status: "voided"), or a refund (status: "refunded"plusrefunded_cents).
{
"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:
{
"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"
}
]
}
}| Status | Type | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 400 | invalid_request | One or more fields failed validation. fields lists each one with its path, for example lines[0].quantity. |
| 400 | card_data_rejected | The body carried card data SyncPay does not accept. Nothing was stored. See Card data. |
| 401 | invalid_api_key | Missing, unknown, revoked or expired key, or a suspended account. Always the same response, so it cannot be used to tell these apart. |
| 403 | merchant_not_linked | Live key, and no active link between your account and that merchant_ref. |
| 403 | merchant_ref_required | A vendor key was used without merchant_ref. |
| 404 | not_found | Unknown path. |
| 405 | method_not_allowed | Wrong HTTP method for the path. |
| 413 | payload_too_large | Body over 256 KB. (More than 500 lines or 20 payments is a 400 invalid_request.) |
| 415 | unsupported_media_type | Content-Type is not application/json. |
| 409 | conflict | Another request for the same transaction_id is being processed right now. Wait for Retry-After (a couple of seconds) and resend the same body. |
| 429 | rate_limited | Over 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. |
| 500 | internal_error | Our fault. Resend the same body; it is safe. |
Limits
| Limit | Value |
|---|---|
| Request body | 256 KB |
| Lines per transaction | 500 |
| Payments per transaction | 20 |
transaction_id | 128 characters |
metadata | 20 keys, values up to 500 characters |
| Rate | 300 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].