Developers

Connect a shop's website to Owwel

Read the catalogue, stock levels and selling prices. Send orders back for the shop to accept, and they become sales the moment the owner says so. REST over HTTPS, with a key the business owner issues themselves.

Where to send requests

https://owwel.com/api/v1

Signing in

Every request carries the key as a bearer token. There are no sessions, no cookies and no login step.

curl https://owwel.com/api/v1/branch \
  -H "Authorization: Bearer owl_sk_live_…"

Keys look like owl_sk_live_…. One key covers one branch, so a business with several shops issues one per shop. The owner creates it in Settings, and it is shown to them once and never again — if it is lost, they revoke it and generate another.

Keep the key on your server

Every key a shop owner issues is a server key, owl_sk_live_. Requests carrying an Originheader are refused outright, because that header means a browser sent the request — and anything a browser can send, a visitor can read. Call from your backend and pass the results down to the page.

A browser-safe key, locked to named domains and limited to the catalogue, exists in the data model but is not issued today. If a shop needs one, ask.

Endpoints

GET/v1/branchcatalogue:readLive
The shop’s name, currency, tax label and timezone. Call this first and format money from what it returns — branches trade in different currencies, and hard-coding one is the commonest integration bug.
GET/v1/productscatalogue:readLive
The catalogue. quantityis what is still sellable — on the shelf, less whatever pending orders have already claimed — so showing it directly cannot oversell. Cursor paginated: limit up to 100, then follow next_cursor. Filters: updated_since, in_stock, search.
GET/v1/products/:idcatalogue:readLive
One product. A product belonging to another branch answers 404, not 403 — a 403 would confirm it exists.
POST/v1/ordersorders:writeLive
Send an order. Server key only, and an Idempotency-Key header is required. See below.
GET/v1/ordersorders:readNot built yet
Past orders, for showing a returning customer their history. Filters: from, to, updated_since.
GET/v1/orders/:idorders:readNot built yet
One order with its lines.

Sending an order

An order is a request to sell, not a sale. Owwel prices it from the shop’s own catalogue and tells the shop owner. Nothing in their books changes: the quantity this API reports simply drops, so you cannot sell the same item twice, while the shop’s own stock count stays as it is for whoever is standing at the counter. It becomes a real sale, and the stock actually moves, only when the owner turns it into an invoice.

That matters for what you build: a successful response means received, not fulfilled. Take payment on your own side and handle refunds there, because Owwel records no money at this point and cannot refund anything.

curl -X POST https://owwel.com/api/v1/orders \
  -H "Authorization: Bearer owl_sk_live_…" \
  -H "Idempotency-Key: checkout_8842" \
  -H "Content-Type: application/json" \
  -d '{
    "customer":  { "name": "Amina Yusuf", "phone": "+254712345678" },
    "reference": "WEB-8842",
    "lines": [
      { "product_id": "50af4693-…", "quantity": 2 }
    ]
  }'

Four things that will catch you out

  • Do not send prices.Send a product id and a quantity. Owwel prices the line from the shop’s own catalogue and returns the totals. A price in the body is ignored, not honoured.
  • Do not send money. Owwel records no payment for a web order. What the customer paid, and any refund, stays entirely on your side.
  • The idempotency key is required, not optional. Reuse the same one on a retry and you get the original order back. Without it, a dropped connection becomes two orders and the stock is taken twice.
  • Accepted is not fulfilled.A 201 means the shop has the order and the items are held for it. The owner still has to approve, and may cancel — in which case the quantity becomes available again. Do not tell a customer their order is confirmed on the strength of this response alone.

When something goes wrong

Every failure has the same shape, and every response carries X-Request-Id. Quote it when asking for help — it is the difference between a diagnosis and a guess.

{
  "error": {
    "code": "scope_required",
    "message": "This key needs the orders:write permission.",
    "request_id": "0f0c8a2e-…"
  }
}
400invalid_requestA parameter is malformed. The message names it.
401invalid_keyMissing, unknown, revoked or expired key.
403scope_requiredValid key, wrong permission.
403origin_not_allowedWrong kind of key for where it was sent from.
404not_foundAlso returned for another branch's record, deliberately.
409idempotency_conflictSame idempotency key, different body.
422product_unavailableA line names a product this branch does not sell.
429rate_limitedWait the number of seconds in Retry-After.
500server_errorOur fault. Quote the request id.

How often you can call

Per key, per minute: 120 for a server key, 600 for a web page key, 20 for a test key. Over the limit answers 429 with Retry-After in seconds. Cache the catalogue for a minute rather than fetching it on every page view.

What this API will never return

Cost prices, profit, margin, expenses, purchases, suppliers, staff, payroll, bank and channel balances, customer phone numbers or emails on read, and anything belonging to another branch. These are not withheld by the endpoints — the database functions behind them do not select those columns at all, so no change to a route can start returning them.