Developer API

Connect your own tools to your store: a read-only REST API and signed webhooks. Keys and webhooks are issued by the store owner in the dashboard under Developers.

Authentication · Scopes · Rate limits · Endpoints · Webhooks · Verifying signatures · Errors

Authentication

Every request carries the store's API key as a bearer token:

curl https://stellarpos.us/api/v1/me \
  -H "Authorization: Bearer sk_live_xxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Scopes

ScopeGrants
menu:readGET /api/v1/menu/items
orders:readGET /api/v1/orders, GET /api/v1/orders/{id}

Any valid key may call GET /api/v1/me. A request outside the key's scopes answers 403 insufficient-scope. The API is read-only.

Rate limits

120 requests per minute per key. Over the limit the API answers 429 rate-limited with a Retry-After header (seconds). All Stellar APIs also share a ceiling of 2,000 requests per 15 minutes per IP address.

Endpoints

GET /api/v1/me

{ "store": { "id": "…", "name": "Cafe Luna", "timezone": "America/Los_Angeles", "currency": "USD" },
  "key":   { "name": "Accounting sync", "scopes": ["orders:read"] } }

GET /api/v1/menu/items?limit=50&after={id}

The store's menu (deleted items are excluded), in stable id order. limit is 1–100 (default 50). Pass nextAfter from a response as after to get the next page; nextAfter: null means the last page.

{ "data": [ { "id": "…", "name": "Burger", "description": null, "price": 12.5, "categoryId": "…", "category": "Mains",
              "visible": true, "soldOut": false, "taxExempt": false, "allergens": ["gluten"], "updatedAt": "2026-10-07T18:00:00.000Z" } ],
  "nextAfter": "…" }

GET /api/v1/orders?updatedSince={ISO-8601}&after={id}&status={STATUS}&limit=50

Orders changed since a moment, oldest change first (ordered by updatedAt, then id). limit is 1–100 (default 50); status keeps one order status (for example COMPLETED).

Every response carries next: pass both of its values back as updatedSince and after to continue strictly after the last order you received. hasMore: true means another page is ready now. When a page is empty, next repeats your cursor, so a sync can keep polling with it. An order that changes again shows up again with its new updatedAt: keep the newest copy of each id.

{ "data": [ { …order… } ], "hasMore": true,
  "next": { "updatedSince": "2026-10-07T20:00:00.123Z", "after": "…" } }

GET /api/v1/orders/{id}

One order of this store. An id from another store answers 404.

{ "data": { "id": "…", "status": "COMPLETED", "orderType": "DINE_IN", "tableId": "…",
            "subtotal": 20, "taxAmount": 1.7, "tipAmount": 3, "serviceCharge": 0, "surchargeAmount": 0, "cashDiscount": 0,
            "discount": null, "totalAmount": 24.7, "paymentMethod": "CARD", "isBarTab": false,
            "splitOfOrderId": null,
            "createdAt": "…", "updatedAt": "…",
            "items": [ { "id": "…", "menuItemId": "…", "name": "Burger", "quantity": 2, "unitPrice": 10, "voided": false,
                         "taxExempt": false, "seat": null, "course": null, "firedAt": "…", "note": "no onions", "discount": null,
                         "splitClaimedByOrderId": null, "modifiers": [ { "name": "Extra cheese", "price": 1 } ] } ] } }

Money is in the store's currency as decimal amounts. Staff identities, customer names, order notes, signatures and processor references are never included.

Webhooks

The owner adds an endpoint and chooses events in the dashboard. Stellar sends an HTTPS POST with a JSON body when an event happens:

EventWhen
order.createdA new order: register, kiosk, online, QR, a bar tab, or a delivery-platform order once it is accepted
order.status_changedAn order's status changed (for example to COMPLETED or REFUNDED)
order.updatedItems were added (also by a guest’s QR order), voided or fired, a line’s price or seat changed, the order moved tables, or it was split. A new split check appears in GET /api/v1/orders and sends order.status_changed when it is paid.
pingOnly from the dashboard's Send test button
POST {your endpoint}
Content-Type: application/json
X-Stellar-Event: order.status_changed
X-Stellar-Delivery: evt_8f1c…
X-Stellar-Signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{ "id": "evt_8f1c…", "type": "order.status_changed", "created": 1760000000, "storeId": "…",
  "data": { "order": { …same shape as GET /api/v1/orders/{id}… } } }

Verifying signatures

Each endpoint has its own signing secret (whsec_…), shown once when the endpoint is added. Compute an HMAC-SHA256 of "{t}.{raw body}" with that secret and compare it with v1; reject old timestamps.

const crypto = require('crypto');
function verify(secret, header, rawBody, toleranceSec = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest();
  const given = Buffer.from(m[2], 'hex');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
Verify against the raw request body bytes, before any JSON parsing.

Errors

StatuscodeMeaning
400invalid-requestA query parameter is malformed
401api-key-required / api-key-invalid / api-key-revokedMissing, wrong or revoked key
403insufficient-scopeThe key lacks the scope this endpoint needs
404not-foundNo such order or route for this store
429rate-limitedOver 120 requests per minute for this key
503api-key-unavailableKey verification is temporarily unavailable — retry