Build on GeniYanga.
GeniYanga exposes a merchant-scoped REST API so your own software — a webshop, an ERP, a booking site, a loyalty app — can read sales, manage the catalogue, move stock, and ring up sales through the same engine that powers the till. Every write is MRA-aware and fully audited, exactly as if it happened at the counter.
What you can build
The API is organised around the same entities you already know from the till. Each is gated by its own permission, so a merchant grants you exactly what your integration needs — no more.
Sales
Create sales (online orders, external checkouts), list and read them, pull receipts, run a reserve-then-confirm flow to hold stock while you charge on your own gateway, and collect lay-bye installments over time.
Products & catalogue
Read and write products, variants, prices, categories, and tags — keep your storefront in lockstep with the shop's catalogue.
Inventory
Read live stock positions and post adjustments, recounts, and store-to-store transfers, all under the same stock invariants as the till.
Customers
Full CRUD over the customer book, including tags and wallet balances — sync your CRM both ways.
Quotes
Read draft quotes a shop has raised, and (with sales access) convert them into real, stock-booking sales.
Incremental sync
A cursor-paginated change feed streams every create, update, and delete since your last cursor — no full re-fetch to stay current.
Getting access
Access is always granted by the merchant and scoped to their business (optionally to a single store). There's no separate developer signup — the shop either issues you a token or brings you on as a co-owner.
- 1
Ask your store's admin for access
Ask your store's admin (the owner) to generate an API token for you from Settings → API tokens, or to add you as a co-owner so you can generate your own. Either way you end up with a merchant-scoped token, granted exactly the permissions your integration needs — no more.
- 2
Claim your token secret — once
Once you receive your token, its full secret is shown exactly one time. Copy it straight into a secrets manager (never source control). If you lose it, the owner simply rotates the token and you claim the new secret.
- 3
Call the API
Send the token as a bearer credential against
https://phindu.dartsmw.com/api/ext/v1. Start withGET /meto confirm your scope, then build from there.
Authenticating requests
Tokens look like phk_<tokenId>_<secret> and travel in the Authorization header with a Bearerprefix. Every call is automatically scoped to the token's merchant — you never pass a merchant id, and any you do pass is ignored.
curl https://phindu.dartsmw.com/api/ext/v1/me \
-H "Authorization: Bearer phk_9f2ka81mq04z_5c0e…e91b"The response tells you who you are and what you may do:
{
"merchant": "m_8h2…",
"store": "MAIN",
"name": "Acme Webshop integration",
"permissions": {
"products": ["read"],
"inventory": ["read"],
"sales": ["create", "read"]
},
"expiresAt": "2026-12-31T23:59:59Z",
"rateLimit": { "rpm": 120, "burst": 20 }
}What to know before you build
Everything is merchant-scoped
A token belongs to one merchant. If it is also pinned to a store, sales and inventory writes are confined to that store and lists are filtered to it automatically.
Stores are addressed by reference
Every store, fromStore, and toStorefield — in and out — uses the store's short reference (e.g. MAIN), never the internal record id. All other relations (customer, variant, category) use record ids.
Permissions are per-entity CRUD
Grants map create / read / update / delete onto each entity. Some verbs are deliberately never grantable — recording a payment is sales:update, cancelling a sale is sales:delete, and the most dangerous money-moving edits stay in the portal.
Money is never your input
Subtotals, tax, and totals are always computed server-side from the catalogue and the merchant's tax profile. You send items and quantities; the till maths and the MRA-compliant receipt are done for you.
Retry safely with idempotency keys
Money-moving POSTs accept an Idempotency-Key header. Replaying the same key within 24 hours returns the original response — so a dropped connection never double-charges or double-books.
Rate limits & pagination
Roughly 120 requests/minute per token (burst 20); over the limit you get a 429 with a Retry-After. Lists use ?page=/?perPage= (max 100) and accept ?updatedSince=<ISO> for incremental pulls.
The endpoints at a glance
All paths are relative to https://phindu.dartsmw.com/api/ext/v1. Reference data (/me, /stores, /categories) is readable by any active token; everything else needs the matching grant.
| GET | /me · /stores · /categories | Introspect your token and read reference data. |
| GET | /products · /inventory | Read the catalogue and live stock positions. |
| POST | /products · /inventory/adjustments | Create products; adjust, recount, and transfer stock. |
| GET | /customers | List and read customers; POST/PATCH to sync them. |
| POST | /sales | Ring up a sale — stock, pricing, receipt, and audit. |
| POST | /sale-reservations | Hold a cart, charge out-of-band, then confirm. |
| POST | /sales/{id}/payments | Record a lay-bye installment against an open sale. |
| GET | /changes | Incremental sync feed across all readable entities. |
The live contract — every field, filter, and error code — is served straight from the backend:
From token to a sale, step by step
Here's the whole journey — from a fresh token to a receipted sale — as real requests you can paste into a terminal. The first three steps are shared; then the flow forks into First Sale and Repay Lay-Bye. Run them in order — each call hands you the id or reference the next one needs. Every example assumes your token is in $TOKEN.
Permissions your token needs
Make sure the owner grants these when they issue your token — a token can only do what it's scoped for.
products:read— list products and read variant ids (step 3).sales:create— reserve, confirm, post sales, and record lay-bye installments (both paths).inventory:read(optional) — see live stock with?includeStock=true.sales:read(optional) — read a sale back after you've created it.
Confirm your token works
Start by introspecting the token. GET /meechoes back the merchant you're bound to, the store you're pinned to (if any), and the exact permissions you were granted. If salesisn't in the list, ask the owner to widen your token before going further.
curl https://phindu.dartsmw.com/api/ext/v1/me \
-H "Authorization: Bearer $TOKEN"{
"merchant": "m_8h2…",
"store": null,
"permissions": {
"products": ["read"],
"inventory": ["read"],
"sales": ["create", "read"]
}
}Find your store reference
A sale is posted to a store, addressed by its short reference — never its internal id. List the stores you can reach and copy the reference (here, MAIN). If your token is already pinned to one store, /me shows it and you can skip this.
curl https://phindu.dartsmw.com/api/ext/v1/stores \
-H "Authorization: Bearer $TOKEN"{
"items": [
{
"id": "rec_9271hgd0",
"name": "Main Branch",
"reference": "MAIN",
"currency": "MWK",
"isDefault": true
}
]
}Find the variant you're selling
This is the step most people trip on, so it's worth being precise. A product is the thing on the shelf (a T-shirt). A variant is the exact sellable unit (T-shirt · Medium · Red) — and it's the variant, not the product, that carries the SKU, barcode, price and stock. A sale line always points at a variant id.
List products and each one carries its variants inline. Add ?search= to match on name or reference, and ?includeStock=true to see live availability per store.
curl "https://phindu.dartsmw.com/api/ext/v1/products?search=t-shirt&includeStock=true" \
-H "Authorization: Bearer $TOKEN"{
"items": [
{
"id": "prod_4kd0…",
"name": "Cotton T-Shirt",
"variants": [
{
"id": "var_7h2k9mn4qp",
"skuCode": "TSHIRT-M-RED",
"barcode": "6009…",
"price": 6000,
"stock": [
{ "store": "MAIN", "quantity": 20, "booked": 6, "available": 14 }
]
}
]
}
]
}What to grab: the variant id (var_7h2k9mn4qp) and the store reference (MAIN). That's everything a sale line needs. skuCode and barcodeare for humans and receipts — don't send those to the API.
You've got everything a sale line needs. From here the flow forks on how the money is settled — switch between the two:
Ring it up — reserve, then confirm
You now have a store reference and a variant id: enough to sell. The one catch is that you never send prices. The till computes the subtotal, tax and grand total from the catalogue, so you don't know the amount to charge until the server tells you. The clean way to handle that is a two-step hold.
a. Reserve the cart. This books the stock and returns the computed grandTotal — but takes no money. The hold lives for 90 seconds (expiresAt). The Idempotency-Keyis your own reference; replaying it won't double-book.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sale-reservations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{
"store": "MAIN",
"items": [
{ "variant": "var_7h2k9mn4qp", "quantity": 2 }
]
}'{
"order": {
"id": "o_5kd93jf0",
"status": "reserved",
"grandTotal": 12000,
"expiresAt": "2026-07-05T10:32:14Z"
}
}b. Charge the customer on your own gateway for exactly the grandTotal you got back (12000).
c. Confirm the hold. The same order flips into a real, receipted sale. amount must equal that grandTotal, and method is how the customer actually paid.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sale-reservations/o_5kd93jf0/confirm \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payment": {
"method": "card",
"amount": 12000,
"reference": "psp_txn_991"
}
}'{
"order": {
"id": "o_5kd93jf0",
"status": "fulfilled",
"grandTotal": 12000,
"amountPaid": 12000,
"balanceDue": 0
},
"receipts": {
"mra": { "validationUrl": "https://…", "issuedAt": "2026-07-05T10:31:02Z" }
}
}If the customer's card fails, do nothing — the hold lapses after 90 seconds and the stock returns to the shelf on its own. There's no cleanup call to make.
Already collected the money? Do it in one call
If your own processor has already captured the payment and you know the exact total, skip the hold and post the sale directly. POST /sales takes the same store and items, plus a payment. Omit payment.method and it settles under third_party — the rail for money a gateway already took. The amount must still match the server-computed grandTotal, which is exactly why most integrations reserve first to learn it.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sales \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{
"store": "MAIN",
"status": "fulfilled",
"items": [{ "variant": "var_7h2k9mn4qp", "quantity": 2 }],
"payment": { "amount": 12000, "reference": "psp_txn_991" }
}'Some sales aren't paid all at once. A lay-bye books the goods against a deposit and lets the customer pay the balance over time. You open it once, then post each installment as the money comes in — the till does the running-total maths and issues the final receipt the moment it's paid off.
Open the lay-bye with a deposit
Same POST /sales as a normal sale, but set fulfillmentType to lay_bye and status to pending. The paymentis the opening deposit (it must meet the product's minimum). As always, you don't send prices — the server computes the grandTotaland tells you what's still owed.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sales \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: laybye-7731" \
-d '{
"store": "MAIN",
"status": "pending",
"fulfillmentType": "lay_bye",
"customer": "8n3k9d2p1q7wz0x",
"items": [{ "variant": "var_7h2k9mn4qp", "quantity": 5 }],
"payment": { "amount": 10000, "reference": "psp_txn_dep1" }
}'{
"order": {
"orderNumber": "ORD-260705-K7M2QP4A",
"status": "pending",
"fulfillmentType": "lay_bye",
"grandTotal": 30000,
"amountPaid": 10000,
"balanceDue": 20000
}
}What to grab: the orderNumber (ORD-260705-K7M2QP4A). That's the handle you pay against — it's on the customer's receipt, so it's the one to store. (You may also use the record order.id if you kept it.)
Record each installment as it's paid
Every time the customer pays a bit more, post it to POST /sales/{orderNumber}/payments. The body is just the amount and your reference — no payment method. Money arriving through the API is always settled on the third_partyrail, so there's no channel to choose. Repeat until the balance is clear.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sales/ORD-260705-K7M2QP4A/payments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: laybye-7731-p2" \
-d '{ "amount": 12000, "reference": "psp_txn_dep2" }'{
"order": {
"orderNumber": "ORD-260705-K7M2QP4A",
"status": "pending",
"grandTotal": 30000,
"amountPaid": 22000,
"balanceDue": 8000
}
}The response always echoes the fresh running total, so you never track it yourself. Re-posting the exact same body within a few minutes is treated as a retry, not a second charge.
The final payment closes it out
When an installment brings amountPaid up to the grandTotal, the sale flips to fulfilledon its own and the MRA-compliant receipt is issued — no separate "complete" call.
curl -X POST https://phindu.dartsmw.com/api/ext/v1/sales/ORD-260705-K7M2QP4A/payments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "amount": 8000, "reference": "psp_txn_dep3" }'{
"order": {
"orderNumber": "ORD-260705-K7M2QP4A",
"status": "fulfilled",
"grandTotal": 30000,
"amountPaid": 30000,
"balanceDue": 0
},
"receipts": {
"mra": { "validationUrl": "https://…", "issuedAt": "2026-07-09T14:02:11Z" }
}
}Overpaying is rejected with a 422 that tells you the remainingPayable, and once a lay-bye is fulfilled it stops accepting payments (409 not_open_lay_bye) — so a stray retry can never push it past the total.
Errors
Errors return the right HTTP status plus a JSON body carrying a stable machine code and a human message — so you can branch on code and log the message. Common ones: 401 (bad or expired token), 403 (missing permission), 409 insufficient_stock (with a shortages[] list), and 429 rate_limited (with Retry-After).
Get in touch
Building something against GeniYanga and need a hand — or want to talk about a deeper partnership? Email us at support@geniyanga.shop.