1. The one rule
Your system is authoritative for stock. Stockwaka is a mirror.
A retail chain will not hand its stock ledger to a new vendor, and two systems each applying their own changes diverge permanently the first time one of them misses a message. So Stockwaka does not try to be a second ledger.
Concretely, that means three things:
- A stock sync SETS an absolute on-hand figure. It never applies a change. Re-sending the same payload does nothing; a payload you dropped is corrected by the next one. There is no ordering requirement between pages and no reconciliation protocol to implement.
- Stockwaka holds only a short reservation window against your figure while a shopper pays — never a parallel count. See Reservations, below.
- Stockwaka orders are requests you accept, not deductions imposed on you. You take an order in, acknowledge it, and your next sync carries the consequence back to us as an ordinary absolute count.
If you are ever tempted to send us what changed since last time, send the current count instead. The API rejects a negative quantity for exactly this reason.
2. Getting access
There is no self-service signup. A partner integration is set up with a person, because binding your store codes to the right shops is a decision nobody should make by guessing.
- 1Get in touch. Email support@bitasei.com with your company, roughly how many stores you run, and which system will be talking to us (SAP, Odoo, Microsoft Dynamics, an in-house ERP — it makes no difference to the API, but it tells us who to put you with).
- 2A short technical call. We agree which stores go live first, and which scopes each of your jobs needs.
- 3Your account and your stores are set up. One Stockwaka account for the chain, with one shop under it per store — created and named from your own store list, however many that is. If some of your branches already use Stockwaka on WhatsApp, nothing about their accounts changes.
- 4We issue your test key. It arrives once, over a secure channel, and starts with
swp_test_. Stockwaka stores only a hash of it, so it can never be shown again — a lost key is replaced, not recovered. - 5You build against the sandbox using the reference below. Start with
GET /partner/whoami; if that returns your business name, everything else is downhill. - 6Bind your store codes with
PUT /partner/stores/:code, once per store. The call is idempotent, so it belongs in your deploy script rather than in a runbook. - 7Pilot on a handful of real stores. We watch the first syncs together — the two numbers that matter early are
unpricedCountandskipped, which between them tell you whether your export is mapping cleanly. - 8Go live. We issue the production key (
swp_live_) and you bring stores on as fast as you like. Nothing is all-or-nothing: a store whose code you have not bound yet is simply not synced, and binding it later needs no change on our side.
One key. Every store.
A key belongs to your company account, not to a store, and it reaches every shop under it. The storeCode on each request is an address, not a credential — it picks the shelf, while the key proves it is you asking. A chain of two hundred stores needs exactly as many keys as a chain of one.
The only reason to hold more than one key is to separate jobs, not stores: give your stock job stock:read and stock:write, your order collector orders:read and orders:ack. Then a bug in one cannot damage the other’s domain, and a leaked key is partial rather than total.
What your merchant sees
Nothing changes for them. Stockwaka stays what it was — voice notes on WhatsApp, the counter app, the storefront — and the stock they see is now kept in step with yours automatically. They keep full control of prices, and a price they set themselves is never overwritten by a sync that does not carry one.
3. Authentication
Send your key as a bearer token on every request.
Every request
Authorization: Bearer swp_live_...A key looks like swp_<env>_<43 characters>, where env is live or test. The environment is part of the key so a test credential pasted into a production config is visibly wrong before it is used.
Scopes
Ask for the narrowest set each job needs. Your nightly stock push has no business reading your customers’ phone numbers.
| Scope | Grants |
|---|---|
stock:read | List stores, and read back the stock we hold. |
stock:write | Bind store codes, and push stock. |
orders:read | Pull the orders your storefront generated. |
orders:ack | Acknowledge an order into your own system. |
orders:write | Confirm, reject, start (preparing), ready or fulfil an order from your own system. |
webhooks:manage | Register, test, rotate and remove your order webhook. Registering a URL also needs orders:read. |
GET /partner/whoami needs no scope at all — it is how you find out what you hold.
What a key can reach
A key resolves to exactly one account, and that account is read from the key itself.
Nothing you send in a URL, query string or body can change which account you are acting as. A store code belonging to another business does not exist as far as your key is concerned — it returns 404, not someone else’s data.
Failures
| Status | Meaning |
|---|---|
401 | Missing, malformed, unknown or revoked key. |
403 | The key is valid but lacks a required scope, or the Stockwaka account is not active. |
A 403 never means “rotate your key”. Read the message — it names the scope you are short of.
4. Stores
Stockwaka calls a shop a branch. You call it a store, with your own code (SW-IKJ-01). Binding the two is a one-time setup step per store.
Lists every shop on the account, including ones you have not bound yet.
Response
[
{ "storeCode": "SW-MAIN", "branchId": "primary", "name": "Lekki", "isPrimary": true, "isActive": true },
{ "storeCode": "SW-IKJ-01", "branchId": "66f1a0c2e4b0a1d2c3e4f5b1", "name": "Ikeja", "isPrimary": false, "isActive": true },
{ "storeCode": null, "branchId": "66f1a0c2e4b0a1d2c3e4f5b2", "name": "Yaba", "isPrimary": false, "isActive": true }
]branchId is opaque. One shop on every account is the main shop, and its branchId is the literal string "primary" rather than an id. That is not a placeholder — it is a real, permanent handle you can use anywhere a branchId is accepted.
Binds your store code to one shop. Idempotent — safe to run on every deploy.
Request
# by the id from GET /partner/stores (recommended for scripts)
curl -X PUT "$BASE/partner/stores/SW-IKJ-01" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"branchId": "66f1a0c2e4b0a1d2c3e4f5b1"}'
# the main shop
curl -X PUT "$BASE/partner/stores/SW-MAIN" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"branchId": "primary"}'
# by name — convenient by hand; refused if it matches more than one shop
curl -X PUT "$BASE/partner/stores/SW-YAB-01" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"branchName": "Yaba"}'A code already bound to a different shop on the same account returns 409 and names the shop holding it. Re-bind that one first.
5. Stock sync
The main endpoint. An absolute per-store upsert.
Request
curl -X POST "$BASE/partner/stock/sync" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"storeCode": "SW-IKJ-01",
"items": [
{ "externalRef": "SKU-88213", "name": "Amlodipine 5mg 30s", "quantity": 42,
"sellingPrice": 2400, "costPrice": 1800, "barcode": "6154001234567",
"unit": "pack", "category": "Medicines" },
{ "externalRef": "SKU-88214", "quantity": 0 }
]
}'Response
{
"storeCode": "SW-IKJ-01",
"branch": "Ikeja",
"total": 2, "created": 0, "updated": 2, "skipped": 0,
"unpricedCount": 0,
"skippedSamples": [],
"warnings": [],
"photosQueued": 0,
"reserved": 3,
"sellable": 39,
"syncedAt": "2026-08-18T09:00:00.000Z"
}Item fields
| Field | Required | Notes |
|---|---|---|
externalRef | strongly recommended | Your SKU — the join key. See below. |
name | on first sight of a SKU | The product's name as your customers should see it. |
quantity | no (defaults to 0) | Absolute on-hand. Integer, never negative. Ignored for a made_to_order product. |
sellingPrice | no | Whole naira. Omit to leave the merchant's own price alone. |
costPrice | no | Whole naira. |
barcode | no | EAN/UPC, for scanning at the counter. |
unit | no | pack, bottle, carton… |
category | no | Browsing group shown to shoppers. |
stockMode | no | tracked (counted stock) or made_to_order (a dish). See Menus. |
soldOut | no | Made-to-order only. true takes the dish off sale; false puts it back. |
soldOutUntil | no | ISO 8601. Takes a dish off sale until then. Implies soldOut: true. |
modifierGroups | no | A dish's complete option groups, replacing what it had. |
description | no | Shopper-facing copy for the photo menu, kept to 280 characters. |
imageUrl | no | A public http(s) link to the product photo, fetched in the background. |
Any field we do not recognise is ignored, not rejected — adding a column to your export will never break the integration. A field you omit always means “leave what Stockwaka holds alone”; it never means “clear it”.
externalRef is the join key
Send your own stable SKU on every line. Stockwaka matches an incoming row against the catalogue in this order: externalRef, then barcode, then the product name.
Matching on name alone is fragile. The first time you correct a product’s description, a name-matched sync would create a second row and split that product’s stock across both. A SKU cannot do that.
You do not need to migrate an existing shop.
On the first sync we match by barcode or name and adopt your SKU onto the row we found. Every sync after that matches on the SKU and is immune to renames on either side. Once a SKU is known, you may push counts without repeating the name.
Once a SKU is known
{ "storeCode": "SW-IKJ-01", "items": [{ "externalRef": "SKU-88213", "quantity": 17 }] }Absolute, not delta
- Sending the same payload twice leaves the quantity unchanged. Retry freely.
- A lower number is applied as-is. It is a count, not an increment.
quantity: 0is how you say “none left” for counted stock. There is deliberately no delete: a zeroed product keeps its price, barcode and SKU, so restocking it later is one more sync rather than a re-setup. A made-to-order dish has no count — take it off sale withsoldOutinstead.- A negative quantity is rejected for the whole request, with nothing written, because it is evidence the payload is deltas — in which case every other row in it is wrong too.
Response fields worth alerting on
| Field | Meaning |
|---|---|
unpricedCount | Products now in the catalogue with no selling price. They cannot be sold online. If your export has a price column, map it — this is the field to alert on. |
skipped | Rows we could not use. Never fails the batch; skippedSamples carries up to five human-readable reasons for your logs. |
warnings | Up to five things we did not apply from rows we otherwise did — a soldOut sent for counted stock, a photo link that is not a link. |
photosQueued | Products whose photo is being fetched in the background. 0 once every photo is current. |
reserved | Units at this store currently held against open orders. Counted stock only. |
sellable | Units a shopper could order right now (on-hand minus reserved). Counted stock only. |
Partial success
Individual bad rows are skipped, counted and reported — they never fail the batch. A contract error rejects the whole request and writes nothing, because it means the payload as a whole cannot be trusted: no storeCode, items not a list, over the size cap, a negative or non-numeric quantity, a stockMode other than tracked or made_to_order, or a soldOut / soldOutUntil that cannot be read. The last two are contract errors on purpose: a mis-mapped availability column would otherwise take every dish on, or off, every menu at once.
7. Stock read-back
What Stockwaka currently holds, for reconciliation.
Request
curl "$BASE/partner/stock?storeCode=SW-IKJ-01&limit=200" \
-H "Authorization: Bearer $KEY"Response
{
"items": [
{
"externalRef": "SKU-88213",
"name": "Amlodipine 5mg 30s",
"storeCode": "SW-IKJ-01",
"branchId": "66f1a0c2e4b0a1d2c3e4f5b1",
"stockMode": "tracked",
"quantity": 42, "reserved": 3, "available": 39,
"soldOut": false, "soldOutUntil": null,
"sellingPrice": 2400, "costPrice": 1800,
"barcode": "6154001234567", "unit": "pack", "category": "Medicines",
"description": null, "imageUrl": null, "modifierGroups": [],
"updatedAt": "2026-08-18T09:00:00.000Z"
}
],
"nextCursor": "66f1a0c2e4b0a1d2c3e4f5c9"
}quantity,reservedandavailablearenullfor a made-to-order dish — it has no count, and a0would read as “none left”.soldOutUntilisnullwhile a dish is on sale, and also while it is off until further notice.modifierGroupscome back in the shape you send them, withminSelectresolved (1for a plain required group).imageUrlis Stockwaka’s stored copy of the photo, never the link you sent.
| Query | Default | Notes |
|---|---|---|
storeCode | all stores | Omit to walk the whole estate in one pass. |
limit | 100 | Max 200. Values above are clamped, not rejected. |
cursor | — | Pass the previous page’s nextCursor. |
Page until nextCursor is null. An item present here but absent from your system is usually one the merchant added themselves on WhatsApp — it will have externalRef: null.
8. Reservations, and why your count and ours can differ
This is the one place your count and ours legitimately differ, and the reserved and sellable fields exist to explain it.
When a shopper places an order, Stockwaka reserves the units for a short window — 15 minutes, extended to at most 24 hours once they say they have paid — so two shoppers cannot buy the same last pack. Nothing is deducted: the units are still on your shelf and still in your count.
your on-hand: 42 (what you sent)
reserved: 3 (held by open orders)
sellable: 39 (what a shopper can order right now)A reservation survives your sync. If you push quantity: 20 while three units are reserved, the result is 20 on hand with 3 still held and 17 sellable. Your figure is authoritative for on-hand; the reservation is ours.
Reservations release automatically when an order is confirmed, cancelled or expires. The only lasting effect on your side is the confirmed sale, which appears as an order in the pull below.
9. Orders
A change feed, ordered by when each order last changed. Poll it with the cursor you last received.
Request
# first ever pull
curl "$BASE/partner/orders?limit=100" -H "Authorization: Bearer $KEY"
# every pull after that
curl "$BASE/partner/orders?limit=100&cursor=$CURSOR" -H "Authorization: Bearer $KEY"Response
{
"orders": [
{
"reference": "7QM4P2",
"status": "preparing",
"storeCode": "PZ-IKJ",
"branchId": "66f1a0c2e4b0a1d2c3e4f5b1",
"customerName": "Mrs Adebayo",
"customerPhone": "2348012345678",
"customerNote": "12 Allen Avenue, opposite GTBank",
"fulfillmentType": "delivery",
"deliveryLocation": { "latitude": 6.6018, "longitude": 3.3515 },
"items": [
{ "externalRef": "PIZ-PEP-L", "name": "Pepperoni Pizza (Large)",
"quantity": 2, "unit": null, "unitPrice": 12000, "lineTotal": 28000,
"modifiers": [
{ "group": "Crust", "label": "Stuffed crust", "priceDelta": 1500, "externalRef": "MOD-CR-ST" },
{ "group": "Extra toppings", "label": "Mushroom", "priceDelta": 500, "externalRef": "MOD-TP-MU" }
] }
],
"totalAmount": 28000, "taxAmount": 0,
"deliveryFee": 1500, "deliveryFeeOnArrival": false, "payableAmount": 29500,
"placedAt": "2026-08-18T08:41:00.000Z",
"confirmedAt": "2026-08-18T08:43:00.000Z",
"preparingAt": "2026-08-18T08:44:00.000Z", "readyAt": null,
"fulfilledAt": null, "cancelledAt": null,
"createdAt": "2026-08-18T08:39:00.000Z",
"updatedAt": "2026-08-18T08:44:00.000Z",
"ackedAt": null, "externalOrderId": null
}
],
"nextCursor": "2026-08-18T08:44:00.000Z|66f1a0c2e4b0a1d2c3e4f5d3"
}| Query | Default | Notes |
|---|---|---|
cursor | — | Opaque. Always prefer this. Wins over since if both are sent. |
since | — | ISO 8601. For a first pull only. |
storeCode | all stores | |
limit | 50 | Max 200. |
Use the cursor, not `since`.
The cursor is an exact position in a total order; a timestamp is not, and two orders touched in the same millisecond can be missed by timestamp paging. An empty orders array means you are caught up — keep the cursor and reuse it.
Because this is a change feed, an order you have already seen reappears when its status changes. That is the point: you never miss a confirmation or a cancellation.
Order statuses
| Status | Meaning |
|---|---|
pending_payment | Placed. Stock reserved, waiting for the shopper to pay. Do not fulfil. |
payment_claimed | The shopper says they have paid; the merchant is verifying. |
confirmed | Paid. Take it in. |
preparing / ready | Paid and still open — the optional kitchen states. Take it in if this is the first time you see it. |
fulfilled | Handed over or delivered. Terminal — but paid, so take it in if you have never seen it. |
cancelled / expired | Dead. The stock reservation was released. |
Draft carts are never published. Take in any paid status you have not seen — not only confirmed. A kitchen’s till moves an order to preparing or ready within a minute of payment, so a feed polled every few minutes often meets it there first.
Order fields
fulfillmentTypeispickup,deliveryordine_in. For a delivery the address is incustomerNote; for dine-in, the table.deliveryLocationis the exact point a delivery goes when the shopper shared a location pin, as namedlatitudeandlongitude.nullfor pickup, dine-in and typed addresses.unitPriceis the base price per unit, before options.lineTotalis(unitPrice + Σ modifiers[].priceDelta) × quantity— options are priced per unit.modifiers[].externalRefis the option code you sent on a sync, frozen when the shopper chose it;nullfor an option the merchant set up on WhatsApp.totalAmountis the goods — what to take in as the sale.deliveryFeesits beside it, never inside it;deliveryFeeOnArrivalistruewhen the rider collects that fee.payableAmountis what the shopper actually paid at checkout. Every money figure is in the shop’s own currency, in whole units.preparingAtandreadyAtstamp the kitchen states, for ticket times.items[].noteis the shopper’s instruction for that line — “no onions” — andkitchenNotetheir note for the whole order. Both arenullwhen empty; print them on the ticket. A fixed choice (“Pepper: Normal / Extra”) belongs inmodifierGroupswithpriceDelta: 0, which carries your option code.- Line items carry
externalRefwhere we know it. It isnullfor a product the merchant added themselves on WhatsApp — we will not invent a SKU your system has never issued.
An order you have already taken in can come back with more than a new status: when a payment confirms automatically, the shopper may choose pickup or delivery a moment later. Update the order you hold rather than skipping it.
Records that your system has taken the order in. Idempotent.
Request
curl -X POST "$BASE/partner/orders/7QM4P2/ack" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"externalOrderId": "ERP-2026-00184"}'externalOrderId is optional and stored so support can trace an order across both systems. A repeat ack returns alreadyAcked: true and changes nothing — including ackedAt, which stays the moment you first took the order. Acking does not advance an order’s updatedAt, so an acknowledged order will not come back on the next pull just because you acknowledged it.
An ack is not a fulfilment. It means “we have this”. Use the four actions below to move the order itself.
Order actions
Five things your system can do to an order. Each returns 200 and each is idempotent — a repeat returns alreadyDone: true and changes nothing, so a retrying job is always safe.
| Action | What it does |
|---|---|
confirm | The money landed. Records the sale, deducts stock, releases the reservation, tells the shopper. |
reject | You cannot fill it. Releases the reservation and tells the shopper. Optional {"reason": "…"}. |
preparing | The kitchen has started it. Optional — for a kitchen display that bumps tickets. |
ready | Cooked, or picked, and waiting. This is the one the shopper is messaged about. |
fulfil | Handed over. Terminal. |
Request
curl -X POST "$BASE/partner/orders/7QM4P2/confirm" \
-H "Authorization: Bearer $KEY"Response
{
"reference": "7QM4P2",
"status": "confirmed",
"alreadyDone": false,
"updatedAt": "2026-08-18T09:05:11.000Z"
}status is read back after the action, so you can store the row you were given rather than inferring what the action must have produced.
orders:write is a separate scope from orders:ack on purpose.
orders:read + orders:ack and nothing more.- Confirm is the same rung as a merchant confirming by hand, so your system and the shop owner can race safely — whichever lands second reports
alreadyDonerather than deducting twice. - Reject only works before payment. Once an order is
confirmedthe money has been taken and the stock deducted, so unwinding it is a refund — a decision this API will not take on a merchant’s behalf. It returns409. - Preparing and ready need a paid order. Both are reachable straight from
confirmed, so you never have to post a state you do not model — a kitchen that only knows “ready” never postspreparing. An order already past the state you post returns409. - None of these writes a stock figure. Confirming moves units the shopper had already reserved into a sale; your next absolute sync still overwrites whatever we hold.
You do not have to use them. A merchant can drive every one of these from WhatsApp or the till, and a small shop does. These exist because nobody types CONFIRM 7QM4P2 a hundred times a day, and your back office already knows when it took the money.
10. Webhooks
Instead of polling every 30–60 seconds, register one HTTPS endpoint and every order change is posted to it within seconds — the same order object GET /partner/orders returns.
Request
curl -X PUT "$BASE/partner/webhook" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://erp.example.com/hooks/orders"}'The response carries a secret (whsec_…) the first time only — store it like your API key. One webhook per account; PUT again to move it (the secret is kept) or to switch a disabled one back on. Registering needs orders:read too, because an endpoint receives order data.
What arrives
A POST signed to the open Standard Webhooks specification, so you can verify it with a published library.
| Header | Value |
|---|---|
webhook-id | The same for a retry of the same order state — de-duplicate on it. |
webhook-timestamp | Unix seconds when this attempt was sent. |
webhook-signature | v1,<base64 HMAC-SHA256 of "id.timestamp.body">. Two, space-separated, during a secret rotation. |
Body
{
"id": "msg_7QM4P2_1755507911000",
"type": "order.updated",
"createdAt": "2026-08-18T09:05:11.000Z",
"data": { "order": { "reference": "7QM4P2", "status": "confirmed", "…": "…" } }
}- Answer any 2xx within 10 seconds, then do the work. Redirects are not followed.
- The order is sent as it stands when the delivery goes out, so a burst of changes arrives as one delivery of the latest state.
- Delivery is at least once. Verify the signature over the raw body, and reject a timestamp more than five minutes old.
Retries, rotation and removal
A failed delivery is retried after 30s, 2m, 10m, 30m, 1h, 3h, 6h, 12h, 24h — about two days. An endpoint that answers 410 Gone, or where 20 deliveries in a row fail every retry, is switched off; GET /partner/webhook shows why, and PUT switches it back on.
| Call | Does |
|---|---|
GET /partner/webhook | The webhook and its health. |
POST /partner/webhook/rotate-secret | A new secret. The old one keeps signing alongside it for 24 hours. |
POST /partner/webhook/test | Sends a webhook.test event now and returns what your endpoint answered. |
DELETE /partner/webhook | Removes it. |
A webhook is the fast path, not the record.
Keep polling GET /partner/orders with your cursor every 10–15 minutes. Anything a webhook could not deliver — your endpoint was down, the webhook was switched off — is always there.
11. Errors and limits
Standard HTTP status codes, with a message written to be read by a human.
Response
{
"statusCode": 400,
"message": "\"items\" carried 1500 products, which is over the limit of 1000 per request. Split the store's catalogue into pages of 1000 or fewer…",
"error": "Bad Request"
}| Status | When |
|---|---|
400 | The payload breaks a contract rule — no storeCode, over the size cap, a negative quantity, an unreadable stockMode, soldOut or soldOutUntil. Nothing was written. |
401 | Missing, malformed, unknown or revoked key. |
403 | Valid key, missing scope; or the Stockwaka account is not active. |
404 | An unrecognised store code, or an order reference not on this account. |
409 | A store code already bound to another shop, or an ambiguous branch name. |
An unrecognised storeCode is a 404 and never falls back to the main shop.
If it did, renaming a store in your system would silently redirect that store’s whole catalogue onto another building’s shelf — and nothing would look wrong until a customer was sold something that is not there.
Retrying
Everything here is safe to retry: the sync sets absolute figures, the ack is idempotent, and reads have no side effects. Retry 5xx and network failures with exponential backoff; do not retry a 4xx without changing the request.
Limits
| Limit | Value |
|---|---|
Items per stock/sync call | 1,000 |
limit on GET /partner/stock | 100 default, 200 max |
limit on GET /partner/orders | 50 default, 200 max |
| Option groups per product | 10 |
| Options per group | 30 |
| Option-group name or option label | 80 characters |
imageUrl length | 2,048 characters |
description | kept to 280 characters |
| Store code length | 64 characters |
A large chain’s catalogue must be paged. Pages are independent — each is applied on its own, order does not matter, and a failed page can be re-sent alone. Send pages sequentially per store: two concurrent syncs of the same store would both be applied, and the last to land wins.
12. Recommended integration shape
Setup, once
GET /partner/whoami # confirm the key and the account
GET /partner/stores # pull the shop list
PUT /partner/stores/:code # bind your codes — safe to re-run on every deployStock or menu, on your own schedule
A full push nightly, plus a smaller push every 15–30 minutes for products that moved, is a good default. Both are the same call; there is no separate “incremental” mode, because an absolute figure needs none. A kitchen should also send a one-row sync the moment a dish is taken off or put back on — waiting for the next scheduled push leaves a window in which that dish can still be ordered.
for each store:
for each page of ≤1000 items:
POST /partner/stock/sync
alert if unpricedCount > 0 or skipped > 0
log warningsOrders, every 30–60 seconds for a kitchen; every 1–5 minutes otherwise
PAID = {"confirmed", "preparing", "ready", "fulfilled"}
cursor = load_saved_cursor() # null on first ever run
loop:
page = GET /partner/orders?cursor={cursor}&limit=100
for order in page.orders:
if order.ackedAt is null and order.status in PAID:
create_in_erp(order)
POST /partner/orders/{order.reference}/ack
elif order.ackedAt is not null:
update_in_erp(order) # status, kitchen times, fulfilment details
save_cursor(page.nextCursor) # persist BEFORE the next request
if page.orders is empty: sleep and continuePersist the cursor durably. Losing it is recoverable — you can replay from the start and your ackedAt check will skip everything you already have — but it is avoidable.
Reconciliation, daily
GET /partner/stock per store, compared against your own count. Expect differences of exactly reserved units; anything else is worth investigating.
13. Support
Questions, a key rotation, or a new store code: email support@bitasei.com or call +234 907 888 4939.
Never send us your key.
Include your keyPrefix — the visible leading characters, which GET /partner/whoami returns — and the storeCode in question. That is enough for us to find the credential. If a key has been exposed, say so and we will revoke and reissue it the same day.