Smart receipts for your point of sale
SyncPay turns the transaction your POS already records into an itemised digital receipt, stored permanently and retrievable by the merchant and their customer. You send a read-only feed of events. Nothing changes at the counter or in your payment flow.
How it works
- Your POS emits an event. A sale is created, a payment is applied, the transaction completes. Three events, all from data you already write.
- SyncPay reads the feed. Your POS pings us when the sale completes and we immediately call your events endpoint. We pull, you never push, and it all happens after the fact, outside the payment flow.
- We build the receipt. The events are replayed into one itemised record and stored against a unique code.
- Everyone retrieves it. The merchant from their own record, the customer without an account, and the merchant's accounting file automatically.
The whole integration is one read-only endpoint over data your POS already produces. No terminal certification, no changes at the counter, no work for the merchant.
One integration, not one per bank
The line items and the card context sit inside your POS, and that is where every bank has to come to get them. Done directly, each institution is a separate build with you, in their format, on their timetable, and every other POS platform they ask repeats the same work. Then the next bank starts again from nothing. Most of it never gets built, which is why this data still is not anywhere.
Direct, bank by bank
Every institution is its own contract, its own spec and its own maintenance job, carried by you and by your merchants. Nothing you build for one bank is reusable for the next, and the queue only gets longer as more of them ask.
Through SyncPay
You build the events endpoint once. The record is then available to every institution connected to SyncPay, and banks that arrive later need nothing further from you. One feed on your side, maintained by us against everyone downstream.
The same holds in reverse for the banks, which is what makes them willing to pay for it. See the bank side.
Partner with us
If you run a POS platform and want smart receipts for your merchants, tell us about your system and we will come back with what the integration looks like on your side.
Building the integration already? Developers can reach us direct at dev@syncpay.au.
In this guide
What smart receipts do for your merchants
One record, created the moment a payment succeeds, that the merchant, their customer and their accountant can all use. Everything below is running in production today.
No paper at the counter
The receipt exists the moment the payment succeeds. Nothing to print, nothing to email, nothing for the customer to fold into a wallet and lose.
Returns become a lookup
Proof of purchase comes out of the record instead of a docket the customer no longer has. The staff member checks rather than negotiates.
Warranty outlives the paper
The receipt for a major appliance is still there three years later, and survives the customer changing their email address. Thermal paper does not.
Disputes have evidence
Card scheme, last four, entry type and auth code sit on the same record as the line items, so a cardholder claim that a purchase never happened has something to answer to.
The books reconcile themselves
Sales flow into Xero with line items and GST per line, so the merchant is not matching bank lines to sales by hand on a Sunday night.
Customers retrieve their own
The cardholder looks up the itemised receipt without an account and without contacting the merchant, which takes the request out of the merchant's inbox entirely.
What the merchant has to do
Nothing. There is no extra step at the counter, no app to install, no card to scan, and no change to how they take a payment. The receipt is a by-product of the sale they were making anyway, which is the only version of this that survives a busy Saturday.
What you ship
Smart receipts appear as a feature of your POS: your interface, your invoice, your support team. We white-label the merchant-facing surface on request, and your merchants do not need to know our name.
Sending transaction events to SyncPay
SyncPay reads a feed of events your POS already produces. We do not write to your system, sit in the payment path, or require terminal certification.
This is the shape we ask for. It is a proposal rather than a fixed requirement: where your system already emits something close, we adapt to it. Everything below has been through implementation with a retail POS partner, so it is a working specification rather than a wish list.
Just want to chuck it in AI?
The whole specification below, as one JSON object your assistant can read. Paste it into whatever your team codes with, point it at your transaction tables, and it will scaffold the endpoint. Then read the rest of this page to check what it produced.
{
"task": "Build the read endpoint SyncPay polls to receive transaction events from our POS system.",
"context": "SyncPay is pull only. It calls our endpoint and we answer. We never push transaction data out, nothing sits in the payment path, and the terminal and checkout flow do not change.",
"deliverables": [
"GET /events?after={cursor}&limit=200 returning {\"events\": [...], \"cursor\": \"opaque-string\"}",
"Auth: Authorization: Bearer <api_key> over TLS. No HMAC on this endpoint, because SyncPay opens the connection and sends no payload.",
"Optional but recommended: on transaction completion, POST a bodyless signed ping to {base_url}/pos/notify with header X-Signature: hmac-sha256 of the merchant id, so SyncPay pulls within a second or two instead of waiting for the next poll.",
"Without the ping, SyncPay polls every 30 to 60 seconds. Same payload either way."
],
"design_rules": [
"Incremental, not snapshot. Each event is one discrete update referencing a parent transaction_id. Never a full current-state dump per call.",
"Transaction-centric, not payment-centric. One transaction accumulates an ordered log. Payments are separate objects referencing it, so deposits, progress payments and a final payment all live on one transaction.",
"Ordered. Every event carries a sequence number, monotonically increasing per transaction.",
"At-least-once is enough. SyncPay deduplicates on event_id, so retries are safe and exactly-once is not required.",
"events is in global order. cursor is opaque to SyncPay and passed back as after on the next call. An empty events array with an unchanged cursor means nothing new."
],
"value_formats": {
"amounts": "Integer minor units. 4995 means 49.95. No floating point anywhere in the payload.",
"timestamps": "ISO 8601 UTC, for example 2026-08-06T02:14:05Z.",
"currency": "Explicit on every transaction, for example AUD. Never implied by the endpoint or the account."
},
"event_envelope": {
"event_id": "b7f3c2a0-1d4e-4f8a-9c6b-2e5d8a917f30",
"event_type": "transaction.created",
"created_at": "2026-08-06T02:14:07Z",
"data": {}
},
"event_types": {
"core": [
"transaction.created — new transaction opened, carrying its initial line items",
"payment.applied — a payment, full or partial, applied to a transaction",
"transaction.completed — finalised, no further edits expected",
"transaction.voided — cancelled in full before completion"
],
"optional": [
"line.added",
"line.quantity_changed",
"line.cancelled",
"line.split — one line becomes two on the same SKU, for example delivered versus backordered",
"line.fulfilled — updates quantity_fulfilled",
"return.created — matched return, linked to the original sale"
],
"note": "Start with the core set. Create, pay in full, complete is simple cash and carry and produces a working receipt on its own. Deposits and progress payments need no new event type, only a payment.applied where amount_cents is less than the outstanding total."
},
"objects": {
"transaction": {
"transaction_id": "POS-TXN-0091822",
"type": "sale | order | return",
"sequence": 1,
"store_id": "ST-014",
"device_id": "REG-014-03",
"employee_id": "EMP-2291",
"currency": "AUD",
"subtotal_cents": 45409,
"gst_cents": 4541,
"total_cents": 49950,
"customer": {
"customer_id": "CUST-55710",
"customer_email": "dana@example.com",
"customer_phone": "+61400000000"
},
"lines": [],
"created_at": "2026-08-06T02:14:05Z",
"original_transaction_id": "set only on type: return, linking a matched return to the sale it reverses"
},
"line": {
"line_id": "L1",
"sku": "TIM-90x45-2.4",
"description": "90x45 MGP10 2.4m",
"quantity": 30,
"unit_price_cents": 1250,
"line_total_cents": 37500,
"gst_cents": 3409,
"quantity_fulfilled": 0,
"parent_line_id": null
},
"payment": {
"payment_id": "POS-PAY-3320471",
"transaction_id": "POS-TXN-0091822",
"sequence": 2,
"amount_cents": 49950,
"outstanding_cents": 0,
"tender_type": "card | eftpos | cash | account | other",
"device_id": "REG-014-03",
"employee_id": "EMP-2291",
"card_last4": "4242",
"card_bin": "451234",
"card_type": "VISA",
"card_entry_type": "CONTACTLESS",
"auth_code": "081224",
"acquirer_reference_number": "74567890123456789012345",
"arn_scheme": "visa",
"rrn": "612345678901",
"stan": "123456",
"terminal_id": "T0142201",
"gateway": "tyro",
"gateway_payment_id": "pi_3Qk2mR2eZvKYlo2C0abcDEFG",
"gateway_order_id": null,
"settlement_batch_id": "BATCH-20260806-014",
"paid_at": "2026-08-06T02:14:06Z"
}
},
"object_notes": [
"line_id is stable for the life of the transaction, because later line events reference it.",
"parent_line_id is set only on lines created by a line.split.",
"There is no line cap. The schema must scale to thirty or more lines with high per-line quantities.",
"customer is optional. Send whatever identifiers exist: email preferred, then phone, then your own customer id.",
"payment_id must be unique. It is used for payment-level idempotency alongside event_id.",
"outstanding_cents after the payment is what distinguishes a deposit from a final payment."
],
"payment_identifiers": {
"why": "These tie the transaction to the processor record and to what the cardholder bank sees. Without at least one, matching falls back to amount and timestamp, which is a guess rather than a match. Send every identifier the terminal or gateway hands back. Null where genuinely absent, never fabricated.",
"highest": ["acquirer_reference_number", "arn_scheme"],
"high": ["gateway", "gateway_payment_id"],
"useful": ["gateway_order_id", "rrn", "stan", "terminal_id", "settlement_batch_id", "auth_code"],
"arn_note": "The ARN often only appears at settlement, not at authorisation. Send it later on a payment update and SyncPay attaches it.",
"stan_note": "Only unique within a terminal and a day, so send terminal_id and paid_at alongside it.",
"do_not_send": "txn_hash. SyncPay derives it."
},
"card_fields": {
"include_where_the_terminal_returns_them": ["card_last4", "card_bin", "card_type", "card_entry_type (chip, contactless, swipe, keyed, on file)", "auth_code", "AVS and CVV result codes"],
"never_include": ["full card number (PAN)", "CVV or CVC values", "magnetic stripe track data", "PIN blocks or key material"],
"note": "SyncPay does not request or retain these and drops them on ingest if present. Excluding card data keeps our PCI scope unchanged. Null is acceptable where a value is genuinely absent. Never populate a placeholder."
},
"known_gaps": [
"Unmatched or negative returns, meaning a refund not tied to an original sale transaction, are deliberately not designed.",
"The merchant REST API takes dollars. This event feed uses integer minor units. Two different surfaces, so check which one you are calling."
],
"instructions": [
"Map each event type onto the tables our POS already writes. Do not invent new state; read the existing ledger.",
"Where our system already emits something close to this shape, keep our shape and say so. SyncPay adapts to it. This is a proposal, not a fixed requirement.",
"Make the endpoint replayable from an old cursor so a rebuild is possible.",
"List every field above that our system cannot produce, and say why. That list is the first thing SyncPay wants on the call.",
"Do not change the payment flow, the terminal integration, or anything at the counter."
],
"docs": "https://syncpay.au/pos-partner.html#docs",
"contact": "dev@syncpay.au"
}
1. Design principles
- Incremental, not snapshot. Each event is one discrete update: a line added, a quantity changed, a payment applied, referencing a parent
transaction_id. Never a full current-state dump of the transaction per call. SyncPay replays the ordered event log to reconstruct state. - Transaction-centric, not payment-centric. One transaction accumulates an ordered log of updates. Payments are separate objects that reference the transaction, so a single transaction can carry a deposit, progress payments and a final payment across its life.
- Ordered. Every event carries a
sequencenumber, monotonically increasing per transaction, so out-of-order delivery is recoverable.
2. Delivery
SyncPay is pull only. Transaction data never leaves your system on an outbound request you have to build, sign and secure. We call your endpoint, you answer. That keeps the direction of travel simple, keeps card data out of any inbound payload, and means you never have to trust our uptime to complete a sale.
You expose one read endpoint:
GET /events?after={cursor}&limit=200
Authentication is TLS plus an API key. No HMAC signature is required, since SyncPay initiates every request and there is no inbound payload for you to verify.
The only open question is what triggers the call. Two modes, and you can start on the second and move to the first later without changing a line of the payload.
Notify and pull
When a transaction completes, your POS sends us a signed ping with no body. We call your events endpoint immediately and read what changed.
POST {base_url}/pos/notify
X-Signature: hmac-sha256 of the merchant id
The receipt exists within a second or two of the payment. The ping carries no transaction data, no card fields and no customer details, so it needs no encryption beyond TLS and cannot leak anything if it is replayed.
Scheduled polling
We poll on a fixed interval, typically every thirty to sixty seconds, and take whatever has accumulated since the last cursor.
Simplest possible integration, and the right starting point if you cannot emit a ping yet. The receipt lands within the poll interval rather than instantly, which is fine for reconciliation and less good for a customer standing at the counter waiting for it.
Both modes are pull. In mode 1 you are telling us to look, not sending us the data. The transaction still only moves when we ask for it, over a connection we opened.
Poll response envelope. Our proposal, and we are happy to take whatever shape your API already returns.
{
"events": [ ],
"cursor": "opaque-string-for-next-page"
}
eventsis in global order.cursoris opaque to us and passed back asafteron the next call.- An empty
eventsarray with an unchanged cursor means nothing new.
Auth header. Authorization: Bearer <api_key>. Also a proposal. If your API keys travel differently, that works, and confirming it is part of the sandbox conversation.
3. Event envelope
Every event in the events array is wrapped the same way.
{
"event_id": "b7f3c2a0-1d4e-4f8a-9c6b-2e5d8a917f30",
"event_type": "transaction.created",
"created_at": "2026-08-06T02:14:07Z",
"data": { }
}
event_idis unique per event. SyncPay deduplicates on it, so retries are always safe and at-least-once delivery is sufficient. You never need exactly-once.event_typeis a string from the table in section 5.created_atis ISO 8601 UTC.
4. Value formats
- Integer minor units.
4995represents 49.95 in the transaction currency. No floating point values anywhere in the payload. - ISO 8601 UTC for every timestamp.
- Explicit currency on every transaction, for example
"AUD". Never implied by the endpoint or the account.
5. Event types
Four events produce a complete receipt. The rest are optional and only matter if your merchants run iterative orders.
| Event type | Set | Description |
|---|---|---|
transaction.created | Core | New transaction opened, carrying its initial line items |
payment.applied | Core | A payment, full or partial, applied to a transaction |
transaction.completed | Core | Transaction finalised, no further edits expected |
transaction.voided | Core | Transaction cancelled in full before completion |
line.added | Optional | Line item added after creation |
line.quantity_changed | Optional | Quantity on an existing line changed |
line.cancelled | Optional | Line removed from the transaction |
line.split | Optional | One line becomes two on the same SKU, for example delivered versus backordered |
line.fulfilled | Optional | Partial or full delivery recorded against a line |
return.created | Optional | Matched return, linked to the original sale transaction |
Start with the core set. Create, pay in full, complete is simple cash and carry and produces a working receipt on its own. The optional set covers trade accounts, deposits and backorders, and can follow whenever you want it. Deposits and progress payments need no additional event type at all, only a payment.applied where amount_cents is less than the outstanding total.
Known gap. Unmatched or negative returns, meaning a refund not tied to an original sale transaction, are deliberately not designed here. We flag it rather than leave it to be discovered.
6. Object schemas
Transaction, sent on transaction.* events:
{
"transaction_id": "POS-TXN-0091822",
"type": "sale", // sale | order | return
"sequence": 1,
"store_id": "ST-014",
"device_id": "REG-014-03",
"employee_id": "EMP-2291",
"currency": "AUD",
"subtotal_cents": 45409,
"gst_cents": 4541,
"total_cents": 49950,
"customer": { // optional, send available identifiers
"customer_id": "CUST-55710",
"customer_email": "dana@example.com",
"customer_phone": "+61400000000"
},
"lines": [ ],
"created_at": "2026-08-06T02:14:05Z"
}
typeissale,orderorreturn. The core set only sendssale.customeris optional. Send whatever identifiers exist: email preferred, then phone, then your own customer ID. SyncPay uses these to resolve the consumer identity so the customer can retrieve their receipt.original_transaction_idappears only ontype: "return"transactions, linking a matched return to the sale it reverses.
Line item, carried in lines and referenced by later line events:
{
"line_id": "L1",
"sku": "TIM-90x45-2.4",
"description": "90x45 MGP10 2.4m",
"quantity": 30,
"unit_price_cents": 1250,
"line_total_cents": 37500,
"gst_cents": 3409,
"quantity_fulfilled": 0,
"parent_line_id": null
}
line_idis stable for the life of the transaction, because later line events reference it.parent_line_idis set only on lines created by aline.split.quantity_fulfilledsupports partial delivery and is updated byline.fulfilled.- There is no line cap on our side. The schema must scale to thirty or more lines with high per-line quantities.
Payment, sent on payment.applied:
{
"payment_id": "POS-PAY-3320471",
"transaction_id": "POS-TXN-0091822",
"sequence": 2,
"amount_cents": 49950,
"outstanding_cents": 0,
"tender_type": "card", // card | eftpos | cash | account | other
"device_id": "REG-014-03",
"employee_id": "EMP-2291",
"card_last4": "4242",
"card_bin": "451234",
"card_type": "VISA",
"card_entry_type": "CONTACTLESS",
"auth_code": "081224",
"acquirer_reference_number": "74567890123456789012345",
"arn_scheme": "visa",
"rrn": "612345678901",
"stan": "123456",
"terminal_id": "T0142201",
"gateway": "tyro",
"gateway_payment_id": "pi_3Qk2mR2eZvKYlo2C0abcDEFG",
"gateway_order_id": null,
"settlement_batch_id": "BATCH-20260806-014",
"paid_at": "2026-08-06T02:14:06Z"
}
tender_typeiscard,eftpos,cash,accountorother.outstanding_centsafter this payment lets us distinguish a deposit from a final payment without recomputing the transaction.payment_idmust be unique. We use it for payment-level idempotency alongsideevent_id.- Card fields are covered in section 8, payment identifiers immediately below.
Payment identifiers
These are the fields that let us tie your transaction to the processor's record and to what the cardholder's bank sees. Without at least one of them, matching falls back to amount and timestamp, which is a guess rather than a match. Send every identifier your terminal or gateway hands back. Null where genuinely absent, never fabricated.
| Field | Priority | What it is and why we want it |
|---|---|---|
acquirer_reference_number | Highest | The ARN. Rail-agnostic and the identifier a dispute is raised against, so it is the strongest correlation key we have. Often not present at authorisation and only appears at settlement, which is fine: send it later on a payment update and we will attach it |
arn_scheme | Highest | Lowercase card brand, sent whenever the ARN is sent, because ARN formats differ by scheme |
gateway | High | Lowercase name of whoever actually processed the card, for example tyro, square, stripe. Tells us which webhook stream to correlate against |
gateway_payment_id | High | The processor's own payment identifier. Deterministic match to the processor webhook, so where you have this we need nothing else to correlate |
gateway_order_id | Useful | Where the processor has an order layer above the payment |
rrn | Useful | Retrieval reference number from the authorisation. A fallback correlation key where no ARN is returned |
stan | Useful | System trace audit number. Only unique within a terminal and a day, so it needs terminal_id and paid_at alongside it |
terminal_id | Useful | The physical terminal, distinct from your own device_id. Disambiguates a lane when several take a payment in the same second |
settlement_batch_id | Useful | The batch this payment settled in, which is what makes end-of-day reconciliation exact rather than approximate |
auth_code | Useful | Six digits from the issuer. Weak on its own, strong combined with amount, terminal and timestamp |
If your terminal integration returns none of the top four, say so on the first call. The receipt still works, but the correlation to the processor and to a dispute is inference rather than a join, and we would rather design around that knowingly than discover it in production.
You do not send txn_hash. SyncPay derives it from the fields above and uses it to disambiguate a receipt lookup. It appears in the lookup API, not in your feed.
7. Example payloads
1. Simple sale. Three events produce a complete receipt. First transaction.created:
{
"event_id": "e1a6f0c2-8f14-4d0b-b6a3-90d1c47e2b11",
"event_type": "transaction.created",
"created_at": "2026-08-06T02:14:05Z",
"data": {
"transaction_id": "POS-TXN-0091822",
"type": "sale",
"sequence": 1,
"store_id": "ST-014",
"device_id": "REG-014-03",
"employee_id": "EMP-2291",
"currency": "AUD",
"subtotal_cents": 4541,
"gst_cents": 454,
"total_cents": 4995,
"customer": { "customer_email": "dana@example.com" },
"lines": [
{
"line_id": "L1",
"sku": "GLOVE-XL",
"description": "Riggers gloves XL",
"quantity": 3,
"unit_price_cents": 1665,
"line_total_cents": 4995,
"gst_cents": 454
}
],
"created_at": "2026-08-06T02:14:05Z"
}
}
Then payment.applied with sequence: 2, amount_cents: 4995 and outstanding_cents: 0, followed by transaction.completed with sequence: 3. That is the whole core integration.
2. Deposit and progress payment. One transaction, two payment.applied events weeks apart. The deposit does not equal the transaction total, and the outstanding balance carries the state.
{
"event_id": "5c9d1b77-2e40-4c53-8a02-f6e3d9a8c001",
"event_type": "payment.applied",
"created_at": "2026-08-06T03:02:11Z",
"data": {
"payment_id": "POS-PAY-3320502",
"transaction_id": "POS-TXN-0091901",
"sequence": 2,
"amount_cents": 100000,
"outstanding_cents": 274500,
"tender_type": "card",
"card_last4": "9903",
"paid_at": "2026-08-06T03:02:10Z"
}
}
{
"event_id": "77b02e19-64aa-4f1d-9d5c-08c1b4a2e442",
"event_type": "payment.applied",
"created_at": "2026-08-20T00:45:33Z",
"data": {
"payment_id": "POS-PAY-3325118",
"transaction_id": "POS-TXN-0091901",
"sequence": 6,
"amount_cents": 274500,
"outstanding_cents": 0,
"tender_type": "account",
"paid_at": "2026-08-20T00:45:31Z"
}
}
3. Line quantity change. The event carries both the new and previous quantity, plus the recalculated transaction total, so we never have to infer either.
{
"event_id": "0f4b9a63-71dd-4b1a-8c2e-5a90e7d3c210",
"event_type": "line.quantity_changed",
"created_at": "2026-08-07T05:20:44Z",
"data": {
"transaction_id": "POS-TXN-0091901",
"sequence": 3,
"line_id": "L2",
"quantity": 24,
"previous_quantity": 30,
"line_total_cents": 30000,
"new_transaction_total_cents": 367000,
"employee_id": "EMP-1104"
}
}
4. Line split. A thirty-unit line becomes twenty delivered plus ten on backorder. Same SKU, two lines: the original shrinks and a child line is created carrying parent_line_id.
{
"event_id": "9a1c3e55-b8f2-4770-a1d6-33e0c9b7f884",
"event_type": "line.split",
"created_at": "2026-08-08T01:12:09Z",
"data": {
"transaction_id": "POS-TXN-0091901",
"sequence": 4,
"source_line_id": "L1",
"source_line": {
"line_id": "L1",
"sku": "TIM-90x45-2.4",
"quantity": 20,
"line_total_cents": 25000,
"quantity_fulfilled": 20
},
"new_line": {
"line_id": "L1-B",
"parent_line_id": "L1",
"sku": "TIM-90x45-2.4",
"quantity": 10,
"line_total_cents": 12500,
"quantity_fulfilled": 0
},
"employee_id": "EMP-1104"
}
}
Partial fulfilment against either line then arrives as line.fulfilled events updating quantity_fulfilled. A matched return arrives as a transaction.created with type: "return" and original_transaction_id set, followed by its own payment.applied. Settlement direction is handled on your side: the return transaction's amounts are positive and the type carries the direction.
8. Card fields
card_last4,card_bin,card_typecard_entry_type: chip, contactless, swipe, keyed or on fileauth_code, plus AVS and CVV result codes where available
Null is acceptable where a value is genuinely absent. Do not populate a placeholder. If your terminal partner retains these fields, tell us early: the receipt still works at order level, with less to answer a dispute with.
- Full card number (PAN)
- CVV or CVC values
- Magnetic stripe track data
- PIN blocks or key material
SyncPay does not request, store or retain these values, and drops them on ingest if present. Excluding card data from the feed keeps your compliance scope unchanged.
9. What SyncPay does with the feed
Events land in an append-only event table keyed on event_id, then project into the receipt using our standard vocabulary: card_last4, card_bin, auth_code, customer_email, device_id, employee_id and the rest. You do not need to know or care about that projection. It is described here only to explain why we ask for these particular fields rather than a summary of the sale.
10. Base URL and authentication
Everything you call on our side lives under one base URL, JSON over HTTPS. The merchant API is the surface built for POS and platform vendors writing payment data. Your base URL is issued with your API key, so the examples below use {base_url} in its place.
{base_url}/{path_prefix}/{endpoint}
| Surface | Path prefix | Credential | Used for |
|---|---|---|---|
| Merchant API | /syncpay-api | sp_live_… | POS and platform vendors writing payment data |
| Receipt lookup | /syncpay-receipt-lookup | None | Consumer and banking apps resolving a code to a receipt |
Pass the key as a bearer token on every request.
curl {base_url}/syncpay-api/merchants/me \
-H "Authorization: Bearer sp_live_3f1c8e42-9b7a-4d21-8e6f-0c5a9d3b7e14"
Keep the key server side. It authorises everything that merchant can do, so it must never ship in browser or mobile code. If one leaks, mail dev@syncpay.au and we reissue it.
11. Conventions
| Item | Behaviour |
|---|---|
| Timestamps | ISO 8601 UTC, for example 2026-07-30T03:50:09.810Z |
| Currency | Lowercase on input, uppercase on output. Defaults to aud |
| Amounts | The merchant REST API takes dollars. The event feed you send us uses integer minor units, per section 4. Two different surfaces, so check which one you are calling |
| GST | Derived as one eleventh of the total, on the Australian tax-inclusive convention |
| Unknown fields | Ignored on input. Always tolerate new fields on output |
The amounts row is the one that catches people. Your inbound event feed is integer minor units because that is what a POS ledger holds; the REST API predates it and takes dollars. We will flag it again in your specification.
12. Errors
Errors return the matching HTTP status with a JSON body carrying an error string.
| Status | Meaning | What to do |
|---|---|---|
400 | Malformed or missing parameter | Read the error string and fix the request. Do not retry unchanged |
401 | Missing, malformed or unrecognised key | Check the Authorization header and the key prefix |
403 | Authenticated but not permitted | Stop requesting another merchant's data, or enable the feature in the portal |
404 | No such route, or no record matching your scope | Treat "not yours" and "does not exist" as the same outcome |
409 | Merchant is not attached to an organisation | Finish portal setup before calling again |
429 | Rate limited, or the monthly allowance is reached | Back off and retry later |
500 | Something failed on our side | Retry with backoff. If it persists, send us the timestamp |
502 | The upstream processor rejected the call | The error string is passed through from the processor |
Rate limits: the merchant API has no request-rate limit. Receipt lookup is 50 requests per minute per IP.
13. Retrieving a receipt
Once the events are in, the receipt resolves from its code. No credential is required for the code mode, because the code is the credential.
curl "{base_url}/syncpay-receipt-lookup?code=SPTC7K2M9X4A"
This is the endpoint behind a customer retrieving their own receipt, so you can surface it in your POS, put it on a QR code, or leave it to the merchant's banking app. A short code can collide, in which case the endpoint returns candidates rather than guessing, with no personal information beyond last-four fragments. Re-query with amount or txn_hash to resolve.
This page covers what a POS integration needs. For the full merchant and bank partner surface, email dev@syncpay.au.
14. Open items to confirm
These are the questions we work through with every partner before writing the final specification. None of them block a first call.
- Cursor semantics and retention window. How far back can we replay if a rebuild is needed?
- API key provisioning and rotation. How keys are issued and how a rotation is coordinated.
- Rate limits, or a recommended poll frequency.
- Sandbox access for integration testing.
- Which card fields your terminal integration returns to the POS. This is the one that changes the value of the receipt most.
15. Scope of work
On your side: an events endpoint over data you already write, an API key, and sandbox access. No change to the payment flow, the terminal, or the counter. On our side: one poller or webhook handler, one projection into the receipt, one backfill job and one migration, all built and maintained by SyncPay. The pattern is identical to every processor and POS already connected.
Send us a sample payload from your system and we will return the receipt it produces, so you can see the output before committing to anything. dev@syncpay.au