Narrow.TV Connect API v1 openapi.json

Narrow.TV developer documentation

Connect API

Connect your own systems to Narrow.TV: send POS data to your accounting package, keep products and stock in sync with your web shop or ERP, and process kiosk and Scan & Order orders. Webhooks tell your system when something changes.

The API is included for customers with at least one screen of type POS, order kiosk or Scan & Order. The account owner creates API tokens and webhooks in the Narrow.TV dashboard, under the account menu → API-toegang.

Conventions

  • Requests and responses are JSON in UTF-8. Send Content-Type: application/json with a request body.
  • Money is an integer number of cents, with a currency field (for example 895 with "EUR" is € 8.95).
  • Times are ISO 8601 in UTC. Ids are UUIDs.
  • HTTPS only. The API is meant for server-to-server use and does not send CORS headers, so do not call it from a browser.
  • Changes made through the API are attributed to API: <token name>, for example in the stock history.

Environments

EnvironmentBase URL
Productionhttps://connect.narrow.tv/v1
Sandboxhttps://connect.sbx.narrow.tv/v1

The sandbox is a separate environment with its own dashboard, data and tokens. Build and test your integration there, then create a production token and switch the base URL.

The full machine-readable specification is available as OpenAPI 3.1. You can import it into Postman, Insomnia or a client generator.

Getting started

Two common integrations, step by step. Both start with a token from the dashboard (see Authentication).

1. Daily figures for your accounting package

  1. Create a token with the scope pos:read for the locations you want to book.
  2. Call GET /locations to find the location ids.
  3. Once a day, after closing, call GET /locations/{locationId}/daily-summary?date=YYYY-MM-DD. The summary contains sales, refunds, discounts, the split per VAT rate and per payment method. It is calculated from the journal in the location's time zone (Europe/Amsterdam), so it also works for days without a Z report.
  4. Need the individual receipts? Page through GET /locations/{locationId}/transactions?from=…&to=…, or subscribe to the pos.transaction.created webhook.

2. Stock from your web shop

  1. Create a token with catalog:read and stock:write.
  2. Match products by barcode: GET /locations/{locationId}/products?barcode=….
  3. After a stock count, set the quantity with PUT /locations/{locationId}/stock/{productId}. For a delivery or a correction, send the change with POST /locations/{locationId}/stock/adjustments.
  4. Send an Idempotency-Key with every write, so a retry after a network error is never booked twice.
  5. Subscribe to stock.changed to hear about sales at the register.

Authentication

Every request needs an API token in the Authorization header:

Authorization: Bearer ntk_…
  • Create tokens in the dashboard, under the account menu → API-toegang. Only the account owner can do this. Give each integration its own token with a recognisable name.
  • The token is shown once, right after you create it. Narrow.TV only stores a hash, so it cannot show the token again. Store it in a secret manager or environment variable, never in source code or a browser.
  • The dashboard shows the first characters of each token (its prefix), when it was last used and when it expires, so you can tell tokens apart.
  • To rotate a token, create a new token with the same permissions, deploy it, and then revoke the old one. A token can also get an expiry date.
  • A missing, unknown, expired or revoked token returns 401 unauthorized.
  • If the account no longer has a POS, order kiosk or Scan & Order screen, all tokens return 403 api_not_available. Nothing is deleted: the tokens work again once a qualifying screen is added.

Scopes

A token's permissions are set per location, as a set of scopes. A request without the required scope returns 403 forbidden_scope. Each endpoint in the reference shows the scope it needs.

ScopeAllows
pos:readPOS transactions, Z reports and daily summaries.
catalog:readCategories, products and option groups.
catalog:writeCreate, update and delete the location's categories and products.
stock:readStock levels and stock movements.
stock:writeStock counts (set a quantity), deliveries and corrections (add or subtract).
orders:readOrder kiosk and Scan & Order orders.

Give a token only the scopes it needs. A web shop that syncs stock does not need pos:read.

Locations

Almost every endpoint lives under a location: /locations/{locationId}/…. A token only has access to the locations that were ticked for it in the dashboard, each with its own scopes. For example, a token can read POS data for one shop and write stock for another.

  • GET /locations lists the locations the token can access, with the scopes it has for each one.
  • A location the token has no access to returns 403 forbidden_location.
  • Dates such as the daily summary's date are interpreted in the location's time zone (Europe/Amsterdam). Timestamps in responses are always UTC.

Pagination

List endpoints return a page of results and a cursor for the next page:

  • limit sets the page size: 50 by default, at most 200.
  • The response has the shape { "data": [ … ], "nextCursor": "…" }.
  • To get the next page, repeat the same request with cursor=<nextCursor>. Keep the other query parameters the same.
  • When nextCursor is null, you have reached the last page.
  • Cursors are opaque: do not parse or build them yourself.

Most lists can also be filtered: updatedSince (ISO 8601) on products and categories, and from / to (an ISO date or date-time) on transactions, Z reports, stock movements and orders. For incremental syncs, store the time of your last successful sync and pass it as updatedSince next time.

Idempotency

Write requests (POST, PUT, PATCH, DELETE) accept an Idempotency-Key header of at most 100 characters. Use a unique value per intended change, such as a UUID or your own delivery number.

  • The same key with the same body within 24 hours returns the stored response. The change is not applied a second time.
  • The same key with a different body returns 409 conflict.
  • Keys are stored per token and expire after 24 hours.

This makes it safe to retry after a timeout or a dropped connection. It matters most for stock adjustments, where a duplicate would change the stock twice.

Rate limits

LimitRequests
Per token120 per minute
Per customer (all tokens together)600 per minute

Every response carries the token's current window in X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp).

When you go over a limit, the API returns 429 rate_limited with a Retry-After header: the number of seconds to wait before trying again. Honour it, and add some jitter when several workers retry at the same time.

Repeated requests with an invalid token are limited per IP address as well; wait for Retry-After before trying again.

Prefer webhooks and updatedSince over polling full lists, and use limit=200 for bulk reads.

Errors

Errors use the usual HTTP status codes and a JSON body with a stable, machine-readable code and a human-readable message. Base your logic on the code; the message may change.

Status and codeMeaning
400 validation_failedThe request is malformed, for example invalid JSON or a bad query parameter.
401 unauthorizedThe token is missing, unknown, expired or revoked.
403 forbidden_scopeThe token lacks the scope this endpoint needs for this location.
403 forbidden_locationThe token has no access to this location.
403 api_not_availableThe account has no POS, order kiosk or Scan & Order screen (any more).
404 not_foundThe resource does not exist in this location.
409 conflictThe request conflicts with the current state, or an Idempotency-Key was reused with a different body.
422 validation_failedThe body is valid JSON but a field is missing or invalid.
429 rate_limitedToo many requests. Wait for Retry-After seconds.

Retry 429 and 5xx responses with backoff (with the same Idempotency-Key for writes). Do not retry other 4xx responses without changing the request.

Webhooks

Instead of polling, let Narrow.TV call your server when something happens. The account owner adds webhooks in the dashboard, under API-toegang: an HTTPS URL, the events to receive, and optionally which locations (none selected means all locations). You receive a signing secret (whsec_…) once; it can be rolled later.

Events

EventSent when
pos.transaction.createdA journal row is written at the register: sales, refunds, opening float and cash in/out.
pos.zreport.createdA Z report is created.
stock.changedStock changed through any stock movement. Bursts are combined: at most one event per product per minute, with the latest quantity.
order.createdA kiosk or Scan & Order order is created.
order.status_changedThe status of a kiosk or Scan & Order order changes.
catalog.product.changedA product is created, updated or deleted.
pingYou press Test in the dashboard.

Payload

Each delivery is a POST with a JSON envelope. data has the same shape as the matching GET endpoint returns.

Delivery and retries

  • Events are delivered by a background job that runs every minute, so an event usually arrives within a minute. Do not rely on it being instant.
  • Respond with any 2xx status within 10 seconds. Redirects are not followed. Do the heavy work after responding, for example in a queue.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. After that, the delivery is given up.
  • After 50 failed deliveries in a row, the webhook is switched off and the account owner gets an e-mail. Switch it back on in the dashboard once your endpoint works again.
  • Deliveries are at least once and may arrive out of order. Use the event id to skip duplicates, and createdAt or a follow-up GET to get the latest state.
  • The dashboard shows the latest deliveries with their status code and lets you send one again.
  • Webhook URLs must use HTTPS and must resolve to a public address.

Signature verification

Every delivery carries a NarrowTV-Signature header, so you can check that it really comes from Narrow.TV and was not changed or replayed:

NarrowTV-Signature: t=<unix timestamp>,v1=<signature>

The signature is the hex-encoded HMAC-SHA256 of the timestamp, a dot and the raw request body, with your webhook secret (the full value, including whsec_) as the key:

v1 = hex(hmac_sha256(secret, t + "." + raw_body))
  1. Read the raw request body, before any JSON parsing. Re-encoded JSON will not match.
  2. Split the header on , and each part on the first =. Take t and the v1 value(s). Ignore parts you do not know.
  3. Reject the request if t is more than 5 minutes away from your current time. This blocks replayed deliveries.
  4. Compute the expected signature and compare it with a constant-time comparison. Accept the request if any v1 matches.
  5. Respond with 400 or 401 when the check fails.

When you roll the secret in the dashboard, the new secret applies to the next deliveries. Update your server right away.