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
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"
- The full key is shown once, when the owner creates it. Stellar stores only a one-way hash; a lost key cannot be recovered — revoke it and issue a new one.
- A key belongs to one store and can read only that store. Staff logins and dashboard tokens are not accepted here.
- Call the API from your own server. A key is a password: never put it in a web page or an app, where anyone could read it (browsers are not served).
- Revoking a key stops it immediately (
401 api-key-revoked).
Scopes
| Scope | Grants |
|---|---|
menu:read | GET /api/v1/menu/items |
orders:read | GET /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.
- Status: for example
PENDING,IN_KITCHEN,READY,COMPLETED,CANCELLED,VOIDED,REFUNDED,PARTIALLY_REFUNDED. Treat a value you do not know as “other”; more may be added. - Voided lines stay on the order with
voided: true, and every price on them is0: the line and its modifiers are not charged. - Splits: a split check is its own order whose
splitOfOrderIdnames the original. The lines it took over stay listed on the original withsplitClaimedByOrderIdset, and the split check carries its own copy: count such a line once. - Refunds are recorded on the order itself: its status becomes
REFUNDEDorPARTIALLY_REFUNDED. Refunded amounts are not in this version of the API.
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:
| Event | When |
|---|---|
order.created | A new order: register, kiosk, online, QR, a bar tab, or a delivery-platform order once it is accepted |
order.status_changed | An order's status changed (for example to COMPLETED or REFUNDED) |
order.updated | Items 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. |
ping | Only 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}… } } }
- Endpoints must be public
https://host names. Addresses that resolve to private, loopback, link-local or other non-public networks are refused at delivery time. Redirects are not followed. - Answer with any
2xxwithin 5 seconds. Anything else is retried twice (after about 2 and 10 seconds); the dashboard shows each endpoint's last status. An event or retry still pending when the server restarts is not sent — use the API to reconcile. A failing endpoint is not switched off automatically: remove it in the dashboard if it is gone. - An event is sent about 1.5 seconds after the change and describes the order as it is then. Events can arrive out of order: compare the order’s
updatedAtand keep the newest. UseX-Stellar-Deliveryto de-duplicate retries. An event can also follow a change this API does not show (a kitchen note, for example); the order in it is then unchanged. - A new or removed endpoint takes effect within 30 seconds.
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);
}
Errors
| Status | code | Meaning |
|---|---|---|
| 400 | invalid-request | A query parameter is malformed |
| 401 | api-key-required / api-key-invalid / api-key-revoked | Missing, wrong or revoked key |
| 403 | insufficient-scope | The key lacks the scope this endpoint needs |
| 404 | not-found | No such order or route for this store |
| 429 | rate-limited | Over 120 requests per minute for this key |
| 503 | api-key-unavailable | Key verification is temporarily unavailable — retry |