Nunua Pro Developers
Reference · v1.0.0

API reference

Every endpoint, field and status code of the POS integration API. New here? Start with the guide; this page is for looking things up.

Base URLhttps://nunuapro.app/api
Download OpenAPI spec

Connect a point-of-sale system to Nunua Pro: keep a shop's products, prices and stock online, and receive the orders customers place. One generic contract for every POS, cloud or locally installed.

How it fits together

  1. Link a shop. Call POST /link-codes (partner key), show the code on the till, and the shop owner types it into the Business app. Poll GET /link-codes/{code} to collect the shop's API key (once).
  2. Send the catalogue. POST /sync/snapshots → batches → commit, then keep it fresh with PATCH /inventory on every sale, stock receipt or price change.
  3. Receive orders. Read GET /events (works for a locally installed POS that cannot receive webhooks) or register a webhook. Record the sale, then POST /orders/{id}/ack.

Rules worth knowing

  • Nothing is deleted. A product that leaves the POS is archived; imports start unlisted until the shop approves them.
  • Stale writes are ignored. Send updated_at; an older update than what we hold is acknowledged as stale.
  • Stock. A product is in stock while stock_qty − shop buffer − units reserved for unacknowledged orders > 0. Send the new stock quantity before acknowledging an order: acknowledging releases the reserved units.
  • Orders reach you only after the customer has accepted the shop's quote, never earlier.
  • Idempotency. Send Idempotency-Key on writes; a repeat within 24 h replays the first answer.
  • Money is TZS only. Prices up to 100 000 000, stock within ±100 000 000; rows outside are rejected individually.
  • Barcodes are matched to the shared catalogue only when they are a valid GTIN (check digit). Junk is stored, never matched on.
  • Photos are https addresses on public hosts, fetched in the background; a photo the shop uploaded itself is never replaced.

Webhook signature

X-Nunua-Signature: sha256= + HMAC-SHA256 of "{X-Nunua-Timestamp}.{raw body}" with the shop's webhook secret. Reject timestamps older than a few minutes. Failed deliveries retry for 24 h; GET /events always has the truth.

Sandbox

POST /pos-partner/v1/sandbox gives a hidden test shop with an npk_test_… key. Use it exactly like a real shop, and POST /sandbox/orders to simulate a customer order, then cancel or complete it.

Authentication

Send a key as a bearer token on every request: Authorization: Bearer …. Each endpoint below says which key it takes.

KeyLooks likeUsed on
Shop keynpk_live_… or npk_test_…/pos/v1: one shop's products, stock and orders
Partner keynpv_live_…/pos-partner/v1: link codes, the sandbox, your shops' health

Get keys in the developer console. More in keys and authentication.

Errors

Every error has the same shape. Branch on error.code; show error.message to people. The full list of codes is in limits and errors.

Error response
{ "error": { "code": "invalid_key", "message": "The API key is missing, wrong or revoked." } }

Catalogue

Check the key

GET/pos/v1/pingShop key

Responses

  • 200The shop and what you may do.
  • 401Missing, wrong or revoked key.
  • 402The shop's plan does not include POS sync.
  • 403The shop paused the connection.
Request
curl https://nunuapro.app/api/pos/v1/ping \
  -H "Authorization: Bearer $NUNUA_KEY"

Upsert up to 200 products

POST/pos/v1/products/batchShop key

Always 200 with a result per product; one bad row never fails the batch.

Headers

  • Idempotency-Keystring

    Repeat of the same key within 24 h returns the first response (header Idempotent-Replay: true).

Body JSON, required

Responses

  • 200One result per product.
Request
curl -X POST https://nunuapro.app/api/pos/v1/products/batch \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
        {
            "external_id": "SKU-1",
            "name": "SUKARI KILOMBERO 1KG",
            "price": 5500
        }
    ]
}'
Response 200
{
    "results": [
        {
            "external_id": "SKU-1",
            "status": "created",
            "product_id": 48213,
            "listed": false,
            "catalog": "linked",
            "errors": []
        }
    ]
}

Price, stock or availability for up to 500 known products or variants

PATCH/pos/v1/inventoryShop key

Call on every sale, stock receipt or price change.

Headers

  • Idempotency-Keystring

    Repeat of the same key within 24 h returns the first response (header Idempotent-Replay: true).

Body JSON, required

  • items array of object required

    Up to 500 items

  • items[].external_id string required
  • items[].variant_external_id string
  • items[].price number

    0 to 100,000,000

  • items[].stock_qty integer
  • items[].active boolean
  • items[].updated_at string

    ISO 8601 with offset

Responses

  • 200One result per item (updated, stale or rejected with unknown_product, unknown_variant, product_has_variants).
Request
curl -X PATCH https://nunuapro.app/api/pos/v1/inventory \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
        {
            "external_id": "SKU-1",
            "price": 5500
        }
    ]
}'

What Nunua Pro holds (for reconciliation)

GET/pos/v1/productsShop key

Query parameters

  • updated_sincestring
  • cursorinteger
  • limitinteger

    Default 100 · At most 500

Responses

  • 200Products with their variants and next_cursor.
Request
curl https://nunuapro.app/api/pos/v1/products \
  -H "Authorization: Bearer $NUNUA_KEY"

Archive a product (never deletes)

DELETE/pos/v1/products/{externalId}Shop key

Path parameters

  • externalIdstringrequired

Responses

  • 200Archived.
  • 404Unknown product.
Request
curl -X DELETE https://nunuapro.app/api/pos/v1/products/SKU-1 \
  -H "Authorization: Bearer $NUNUA_KEY"

Open a full-catalogue resync

POST/pos/v1/sync/snapshotsShop key

Abandons any snapshot still open.

Responses

  • 201snapshot_id.
Request
curl -X POST https://nunuapro.app/api/pos/v1/sync/snapshots \
  -H "Authorization: Bearer $NUNUA_KEY"

Stream a batch of the catalogue into the snapshot

POST/pos/v1/sync/snapshots/{id}/itemsShop key

Same body and result as /products/batch; marks each product as still in the POS.

Path parameters

  • idintegerrequired

Body JSON, required

Responses

  • 200One result per product.
  • 409The snapshot is closed.
Request
curl -X POST https://nunuapro.app/api/pos/v1/sync/snapshots/5521/items \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
        {
            "external_id": "SKU-1",
            "name": "SUKARI KILOMBERO 1KG",
            "price": 5500
        }
    ]
}'

Finish the snapshot; archive what the POS no longer has

POST/pos/v1/sync/snapshots/{id}/commitShop key

Refused with 422 too_many_removed if it would archive more than 40 % of the catalogue, unless force is true. Products that came from a CSV file are never archived by a snapshot.

Path parameters

  • idintegerrequired

Body JSON, optional

  • force boolean

Responses

  • 200archived and seen counts.
  • 409The snapshot is closed.
  • 422Would archive too much.
Request
curl -X POST https://nunuapro.app/api/pos/v1/sync/snapshots/5521/commit \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "force": true
}'

Orders

Orders the POS was told about

GET/pos/v1/ordersShop key

Query parameters

  • statestring

    One of pending, accepted, rejected, voided, all · Default 'pending'

  • cursorinteger
  • limitinteger

    Default 50 · At most 200

Responses

  • 200Orders (see Order) and next_cursor.
Request
curl https://nunuapro.app/api/pos/v1/orders \
  -H "Authorization: Bearer $NUNUA_KEY"

The change feed (poll this if you cannot receive webhooks)

GET/pos/v1/eventsShop key

order.confirmed, order.cancelled (payload says what the POS had done: was = pending|accepted, and pos_ref, so you know whether to void a recorded sale) and order.completed, in order. Keep the last event_id and pass it as after.

Query parameters

  • afterinteger
  • limitinteger

    Default 100 · At most 200

Responses

  • 200Events and next_after.
  • 403Connection paused. Resume it and the feed catches up.
Request
curl https://nunuapro.app/api/pos/v1/events \
  -H "Authorization: Bearer $NUNUA_KEY"

Say whether the POS recorded the sale

POST/pos/v1/orders/{id}/ackShop key

Safe to repeat (same answer → replay: true); a different answer after the first is 409. A rejection never cancels the order (the customer may have paid): the shop is told and decides. Send updated stock quantities before acknowledging.

Path parameters

  • idintegerrequired

    The Nunua Pro order id.

Body JSON, required

  • status string required

    One of accepted, rejected

  • pos_ref string

    Your sale number.

  • reason string

    One of out_of_stock, closed, other

  • message string
  • unavailable array of string

    Product or variant ids you have none of; they go off sale.

Responses

  • 200Recorded.
  • 404Unknown order for this shop.
  • 409Already answered differently.
Request
curl -X POST https://nunuapro.app/api/pos/v1/orders/5521/ack \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "accepted"
}'

Sandbox

Simulate a customer order (sandbox key only)

POST/pos/v1/sandbox/ordersShop key

Body JSON, optional

  • type string

    One of pickup, delivery · Default 'pickup'

  • lines array of object
  • lines[].external_id string
  • lines[].quantity integer

Responses

  • 201The confirmed order, as your POS will receive it.
  • 403Not a sandbox key.
  • 422Sync some products first.
Request
curl -X POST https://nunuapro.app/api/pos/v1/sandbox/orders \
  -H "Authorization: Bearer $NUNUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "pickup",
    "lines": [
        {
            "external_id": "SKU-1",
            "quantity": 1
        }
    ]
}'

Cancel or complete a sandbox order

POST/pos/v1/sandbox/orders/{id}/{action}Shop key

Path parameters

  • idintegerrequired
  • actionstringrequired

    One of cancel, complete

Responses

  • 200Done; the matching event is on the feed.
  • 404Not your sandbox order.
Request
curl -X POST https://nunuapro.app/api/pos/v1/sandbox/orders/5521/cancel \
  -H "Authorization: Bearer $NUNUA_KEY"

Partner

Make a code for the till to show a shop owner

POST/pos-partner/v1/link-codesPartner key

Body JSON, optional

  • label string

    Up to 120 characters

Responses

  • 201code (like K7QM-2XPD), expires_at (15 minutes).
Request
curl -X POST https://nunuapro.app/api/pos-partner/v1/link-codes \
  -H "Authorization: Bearer $NUNUA_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Till 2"
}'

Your linked shops and how their sync is doing

GET/pos-partner/v1/shopsPartner key

Responses

  • 200Per shop: status, last_seen_at, last_sync_at, last_error, products, orders_waiting.
Request
curl https://nunuapro.app/api/pos-partner/v1/shops \
  -H "Authorization: Bearer $NUNUA_PARTNER_KEY"

What your shops sold through Nunua Pro

GET/pos-partner/v1/statsPartner key

Query parameters

  • daysinteger

    Default 30 · At most 90

Responses

  • 200Totals and per-shop orders and goods value (accepted, not cancelled).
Request
curl https://nunuapro.app/api/pos-partner/v1/stats \
  -H "Authorization: Bearer $NUNUA_PARTNER_KEY"

Create (or rotate the key of) your sandbox shop

POST/pos-partner/v1/sandboxPartner key

Body JSON, optional

  • rotate boolean

Responses

  • 201Created; the npk_test_ key is shown once.
  • 200Exists already.
Request
curl -X POST https://nunuapro.app/api/pos-partner/v1/sandbox \
  -H "Authorization: Bearer $NUNUA_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rotate": true
}'

Objects

The shapes that appear in requests and responses.

Product

  • external_id string required

    Your own id for the product; stable.

    Up to 100 characters

  • updated_at string

    ISO 8601 with offset

  • name string required

    ALL-CAPS names are tidied for the storefront; your wording is kept.

    Up to 255 characters

  • names object

    Optional second language.

  • names.sw string
  • names.en string
  • description string

    Up to 2000 characters

  • category string
  • unit string
  • barcode string
  • sku string
  • price number

    0 to 100,000,000

  • currency string

    One of TZS

  • track_stock boolean
  • stock_qty integer
  • active boolean

    false archives the product.

    Default true

  • options array of object

    Up to 3 items

  • options[].name string
  • options[].values array of string

    Up to 50 items

  • variants array of Variant

    The COMPLETE set for the product; missing ones are archived.

    Up to 100 items

  • images array of object

    Up to 5 items

  • images[].url string

    https only

Variant

  • external_id string required

    Up to 100 characters

  • options object
  • price number

    0 to 100,000,000

  • stock_qty integer
  • track_stock boolean
  • active boolean
  • barcode string

ItemResult

  • external_id string
  • status string

    One of created, updated, stale, rejected, archived

  • product_id integer
  • listed boolean
  • catalog string

    One of linked, suggested, none

  • renamed_to string
  • warnings array of string
  • errors array of string

Order

  • id integer
  • order_number string
  • status string
  • type string

    One of delivery, pickup, parcel

  • currency string
  • created_at string

    ISO 8601 with offset

  • confirmed_at string

    ISO 8601 with offset

  • note string | null
  • contains_alcohol boolean
  • customer object | null
  • customer.name string
  • customer.phone string
  • delivery object | null
  • pickup object | null
  • subtotal number
  • delivery_fee number
  • total number
  • payment object
  • payment.method string
  • payment.status string | null
  • pos object | null
  • pos.state string

    One of pending, accepted, rejected, voided

  • pos.pos_ref string | null

OrderLine

  • line_id integer
  • external_id string | null
  • variant_external_id string | null
  • name string
  • quantity integer
  • unit_price number
  • addons array of object
  • addons[].name string
  • addons[].price number
  • line_total number

    Includes the add-ons.

Error

  • error object
  • error.code string
  • error.message string