1. The one rule
Stockwaka is the master. Your website is a mirror.
Names, prices, offers, descriptions, photos and stock come from the shop's Stockwaka account. A change made there reaches your website on its next sync. A change made in your website's own admin to any of these is overwritten.
This is the reverse of the Partner Sync API, where a chain's ERP is the master and Stockwaka mirrors it. Here the shop keeps its website, its domain and its search ranking, and the website stops being a second place to keep stock.
2. Getting a key
- 1The shop owner opens Selling online → Website in their Stockwaka panel and chooses My own website. Their Stockwaka website switches off — a shop sells through one website, not two.
- 2They choose how shoppers pay: on WhatsApp or on your website's checkout.
- 3They press Create key and give you the key. It is shown to them once.
The key carries exactly these scopes:
| Scope | Grants |
|---|---|
catalog:read | Read your catalogue for your own website: prices, offers, descriptions, photos and stock left to sell online. |
checkout:write | Check out from your own website: send a basket to WhatsApp, or place, pay and cancel orders from your own checkout. |
3. Authentication and limits
Every request
Authorization: Bearer swp_live_…| Rule | What happens |
|---|---|
| Call from your server only. | A request carrying a browser Origin or Sec-Fetch-Site header is refused (403), even with a valid key. A key in page JavaScript can be read by every visitor. |
| Only the current key works. | Replacing the key, or the shop switching back to its Stockwaka website, cancels the old one at once (401). |
| Rate limits per key. | 300 reads and 60 writes a minute; past that, 429 with Retry-After. Sync on a schedule and cache — never call the API on every page view. |
| An active account that sells online. | 403, or 402 when the subscription has lapsed. |
Every error is JSON with a stable machine code. Branch on the code, not the message:
Error
{ "statusCode": 409, "code": "price_changed", "message": "…", "field": "expectedTotal", "details": { } }4. The shop
The business name, its currency, the checkout mode (whatsapp or own), whether it is taking orders now, and the shops your website sells from — each with a key, its opening status and how it hands orders over. Most businesses have one shop; for a chain, pass ?shop=<key> on catalogue calls, because each shop has its own prices and stock.
5. The catalogue
A change feed. Call it with no cursor for a full sync and keep paging while hasMore is true; then store nextCursor and pass it back next time to get only what changed. limit defaults to 100, at most 250. An empty page is normal and cheap — poll every one to five minutes.
The clock is a change too.
An offer starting or ending, stock reaching its expiry date, a finished dish coming back — none of these is an edit, and all of them put the product back in the feed at the moment they happen. You need no timers of your own.
| Field | Meaning |
|---|---|
id | Stable per shop. Store it against your own product and update that same page, so its address and ranking are kept. |
key | The catalogue key — the same at every shop of the business. |
name · description · category · size · unit · imageUrls | Show these. |
sku · barcode | Use these to link the products you already have on the first sync. |
status | active — show it; sold_out — show it as out of stock and keep the page; hidden — it has no price; do not show it. |
statusReason | out_of_stock, expired, finished, unpriced. |
listings[] | Each way it is sold — its own unit and any packs — with its own id, price, wasPrice, discountPercent, offerEndsAt, available and purchasable. |
hasOptions | It has choices (sides, toppings) and cannot be ordered through the API — link it to the shop’s WhatsApp. |
available is what is left to sell online: stock on hand, minus open orders, minus the units the shop keeps back for walk-in customers. It is null for something made to order.
Every product id the shop holds. An id you have that is not in this list was deleted — unpublish it (a draft keeps the page's history). Run it once a day.
One product as it is now, or 404 if it was deleted.
6. Checkout on WhatsApp
Request
{ "shop": "main", "lines": [ { "listingId": "64b7…", "quantity": 2 } ] }For a shop whose checkout is whatsapp. Every line is re-read at today's price and capped at what is left. Send the shopper to the returned whatsappUrl, where the order is finished and paid in the shop's chat — after showing them adjusted, the lines that sold out or were reduced since your last sync. At most 50 lines of up to 99 each.
7. Checkout on your website
For a shop whose checkout is own. Your checkout takes the money straight into the shop's own account; Stockwaka holds the stock and records the sale.
Request
{
"externalOrderId": "WC-10293",
"shop": "main",
"lines": [ { "listingId": "64b7…", "quantity": 3 } ],
"customer": { "name": "Ada Obi", "phone": "+2348031234567" },
"fulfilment": "delivery",
"deliveryAddress": "12 Allen Avenue, Ikeja",
"note": "Call at the gate",
"offersConsent": false,
"expectedTotal": 3000
}- 1When the shopper starts paying, send the order. The stock is held for 15 minutes and the answer carries our
reference. If your checkout already took the money, add"paid": trueand skip step 2. - 2When payment succeeds:
POST /storefront/orders/:reference/paid. The sale is recorded and the stock deducted, once. - 3If they abandon payment:
POST /storefront/orders/:reference/cancel. The stock goes back on sale. - 4
GET /storefront/orders/:referencesays where it is:reserved,paid,preparing,ready,fulfilled,cancelledorexpired.
| Answer | What to do |
|---|---|
| The same externalOrderId again | You get the same order back — never a second hold. Retry any request that timed out. |
409 price_changed | A price moved since your last sync. Nothing is held; show the new total from details and send again. |
409 insufficient_stock · unavailable | Someone bought the last ones, or an item is off sale. Nothing is held. |
409 order_closed | On /paid: the 15-minute hold lapsed and the stock went back on sale. Refund, or place a new order. |
409 already_paid | On /cancel: a paid order is a sale. A refund is the shop’s to make. |
409 shop_closed | The shop is outside its opening hours. |
offersConsent is the shopper's own tick.
Add an unticked box to your checkout — “Send me this shop's offers on WhatsApp” — and send what the shopper chose. Only true puts them on the shop's offer messages. Nothing else about the order is ever sent to them on WhatsApp: your website tells its own customer.
8. Webhook — instant updates
Register one HTTPS URL. The signing secret (whsec_…) is in the response when it is first created — and never again. Private network addresses are refused. GET reads its status, POST …/test sends a test now, POST …/rotate-secret issues a new secret (the old one keeps signing for 24 hours), and DELETE removes it.
| Event | Meaning |
|---|---|
catalog.changed | Something you mirror changed — a sale, a restock, a price, an offer starting or ending. No products are carried: pull the feed with your saved cursor. A burst is one event. |
order.updated | One of your website’s orders changed — paid, being prepared, ready, handed over, cancelled, expired. Carries the order. Tell your customer: Stockwaka never messages them. |
Deliveries are signed to the open Standard Webhooks format (webhook-id, webhook-timestamp, webhook-signature) — verify with any Standard Webhooks library and de-duplicate on webhook-id. Answer 2xx within 10 seconds; failures are retried (30s, 2m, 10m, 30m, 1h, 3h, 6h, 12h, 24h), then the webhook switches off until you register it again. 410 Gone switches it off at once.
A webhook is a hint, not the record.
Keep a slow poll of the feed — every 15 to 30 minutes — as your safety net. The feed cannot miss a change; a webhook that was down can.
9. A sync loop that works
Pseudocode
every 2 minutes:
page = GET /storefront/products?cursor=<saved>
for each product: upsert your product by product.id
hidden -> unpublish
sold_out -> show "out of stock"
active -> show, with listings[].price and available
save page.nextCursor; repeat while page.hasMore
once a day:
ids = GET /storefront/products/ids
unpublish every product of yours whose id is not in ids- Keep the key in a server environment variable. Never in browser code.
- Cache what you sync; serve your pages from your own database.
- Send
expectedTotalwith every order.