BlueDuka API for developers
Connect an online shop, accounting package, ERP, delivery app or dashboard to a BlueDuka shop. Everything here works over HTTPS with JSON.
-
Base URL:
https://api.blueduka.co.ke/api/v1/ -
Machine-readable summary (scopes, events, formats; no data):
GET /api/v1/developer/ - Available on the shop's Premium plan.
1. Getting a key
Only the shop owner can create keys: BlueDuka app → Integrations → API keys → New key. They choose:
| Option | What it does |
|---|---|
| Name | e.g. "Website orders", shown in the key list and logs |
| Scopes | what the key may do (below). Default: read only |
| Allowed addresses | optional IPs or ranges (e.g. 41.90.12.7, 41.90.0.0/16); any other address is refused |
| Expiry | optional, 1 to 730 days |
| Requests per minute | 10 to 600 (default 120) |
The key (bdk_...) is shown once. BlueDuka stores only a SHA-256 hash of it, so a lost key can't be recovered: revoke it and make a new one. A key belongs to one shop and only ever sees that shop.
Send it on every request:
Authorization: Api-Key bdk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Never put a key in a browser, mobile app or public repository: call BlueDuka from your server.
2. Scopes
| Scope | Allows |
|---|---|
read | GET products (and their batches: /products/{id}/batches/), categories, stock movements, low stock, expiring stock (/stock/expiring/), sales, payments, returns, customers, debts, invoices, quotations, expenses, reports, suppliers, purchase orders, deliveries, supplier bills/payments/returns, purchases summary, incoming M-Pesa payments, online-shop orders (/online-orders/) |
sales:write | POST /sales/ (cash, M-Pesa, credit sales) and POST /sales/{id}/returns/ |
customers:write | POST /customers/, PATCH/PUT /customers/{id}/, POST /customers/{id}/payments/ (debt repayments) |
products:write | POST /products/, POST /categories/, PATCH/PUT /products/{id}/ |
stock:write | POST /stock/receive/, /stock/adjust/, /stock/count/ |
Whatever its scopes, a key can never reach staff, permissions, shop settings, integrations, M-Pesa or other integration settings, other API keys, webhooks or billing. Everything a key changes goes through the same rules as the app: plan limits, stock checks, credit limits, manager approvals, and the shop's audit log.
3. Quick start
# list products
curl -H "Authorization: Api-Key $BLUEDUKA_KEY" https://api.blueduka.co.ke/api/v1/products/
# record a sale paid by M-Pesa (needs sales:write)
curl -X POST https://api.blueduka.co.ke/api/v1/sales/ \
-H "Authorization: Api-Key $BLUEDUKA_KEY" -H "Content-Type: application/json" \
-d '{"client_ref": "8f6a2c1e-3b1d-4f5e-9a77-1c2d3e4f5a6b",
"items": [{"product_id": "<product uuid>", "quantity": "2"}],
"payments": [{"method": "mpesa", "amount_minor": 46400, "reference": "TJK1ABC2DE"}]}'
import uuid, requests
s = requests.Session()
s.headers["Authorization"] = f"Api-Key {KEY}"
sale = s.post("https://api.blueduka.co.ke/api/v1/sales/", json={
"client_ref": str(uuid.uuid4()), # keep it and reuse it if you retry
"items": [{"product_id": product_id, "quantity": "1"}],
"payments": [{"method": "cash", "amount_minor": 23200}],
}, timeout=30).json()
print(sale["number"], sale["total_minor"], sale["etims"])
4. Formats and rules
-
Money is an integer in cents: KES 250.00 is
25000. Prices always come from BlueDuka: you send what was sold, the server works out the totals. -
Quantities are decimal strings, up to 3 decimals:
"1","0.5","2.125". -
Expiry dates: for products with
track_expiry: true,/stock/receive/needsexpiry_date(2027-03-31) and takes an optionalbatch_number. Sales take the batch that expires first by themselves. - IDs are UUIDs. Times are ISO 8601 with time zone.
-
Lists are paged:
{"count", "next", "previous", "results"}, 50 per page; follownext. -
Idempotency: send a
client_ref(a UUID you generate) with sales, returns, payments and stock changes, and your ownid(UUID) when creating a product or customer. Sending the sameclient_ref(orid) again returns the record already made instead of a second one, so network retries are safe. - M-Pesa codes can be used once per shop. If the owner turned on "only accept codes the till has received", a code must have reached the shop's till first.
- Sales are immutable: correct them with a return, never by editing.
5. Errors
| Status | Meaning |
|---|---|
| 400 | Invalid input. The body says which field and why, e.g. {"detail": "Only 3 left in stock"} |
| 401 | Missing, wrong, revoked or expired key, or the owner who made it was removed |
| 402 | The shop's plan doesn't include API access (or the feature asked for) |
| 403 | The key's scopes or allowed addresses don't permit this request |
| 404 | Not found in this shop |
| 202 | A manager must approve (e.g. a large discount). Body: approval_request_id; send it back as approval_id once approved |
| 429 | Too many requests. Wait for the number of seconds in Retry-After |
Every change made with a key, and every refused request, is logged (time, method, path, result, address). The owner sees this per key and can revoke a key at any time.
6. Webhooks: BlueDuka tells you when something happens
The owner adds a webhook in Integrations → Webhooks: an https:// address of yours and the events to send. Events: sale.created, sale.returned, debt.repayment, expense.recorded, goods.received, supplier.paid, product.created, stock.transferred (or * for all).
Each delivery is a POST with JSON {"id", "event", "created_at", "business_id", "data": {...}} and headers:
| Header | |
|---|---|
X-BlueDuka-Event | the event name |
X-BlueDuka-Delivery | the event id: the same on every retry, use it to ignore repeats |
X-BlueDuka-Timestamp | Unix seconds when signed |
X-BlueDuka-Signature | sha256= + HMAC-SHA256 (hex) of "<timestamp>.<raw body>" with your webhook secret |
The secret is shown once when the webhook is created. Check every delivery before trusting it:
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, timestamp: str, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # refuse old or replayed deliveries
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
const crypto = require("crypto");
function verify(secret, rawBody, timestamp, signature) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Answer with any 2xx within 10 seconds (do the work after answering). A failed delivery is retried after 1, 5, 30, 120, 360 and 720 minutes with the same X-BlueDuka-Delivery. After 20 failures in a row the webhook switches itself off and the owner sees it. BlueDuka only delivers to public internet addresses, over HTTPS, and never sends secrets in events.
7. Tax receipts (only if the shop connected them)
Some shops connect KRA eTIMS in the app; most don't, and then the sale's etims field is always null. When a shop has connected it, every sale you record through the API is sent to KRA automatically (returns become credit notes); you don't call KRA yourself. The etims field stays null until KRA accepts the sale, then holds the control unit invoice number, signature and the KRA verification link (qr_url) to print on your own receipt. Sales still waiting or refused are handled by the owner in the app.
8. Security summary
- HTTPS only. Keys are hashed (SHA-256), shown once, scoped, revocable, optionally address-locked and expiring.
- Each key has its own rate limit; app users' limits are separate.
- A key acts as the shop owner but only inside its scopes, and only while that owner is still active and the shop's plan includes API access.
- Webhooks are signed (HMAC-SHA256 with a timestamp), retried with the same id, HTTPS and public addresses only.
- Owner-only setup; every key change and every write is in the shop's audit log.
Report a security problem: see SECURITY.md.