Push paid orders into Qashier, pull products and stock, and receive webhooks when stock moves or an order is cancelled.
Authentication. A bearer API key, created by the account owner in HQ under Settings → API. The key is bound to one client and to a set of that client's stores. The client is never sent — it comes from the key — and every storeId, in a path or a body, must be inside the key's scope: a store that is not is 403 STORE_NOT_IN_KEY_SCOPE, and a store id that does not exist or belongs to someone else is 404 STORE_NOT_FOUND. A key can only ever act on its own merchant's data.
Money is an integer in minor units throughout — 4550 is MYR 45.50. No floats cross this boundary.
Idempotency. POST /orders requires X-Idempotency-Key. A replay returns the original order; it does not create a second transaction, deduct stock again, or print again.
Rate limits. Every response to an authenticated request carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 also carries Retry-After in seconds. A 401 carries none of them: the bucket is per credential, and there is no credential.
Request ids. Every response carries X-Request-Id. Quote it to support — it resolves to one trace on our side.
Webhooks. inventory.changed fires for every stock movement in the store whatever caused it, and transaction.cancelled when a cancellation finalizes — which is not the moment staff start one, so read the order back for in-flight state. Delivery is at-least-once and unordered: dedupe on eventId, and for stock keep the latest occurredAt per SKU and drop anything older — resultingBalance is absolute, which makes that comparison enough, but it does not make it optional. Each delivery is signed as X-Qashier-Signature: t=<unix>,v1=<hex>, an HMAC-SHA256 over "{timestamp}.{rawBody}"; reject anything older than five minutes. Payload shapes are the InventoryChangedEvent and TransactionCancelledEvent components.
If a delivery fails we retry with exponential backoff for 24 hours. Reconcile daily against GET /stores/{storeId}/stock — that pull is the recovery path for any event we gave up on.
Every store this credential can reach. Lets a partner discover store ids instead of being sent a list by email, which goes stale the first time the merchant opens an outlet.
| limit | integer [ 1 .. 200 ] Default: 100 |
| cursor | string non-empty |
{- "data": [
- {
- "storeId": "string",
- "name": "string",
- "timezone": "string",
- "currency": "string"
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}The catalogue a partner builds its menu from: per-store price, modifiers, variants, SKUs and the track-inventory flag. duplicateSku marks a SKU that matches more than one product, because such a line becomes a custom item with no stock movement and the partner should see that here rather than as stock drift later.
| storeId required | string [ 1 .. 64 ] characters |
| limit | integer [ 1 .. 200 ] Default: 100 |
| cursor | string non-empty |
{- "data": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "productId": "string",
- "category": "string",
- "tab": "string",
- "variants": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "variantId": "string"
}
], - "modifiers": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true
}
], - "duplicateSku": true
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}Current quantity per SKU. This is also the daily reconciliation path: webhook delivery is at-least-once and unordered, so a partner that missed events converges by pulling this.
| storeId required | string [ 1 .. 64 ] characters |
| limit | integer [ 1 .. 200 ] Default: 100 |
| cursor | string non-empty |
string or Array of strings Repeatable. Omit for every tracked item in the store |
{- "data": [
- {
- "sku": "string",
- "productId": "string",
- "variantId": "string",
- "available": 0,
- "trackInventory": true,
- "updatedAt": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}Creates the transaction already paid, in one call. Qashier recomputes the total from the store's own tax, service-charge and rounding settings and rejects the order if it differs from amountCharged — nothing is persisted, deducted or printed on a rejection. X-Idempotency-Key is required; send the partner order id.
| x-idempotency-key required | string [ 1 .. 200 ] characters Required. Send your own order id. A replay returns the original order rather than creating a second one; the same key with a different body is a 409. |
| storeId required | string [ 1 .. 64 ] characters | ||||||||||||||
| externalOrderReference required | string [ 1 .. 100 ] characters | ||||||||||||||
| fulfilmentType required | string Enum: "PICKUP" "DELIVERY" | ||||||||||||||
required | Array of objects [ 1 .. 200 ] items | ||||||||||||||
Array ([ 1 .. 200 ] items)
| |||||||||||||||
Array of objects <= 20 items Default: [] | |||||||||||||||
Array (<= 20 items)
| |||||||||||||||
| amountCharged required | integer >= 0 What the partner actually collected. Rejected if it differs from our computed total | ||||||||||||||
| currency required | string^[A-Z]{3}$ ISO 4217, uppercase | ||||||||||||||
required | object | ||||||||||||||
| |||||||||||||||
| note | string <= 1000 characters | ||||||||||||||
| printOrderChit | boolean Default: true | ||||||||||||||
| showInKds | boolean Default: true | ||||||||||||||
{- "storeId": "string",
- "externalOrderReference": "string",
- "fulfilmentType": "PICKUP",
- "lineItems": [
- {
- "sku": "string",
- "name": "string",
- "quantity": 1,
- "unitPrice": 0,
- "modifiers": [ ],
- "discounts": [ ],
- "note": "string"
}
], - "discounts": [ ],
- "amountCharged": 0,
- "currency": "string",
- "payment": {
- "reference": "string",
- "paidAt": "2019-08-24T14:15:22Z"
}, - "note": "string",
- "printOrderChit": true,
- "showInKds": true
}{- "data": {
- "transactionId": "string",
- "receiptNumber": "string",
- "externalOrderReference": "string",
- "storeId": "string",
- "status": "PAID",
- "totals": {
- "subtotal": 0,
- "itemDiscount": 0,
- "billDiscount": 0,
- "serviceCharge": 0,
- "tax": 0,
- "rounding": 0,
- "total": 0,
- "currency": "string"
}, - "lines": [
- {
- "index": 0,
- "sku": "string",
- "productId": "string",
- "computedAmount": 0
}
], - "cancellation": {
- "status": "PENDING",
- "initiatedAt": "2019-08-24T14:15:22Z",
- "completedAt": "2019-08-24T14:15:22Z",
- "tasks": [
- {
- "task": "REFUND",
- "status": "PENDING"
}
]
}, - "createdAt": "2019-08-24T14:15:22Z"
}
}What we stored, keyed by the transactionId the create answered with. Needed for partner-side reconciliation and so support can answer a question without a Qashier login.
Keep that id against your own order. If you lose it, replay the create with the same X-Idempotency-Key — a replay returns the original order rather than creating a second one, so the id comes back. ORDER_NOT_FOUND also answers a transaction belonging to another client, so this read cannot be used to discover whether an id exists elsewhere.
This is also where a cancellation in progress is visible. A cancellation reverses several things and finalizes only when all of them are done, and the refund leg of an order paid on your own gateway waits for a cashier — so status: CANCELLING with per-task detail in cancellation.tasks is the normal path, not a rare window. transaction.cancelled is delivered when the cancellation finalizes, so poll this to see where a cancellation has got to before then.
| transactionId required | string [ 1 .. 100 ] characters The |
{- "data": {
- "transactionId": "string",
- "receiptNumber": "string",
- "externalOrderReference": "string",
- "storeId": "string",
- "status": "PAID",
- "totals": {
- "subtotal": 0,
- "itemDiscount": 0,
- "billDiscount": 0,
- "serviceCharge": 0,
- "tax": 0,
- "rounding": 0,
- "total": 0,
- "currency": "string"
}, - "lines": [
- {
- "index": 0,
- "sku": "string",
- "productId": "string",
- "computedAmount": 0
}
], - "cancellation": {
- "status": "PENDING",
- "initiatedAt": "2019-08-24T14:15:22Z",
- "completedAt": "2019-08-24T14:15:22Z",
- "tasks": [
- {
- "task": "REFUND",
- "status": "PENDING"
}
]
}, - "createdAt": "2019-08-24T14:15:22Z"
}
}Fires for every stock movement in a store whose credential has a webhook URL, whatever caused it: a sale through this API, a sale on the POS, a restock or adjustment in HQ, a stock take, a transfer, or a cancellation putting quantities back. reason says which. A product that does not track inventory never produces one, and neither does a custom line whose SKU matched nothing.
Ordering. Two events for the same SKU can arrive in either order. quantityChange is what moved and resultingBalance is the quantity after it moved, so never apply quantityChange as a delta — but an absolute value does not make order irrelevant: if you store whatever arrives last, a reversed pair leaves you on the older number. Keep the occurredAt you last applied per SKU and drop any event whose occurredAt is earlier. Compare occurredAt (when stock moved), not createdAt (when we emitted the event).
If you miss events, GET /stores/{storeId}/stock is the recovery path and the daily reconciliation path. It is the authority; a webhook is a notification.
Delivery is at-least-once and unordered. Dedupe on eventId, which is stable across every retry of the same event. Order between different events is not guaranteed either; what that means for you is specific to the event and described above.
Retries. An attempt that does not return 2xx is retried with exponential backoff and equal jitter: the window doubles from 10 seconds to a ceiling of 1 hour, and each attempt fires at a random point in the second half of its window, so a fleet of failed deliveries does not return as a thundering herd. We give up after 30 attempts or 24 hours, whichever comes first.
Sustained failure. After 20 consecutive failures across all events we pause delivery for your credential and probe every 5 minutes; two successful probes resume it. Queued events are not discarded while paused — they drain once delivery resumes, subject to the 24-hour limit above.
| X-Qashier-Signature required | string Signature of this delivery: |
| X-Request-Id | string The trace id of the delivery attempt. Quote it to support and it resolves to one trace on our side. |
| eventId required | string <uuid> | ||||||||||||||||||||
| type required | string Value: "inventory.changed" | ||||||||||||||||||||
| createdAt required | string <date-time> | ||||||||||||||||||||
required | object | ||||||||||||||||||||
| |||||||||||||||||||||
{- "eventId": "d6703cc8-9e79-415d-ac03-a4dc7f6ab43c",
- "type": "inventory.changed",
- "createdAt": "2019-08-24T14:15:22Z",
- "data": {
- "storeId": "string",
- "sku": "string",
- "productId": "string",
- "variantId": "string",
- "productName": "string",
- "quantityChange": 0,
- "resultingBalance": 0,
- "reason": "SALE",
- "transactionId": "string",
- "occurredAt": "2019-08-24T14:15:22Z"
}
}Fires when a cancellation finalizes, not when staff start one. A cancellation reverses several things — stock, the refund leg, loyalty, a table session — and finalizes once every one of them is terminal; until then the order reads status: CANCELLING on GET /orders/{transactionId} with a task breakdown. Because Qashier never held the money for your orders, the refund leg waits for a cashier to record how it was returned, so expect a gap between the staff action and this event rather than treating it as immediate.
It reports that the order is cancelled in Qashier and what staff recorded as refunded, which may be 0 — it does not move money. Refunding the customer is yours to do on the gateway that took the payment. Each restored line also produces an inventory.changed with reason: CANCELLATION_RESTORE.
outcome says whether every reversal succeeded. COMPLETED_WITH_FAILURES means the order is cancelled but at least one reversal did not go through and staff have to finish it by hand — read the order back for cancellation.tasks to see which.
Ordering. One order cancels once, so there is nothing to order between deliveries of this event. It can arrive before or after the inventory.changed events for the same cancellation; neither implies the other has been delivered.
If you miss it, GET /orders/{transactionId} shows status: CANCELLED and the cancellation block with the same outcome. That read is the authority; this event is a notification.
Delivery is at-least-once and unordered. Dedupe on eventId, which is stable across every retry of the same event. Order between different events is not guaranteed either; what that means for you is specific to the event and described above.
Retries. An attempt that does not return 2xx is retried with exponential backoff and equal jitter: the window doubles from 10 seconds to a ceiling of 1 hour, and each attempt fires at a random point in the second half of its window, so a fleet of failed deliveries does not return as a thundering herd. We give up after 30 attempts or 24 hours, whichever comes first.
Sustained failure. After 20 consecutive failures across all events we pause delivery for your credential and probe every 5 minutes; two successful probes resume it. Queued events are not discarded while paused — they drain once delivery resumes, subject to the 24-hour limit above.
| X-Qashier-Signature required | string Signature of this delivery: |
| X-Request-Id | string The trace id of the delivery attempt. Quote it to support and it resolves to one trace on our side. |
| eventId required | string <uuid> | ||||||||||||||||||||||
| type required | string Value: "transaction.cancelled" | ||||||||||||||||||||||
| createdAt required | string <date-time> | ||||||||||||||||||||||
required | object | ||||||||||||||||||||||
| |||||||||||||||||||||||
{- "eventId": "d6703cc8-9e79-415d-ac03-a4dc7f6ab43c",
- "type": "transaction.cancelled",
- "createdAt": "2019-08-24T14:15:22Z",
- "data": {
- "storeId": "string",
- "transactionId": "string",
- "externalOrderReference": "string",
- "receiptNumber": "string",
- "orderTotal": 0,
- "refundAmount": 0,
- "currency": "string",
- "reason": "string",
- "cancelledBy": "string",
- "cancelledAt": "2019-08-24T14:15:22Z",
- "outcome": "COMPLETED"
}
}| code required | string^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$ |
| message required | string non-empty |
| details | object |
{- "code": "string",
- "message": "string",
- "details": { }
}| timestamp required | string <date-time> |
| requestId required | string non-empty |
{- "timestamp": "2019-08-24T14:15:22Z",
- "requestId": "string"
}| page required | integer >= 1 |
| size required | integer [ 1 .. 100 ] |
| total required | integer >= 0 |
| pages required | integer >= 0 |
{- "page": 1,
- "size": 1,
- "total": 0,
- "pages": 0
}| limit required | integer >= 1 |
| next required | string or null |
| more required | boolean |
{- "limit": 1,
- "next": "string",
- "more": true
}| eventId required | string <uuid> | ||||||||||||||||||||
| type required | string Value: "inventory.changed" | ||||||||||||||||||||
| createdAt required | string <date-time> | ||||||||||||||||||||
required | object | ||||||||||||||||||||
| |||||||||||||||||||||
{- "eventId": "d6703cc8-9e79-415d-ac03-a4dc7f6ab43c",
- "type": "inventory.changed",
- "createdAt": "2019-08-24T14:15:22Z",
- "data": {
- "storeId": "string",
- "sku": "string",
- "productId": "string",
- "variantId": "string",
- "productName": "string",
- "quantityChange": 0,
- "resultingBalance": 0,
- "reason": "SALE",
- "transactionId": "string",
- "occurredAt": "2019-08-24T14:15:22Z"
}
}| eventId required | string <uuid> | ||||||||||||||||||||||
| type required | string Value: "transaction.cancelled" | ||||||||||||||||||||||
| createdAt required | string <date-time> | ||||||||||||||||||||||
required | object | ||||||||||||||||||||||
| |||||||||||||||||||||||
{- "eventId": "d6703cc8-9e79-415d-ac03-a4dc7f6ab43c",
- "type": "transaction.cancelled",
- "createdAt": "2019-08-24T14:15:22Z",
- "data": {
- "storeId": "string",
- "transactionId": "string",
- "externalOrderReference": "string",
- "receiptNumber": "string",
- "orderTotal": 0,
- "refundAmount": 0,
- "currency": "string",
- "reason": "string",
- "cancelledBy": "string",
- "cancelledAt": "2019-08-24T14:15:22Z",
- "outcome": "COMPLETED"
}
}| amountCharged required | integer Amount in minor units, e.g. 4550 is MYR 45.50 | ||||||
| computedTotal required | integer Amount in minor units, e.g. 4550 is MYR 45.50 | ||||||
| currency required | string^[A-Z]{3}$ ISO 4217, uppercase | ||||||
required | Array of objects | ||||||
Array
| |||||||
{- "amountCharged": 0,
- "computedTotal": 0,
- "currency": "string",
- "lines": [
- {
- "index": 0,
- "sku": "string",
- "computedAmount": 0
}
]
}| code required | string Value: "STORE_NOT_IN_KEY_SCOPE" |
| message required | string non-empty |
| details | object |
{- "code": "STORE_NOT_IN_KEY_SCOPE",
- "message": "string",
- "details": { }
}| code required | string Value: "STORE_NOT_FOUND" |
| message required | string non-empty |
| details | object |
{- "code": "STORE_NOT_FOUND",
- "message": "string",
- "details": { }
}| code required | string Value: "ORDER_NOT_FOUND" |
| message required | string non-empty |
| details | object |
{- "code": "ORDER_NOT_FOUND",
- "message": "string",
- "details": { }
}| code required | string Value: "IDEMPOTENCY_KEY_CONFLICT" |
| message required | string non-empty |
| details | object |
{- "code": "IDEMPOTENCY_KEY_CONFLICT",
- "message": "string",
- "details": { }
}| code required | string Value: "AMOUNT_MISMATCH" | ||||||||||||||||
| message required | string non-empty | ||||||||||||||||
required | object (AmountMismatchDetails) | ||||||||||||||||
| |||||||||||||||||
{- "code": "AMOUNT_MISMATCH",
- "message": "string",
- "details": {
- "amountCharged": 0,
- "computedTotal": 0,
- "currency": "string",
- "lines": [
- {
- "index": 0,
- "sku": "string",
- "computedAmount": 0
}
]
}
}| storeId required | string [ 1 .. 64 ] characters |
| name required | string non-empty |
| timezone required | string non-empty IANA name, e.g. Asia/Kuala_Lumpur |
| currency required | string^[A-Z]{3}$ ISO 4217, uppercase |
{- "storeId": "string",
- "name": "string",
- "timezone": "string",
- "currency": "string"
}required | Array of objects (Store) | ||||||||
Array
| |||||||||
required | object | ||||||||
| |||||||||
{- "data": [
- {
- "storeId": "string",
- "name": "string",
- "timezone": "string",
- "currency": "string"
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}| sku required | string or null [ 1 .. 64 ] characters Null when the merchant has not set one | ||||||||||
| name required | string non-empty | ||||||||||
| price required | integer Price at this store, after any store tier | ||||||||||
| trackInventory required | boolean | ||||||||||
| productId required | string non-empty | ||||||||||
| category required | string or null | ||||||||||
| tab required | string or null | ||||||||||
required | Array of objects | ||||||||||
Array
| |||||||||||
required | Array of objects | ||||||||||
Array
| |||||||||||
| duplicateSku required | boolean | ||||||||||
{- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "productId": "string",
- "category": "string",
- "tab": "string",
- "variants": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "variantId": "string"
}
], - "modifiers": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true
}
], - "duplicateSku": true
}required | Array of objects (Product) | ||||||||||||||||||||
Array
| |||||||||||||||||||||
required | object | ||||||||||||||||||||
| |||||||||||||||||||||
{- "data": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "productId": "string",
- "category": "string",
- "tab": "string",
- "variants": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true,
- "variantId": "string"
}
], - "modifiers": [
- {
- "sku": "string",
- "name": "string",
- "price": 0,
- "trackInventory": true
}
], - "duplicateSku": true
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}| sku required | string or null [ 1 .. 64 ] characters The merchant's SKU, or null when they have not set one. Such a row cannot be filtered by |
| productId required | string non-empty |
| variantId required | string or null non-empty |
| available required | number Quantity a customer can still buy: on hand minus what open bills are holding. A hold counts until it is confirmed or released. |
| trackInventory required | boolean |
| updatedAt required | string <date-time> |
{- "sku": "string",
- "productId": "string",
- "variantId": "string",
- "available": 0,
- "trackInventory": true,
- "updatedAt": "2019-08-24T14:15:22Z"
}required | Array of objects (StockLevel) | ||||||||||||
Array
| |||||||||||||
required | object | ||||||||||||
| |||||||||||||
{- "data": [
- {
- "sku": "string",
- "productId": "string",
- "variantId": "string",
- "available": 0,
- "trackInventory": true,
- "updatedAt": "2019-08-24T14:15:22Z"
}
], - "pagination": {
- "limit": 1,
- "next": "string",
- "more": true
}
}| transactionId required | string non-empty | ||||||||||||||||
| receiptNumber required | string non-empty | ||||||||||||||||
| externalOrderReference required | string non-empty | ||||||||||||||||
| storeId required | string [ 1 .. 64 ] characters | ||||||||||||||||
| status required | string Enum: "PAID" "CANCELLING" "CANCELLED"
| ||||||||||||||||
required | object | ||||||||||||||||
| |||||||||||||||||
required | Array of objects | ||||||||||||||||
Array
| |||||||||||||||||
required | object or null Present once a cancellation has been started, and kept afterwards so the outcome stays readable. Null for an order nobody has cancelled. | ||||||||||||||||
| |||||||||||||||||
| createdAt required | string <date-time> | ||||||||||||||||
{- "transactionId": "string",
- "receiptNumber": "string",
- "externalOrderReference": "string",
- "storeId": "string",
- "status": "PAID",
- "totals": {
- "subtotal": 0,
- "itemDiscount": 0,
- "billDiscount": 0,
- "serviceCharge": 0,
- "tax": 0,
- "rounding": 0,
- "total": 0,
- "currency": "string"
}, - "lines": [
- {
- "index": 0,
- "sku": "string",
- "productId": "string",
- "computedAmount": 0
}
], - "cancellation": {
- "status": "PENDING",
- "initiatedAt": "2019-08-24T14:15:22Z",
- "completedAt": "2019-08-24T14:15:22Z",
- "tasks": [
- {
- "task": "REFUND",
- "status": "PENDING"
}
]
}, - "createdAt": "2019-08-24T14:15:22Z"
}required | object (Order) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{- "data": {
- "transactionId": "string",
- "receiptNumber": "string",
- "externalOrderReference": "string",
- "storeId": "string",
- "status": "PAID",
- "totals": {
- "subtotal": 0,
- "itemDiscount": 0,
- "billDiscount": 0,
- "serviceCharge": 0,
- "tax": 0,
- "rounding": 0,
- "total": 0,
- "currency": "string"
}, - "lines": [
- {
- "index": 0,
- "sku": "string",
- "productId": "string",
- "computedAmount": 0
}
], - "cancellation": {
- "status": "PENDING",
- "initiatedAt": "2019-08-24T14:15:22Z",
- "completedAt": "2019-08-24T14:15:22Z",
- "tasks": [
- {
- "task": "REFUND",
- "status": "PENDING"
}
]
}, - "createdAt": "2019-08-24T14:15:22Z"
}
}| code required | string Value: "INVALID_REQUEST" |
| message required | string non-empty |
| details | object |
{- "code": "INVALID_REQUEST",
- "message": "string",
- "details": { }
}| code required | string Value: "UNAUTHENTICATED" |
| message required | string non-empty |
| details | object |
{- "code": "UNAUTHENTICATED",
- "message": "string",
- "details": { }
}| code required | string Value: "RATE_LIMITED" |
| message required | string non-empty |
| details | object |
{- "code": "RATE_LIMITED",
- "message": "string",
- "details": { }
}