BlueDuka

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:

OptionWhat it does
Namee.g. "Website orders", shown in the key list and logs
Scopeswhat the key may do (below). Default: read only
Allowed addressesoptional IPs or ranges (e.g. 41.90.12.7, 41.90.0.0/16); any other address is refused
Expiryoptional, 1 to 730 days
Requests per minute10 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

ScopeAllows
readGET 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:writePOST /sales/ (cash, M-Pesa, credit sales) and POST /sales/{id}/returns/
customers:writePOST /customers/, PATCH/PUT /customers/{id}/, POST /customers/{id}/payments/ (debt repayments)
products:writePOST /products/, POST /categories/, PATCH/PUT /products/{id}/
stock:writePOST /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/ needs expiry_date (2027-03-31) and takes an optional batch_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; follow next.
  • Idempotency: send a client_ref (a UUID you generate) with sales, returns, payments and stock changes, and your own id (UUID) when creating a product or customer. Sending the same client_ref (or id) 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

StatusMeaning
400Invalid input. The body says which field and why, e.g. {"detail": "Only 3 left in stock"}
401Missing, wrong, revoked or expired key, or the owner who made it was removed
402The shop's plan doesn't include API access (or the feature asked for)
403The key's scopes or allowed addresses don't permit this request
404Not found in this shop
202A manager must approve (e.g. a large discount). Body: approval_request_id; send it back as approval_id once approved
429Too 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-Eventthe event name
X-BlueDuka-Deliverythe event id: the same on every retry, use it to ignore repeats
X-BlueDuka-TimestampUnix seconds when signed
X-BlueDuka-Signaturesha256= + 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.

Open BlueDuka