Developers

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. 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. 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. 3

    Call the API

    Send the token as a bearer credential against https://phindu.dartsmw.com/api/ext/v1. Start with GET /me to 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 · /categoriesIntrospect your token and read reference data.
GET/products · /inventoryRead the catalogue and live stock positions.
POST/products · /inventory/adjustmentsCreate products; adjust, recount, and transfer stock.
GET/customersList and read customers; POST/PATCH to sync them.
POST/salesRing up a sale — stock, pricing, receipt, and audit.
POST/sale-reservationsHold a cart, charge out-of-band, then confirm.
POST/sales/{id}/paymentsRecord a lay-bye installment against an open sale.
GET/changesIncremental sync feed across all readable entities.

The live contract — every field, filter, and error code — is served straight from the backend:

https://phindu.dartsmw.com/api/ext/v1/docs

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.
1

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"]
  }
}
2

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
    }
  ]
}
3

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:

4

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" }
  }'

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.