Base URL https://cashier.basmtak.com/api/partner/v1 · OpenAPI 3.0.3 · Download the OpenAPI file
Read sales and the catalogue, write items, send orders to the till and receive signed webhooks. Money is always integer halalas (1 SAR = 100). Errors are JSON {error, message, details}; the message follows Accept-Language (ar or en). Lists answer {data, meta}.
Authentication
An account owner or manager creates an API client under Back office → Developers and chooses its abilities. The token is shown once. Send it on every call:
curl -H "Authorization: Bearer bmc_…" -H "Accept-Language: ar" https://cashier.basmtak.com/api/partner/v1/ping
Abilities
pos:read | GET branches · categories · items · price-lists · customers · receipts · sessions/{id}/z-report · stock/levels |
pos:write | POST /items · PUT /items/{id} |
pos:orders | POST /orders · GET /orders/{id} |
pos:webhooks | GET|POST|DELETE /webhooks |
Rate limit
120 requests a minute per API client. A 429 answer carries Retry-After; every answer carries X-RateLimit-Limit and X-RateLimit-Remaining.
Errors
Every error is JSON {error, message, details}; the message follows Accept-Language (ar or en). Money is always integer halalas (1 SAR = 100).
{"error": "insufficient_ability", "message": "…", "details": {"required_ability": "pos:write"}}
Webhooks (REST hooks)
Subscribe with POST /webhooks {target_url, event} and unsubscribe with DELETE /webhooks/{id} — the Zapier and Make REST-hook pattern. Each delivery is a POST of {id, event, tenant_id, occurred_at, api_version, data}, retried with exponential backoff; 20 failures in a row (or a 410 answer) switch the subscription off.
Verifying a delivery
X-Basmtak-Signature = "sha256=" + hex HMAC-SHA256(secret, X-Basmtak-Timestamp + "\n" + "POST" + "\n" + path + "\n" + hex SHA-256(raw body)). Reject timestamps older than five minutes. X-Basmtak-Event-Id is unique per event — use it to drop duplicates.
// Node.js
const crypto = require('crypto');
const canonical = ts + "\nPOST\n" + new URL(targetUrl).pathname + "\n" + crypto.createHash('sha256').update(rawBody).digest('hex');
const ok = 'sha256=' + crypto.createHmac('sha256', secret).update(canonical).digest('hex') === req.headers['x-basmtak-signature'];
Events
receipt.issued
Sample data
{
"id": "01J9ZK3Q7R8S9T0V1W2X3Y4Z5A",
"number": "R1-418",
"kind": "simplified",
"branch_id": 1,
"register_id": 3,
"session_id": "01J9ZJ0000000000000000000S",
"issued_at": "2027-04-02T18:21:07+03:00",
"business_date": "2027-04-02",
"currency": "SAR",
"subtotal_halalas": 3130,
"tax_halalas": 470,
"total_halalas": 3600,
"prepaid_applied": null,
"amount_due_halalas": 3600,
"lines": [
{
"line_no": 1,
"variant_id": 21,
"name": "كابتشينو كبير",
"qty": "2",
"unit_price_halalas": 1800,
"total_halalas": 3600
}
],
"payments": [
{
"tender": "card",
"amount_halalas": 3600,
"scheme": "mada",
"last4": "4242"
}
]
}
credit_note.issued
Sample data
{
"kind": "credit_note",
"refund_of": "01J9ZK3Q7R8S9T0V1W2X3Y4Z5A",
"total_halalas": 1800,
"id": "01J9ZK3Q7R8S9T0V1W2X3Y4Z5A",
"number": "R1-418",
"branch_id": 1,
"register_id": 3,
"session_id": "01J9ZJ0000000000000000000S",
"issued_at": "2027-04-02T18:21:07+03:00",
"business_date": "2027-04-02",
"currency": "SAR",
"subtotal_halalas": 3130,
"tax_halalas": 470,
"prepaid_applied": null,
"amount_due_halalas": 3600,
"lines": [
{
"line_no": 1,
"variant_id": 21,
"name": "كابتشينو كبير",
"qty": "2",
"unit_price_halalas": 1800,
"total_halalas": 3600
}
],
"payments": [
{
"tender": "card",
"amount_halalas": 3600,
"scheme": "mada",
"last4": "4242"
}
]
}
session.closed
Sample data
{
"id": "01J9ZJ0000000000000000000S",
"branch_id": 1,
"register_id": 3,
"status": "closed",
"business_date": "2027-04-02",
"receipts": 57,
"total_halalas": 412300,
"net_sales_halalas": 358522,
"variance_halalas": -50,
"z_report": {
"sales": {
"total_halalas": 412300
}
}
}
stock.low
Sample data
{
"branch_id": 1,
"variant_id": 21,
"sku": "MILK-1L",
"name_ar": "حليب ١ لتر",
"qty": "3.0000",
"reorder_point": "5.0000",
"suggested_qty": "7.0000"
}
order.status_changed
Sample data
{
"from": "received",
"to": "accepted",
"id": "01J9ZM0000000000000000000O",
"external_id": "web-1001",
"provider": "partner:4",
"status": "accepted",
"total_halalas": 5400
}
customer.created
Sample data
{
"id": 77,
"marketing_consent": true,
"name": "نورة",
"phone": "+966501234567",
"email": null,
"vat_number": null
}
Endpoints
meta Connectivity
GET
/ping
Check the token and see the client's abilities
Ability: any
Responses
200 | The client, its abilities and the account | |
401 | {error, message, details} | Error |
catalogue Branches, categories, items, price lists and customers
GET
/branches
List branches
Ability: pos:read
Parameters
page | query | integer | |
per_page | query | integer | |
Responses
200 | A page of branches | BranchPage |
GET
/categories
List categories
Ability: pos:read
Parameters
page | query | integer | |
per_page | query | integer | |
Responses
GET
/items
List items with their variants
Ability: pos:read
Each variant carries `plu` (V + variant id), the reference to use in orders.
Parameters
page | query | integer | |
per_page | query | integer | |
category_id | query | integer | |
updated_since | query | string | ISO-8601; only items changed since then |
sku | query | string | |
barcode | query | string | |
Responses
200 | A page of items | ItemPage |
POST
/items
Create an item (one payload with its variants)
Ability: pos:write
Same payload as the back office. Barcodes and SKUs are unique per account. Without variants one default variant is made at default_price_halalas.
Request body ItemWrite
{
"name_ar": "كابتشينو",
"name_en": "Cappuccino",
"category_id": 3,
"variants": [
{
"name_ar": "صغير",
"sku": "CAP-S",
"price_halalas": 1400,
"is_default": true
},
{
"name_ar": "كبير",
"sku": "CAP-L",
"price_halalas": 1800
}
]
} Responses
201 | The item | object |
403 | {error, message, details} | Error |
422 | {error, message, details} | Error |
GET
/items/{id}
One item
Ability: pos:read
Parameters
Responses
200 | The item | |
404 | {error, message, details} | Error |
PUT
/items/{id}
Update an item
Ability: pos:write
Arrays present in the body (variants, modifier_group_ids, combo_slots) replace what is stored; rows without id are created, missing ids are deleted. Arrays absent are untouched.
Parameters
Request body ItemWrite
Responses
200 | The item | |
422 | {error, message, details} | Error |
GET
/price-lists
Price lists with their prices
Ability: pos:read
Parameters
page | query | integer | |
per_page | query | integer | |
Responses
GET
/customers
Customers (consented fields only)
Ability: pos:read
Name, phone, email and address are returned only while the customer's marketing consent is granted (PDPL); otherwise they are null. VAT and CR numbers are always returned.
Parameters
page | query | integer | |
per_page | query | integer | |
updated_since | query | string | |
consented_only | query | boolean | Only customers whose latest marketing consent in the consent ledger is granted (never erased ones). |
consent_channel | query | string | With consented_only, only consents that cover this channel. |
Responses
200 | A page of customers | object |
sales Receipts, Z-reports and stock
GET
/receipts
Receipts and credit notes, oldest first, with a since_id cursor
Ability: pos:read
Pass the last receipt id you saw as since_id to get what arrived after it; meta.next_since_id is the value for the next call and meta.has_more says whether to call again now. The order is the server's arrival order, so receipts a till sold offline and synced later are never skipped. Receipts that arrived in the last few seconds are held back.
Parameters
since_id | query | string | |
branch_id | query | integer | |
kind | query | string | |
per_page | query | integer | |
Responses
200 | Receipts after the cursor | ReceiptCursorPage |
422 | {error, message, details} | Error |
GET
/receipts/{id}
One receipt or credit note
Ability: pos:read
Parameters
Responses
200 | The receipt | object |
404 | {error, message, details} | Error |
GET
/sessions/{id}/z-report
The Z-report of a closed till session
Ability: pos:read
The Z as stored at close; deposits already invoiced are netted (prepayments_applied).
Parameters
Responses
200 | The session with z_report, expected and counted | |
409 | {error, message, details} | Error |
GET
/stock/levels
Stock on hand per branch and variant
Ability: pos:read
Parameters
page | query | integer | |
per_page | query | integer | |
branch_id | query | integer | |
variant_id | query | integer | |
low_only | query | boolean | |
Responses
200 | A page of stock levels | |
orders Orders sent to the till's incoming-orders panel
POST
/orders
Send an order to the till
Ability: pos:orders
The order appears on the branch till's incoming-orders panel; the cashier accepts it and the receipt is issued on the till. Idempotent on external_id per API client: a retry answers 200 with duplicate true. Lines reference a variant by variant_id, or plu / sku / barcode; a line nothing matches stays an open item with your name and price. Prices are VAT-inclusive halalas; a line without a price takes the catalogue price. Status changes come back as order.status_changed webhooks.
Request body OrderWrite
{
"external_id": "web-1001",
"branch_id": 1,
"order_type": "pickup",
"prepaid": true,
"customer": {
"name": "Noura",
"phone": "+966501234567"
},
"lines": [
{
"variant_id": 21,
"qty": 2,
"unit_price_halalas": 1800
}
]
} Responses
201 | The order | object |
200 | The same order again (a retry) | |
422 | {error, message, details} | Error |
GET
/orders/{id}
The status of one of your orders
Ability: pos:orders
Parameters
Responses
200 | The order | |
404 | {error, message, details} | Error |
POST
/orders/{id}/cancel
Cancel one of your orders
Ability: pos:orders
Withdraws the order from the till's incoming-orders panel. Cancelling an order that is already cancelled answers the same order again. If the till already issued its receipt, the receipt stands and the cashier is asked to decide on a refund.
Parameters
Request body object
Responses
200 | The order, now cancelled | object |
404 | {error, message, details} | Error |
409 | {error, message, details} | Error |
webhooks REST hooks (Zapier and Make)
GET
/webhooks
Your subscriptions
Ability: pos:webhooks
Responses
200 | A page of subscriptions | |
POST
/webhooks
Subscribe (REST hook)
Ability: pos:webhooks
The answer carries the subscription id and its signing secret. The secret is shown only here.
Request body object
{
"target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
"event": "receipt.issued"
} Responses
201 | {id, secret, data} | |
422 | {error, message, details} | Error |
GET
/webhooks/events
The events, each with sample data
Ability: pos:webhooks
Responses
200 | {data: [{event, sample}]} | |
GET
/webhooks/{id}
One subscription
Ability: pos:webhooks
Parameters
Responses
DELETE
/webhooks/{id}
Unsubscribe (REST hook)
Ability: pos:webhooks
Parameters
Responses
204 | Removed | |
404 | {error, message, details} | Error |