Sahtein صحتين
Menu

Sahtein · Developer Docs

POS integration

Connect a POS, a thermal-printer bridge, or a no-code tool (Make, Zapier, n8n) to a Sahtein restaurant: we push signed webhooks for every order event, and you push statuses, sold-out flags and menu reads back through the REST API.

1. Overview

The integration is two one-way channels sharing one credential. The restaurant issues an API key from Dashboard → Settings → Integrations, and registers your webhook receiver there too.

DirectionMechanismUsed for
Sahtein → youWebhooks (signed POSTs)New orders, status changes, cancellations, sold-out flags
You → SahteinREST APIAdvancing order status from the kitchen, marking items sold out, reading the menu

Start from the edges: register a webhook and confirm you receive order.created, then add the status PATCH. Base URL: https://api.sahtein.app/v1 (self-hosted installations use their own host).

2. Authentication

Every API call carries the key in an Authorization header:

Authorization: Bearer shk_<12-char prefix>_<48-char secret>

The full token is shown once, at creation — we store only a hash. A key carries scopes chosen at creation (orders:read, orders:write, menu:read, menu:write); calls outside them return 403. Keys are server-to-server only — never ship one in a browser or a mobile app. If a key leaks, revoke it in the dashboard; revocation is immediate.

3. Error format

Every error is JSON with a stable code:

{
  "error": {
    "code": "invalid_transition",
    "message": "Invalid transition: ready → accepted",
    "requestId": "..."
  }
}
HTTPcodeWhen
401unauthorizedMissing, malformed, revoked or expired key
403forbiddenKey lacks the required scope
404not_foundResource does not exist in your restaurant
409invalid_transitionStatus move the state machine forbids
422validation_errorRequest body fails validation
429rate_limitedOver the limits — see Rate limits

4. Webhooks (outbound)

When something happens we POST JSON to your URL. Respond 2xx within 8 seconds; anything else is retried on the schedule below. Heavy processing belongs on your own queue — acknowledge first, work later.

Envelope and headers

POST <your-url>
Content-Type: application/json
X-Sahtein-Event: order.created
X-Sahtein-Delivery: <uuid — use as your dedup key>
X-Sahtein-Signature: t=<unix seconds>,v1=<hex hmac>

{
  "id": "<delivery uuid>",
  "event": "order.created",
  "created_at": "2026-08-18T10:30:45.123Z",
  "tenant_id": "<restaurant uuid>",
  "data": { ...event-specific payload... }
}

Verifying the signature

The signature is HMAC-SHA256 over `${t}.${rawBody}` with the endpoint secret shown when the endpoint was registered. Verify against the raw request body, before any JSON parsing:

const crypto = require("crypto");

function verify(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(parts.v1)
  );
}

Reject unverified payloads with 401 and do not process them. Retries mint a new delivery id per attempt, so dedup on X-Sahtein-Delivery and you will never double-process.

Events

EventFires whenKey payload fields
order.createdAn order is placed (any channel: web, app, kiosk, table QR)order_id, number, total, items[], customer
order.status_changedEvery fulfillment transition except cancellationorder_id, from, to, changed_at
order.cancelledAn order is cancelled by stafforder_id, reason
menu.availability_changedAn item is marked sold out / back in stockproduct_id, available

Money fields are integer agorot (₪108.50 = 10850). Item and product names are localized objects ({"ar": "...", "he": "...", "en": "..."}) — pick the language your screen prints.

Retry schedule

AttemptDelay after failure
1immediate
2+1 minute
3+5 minutes
4+15 minutes
5+1 hour
6+6 hours

After six failures the delivery is marked abandoned. Nothing is deleted — the restaurant sees the full history in the dashboard and can retry an abandoned delivery manually. Webhooks are emitted transactionally with the change they describe: if you receive an event, a GET for that order will always succeed.

5. Orders API

GET  /partner/v1/orders?status=active&page=1&per_page=30
GET  /partner/v1/orders/{id}
PATCH /partner/v1/orders/{id}/status   { "status": "in_preparation" }

status=active returns everything in flight. The PATCH drives the same state machine as the restaurant's own console and kitchen screen — your update moves the customer's live tracking page, prints tickets, and the console in one stroke. Setting accepted performs full acceptance: kitchen tickets are routed and print jobs queued exactly as if staff had tapped Accept.

You may setNotes
acceptedFull acceptance — tickets + prints + delivery job
in_preparationKitchen started
readyAll stations done; clears the KDS lanes
picked_up · out_for_delivery · delivered · completedHand-off and completion

Cancellation and refunds are staff-only (they touch money) and return 409 from this API. Illegal moves — e.g. delivered straight from accepted — return 409 invalid_transition.

Loop warning: your own PATCH triggers an order.status_changed webhook back at you. Dedup on the delivery id and never react to a transition you caused.

7. Rate limits

600 requests/minute per key, plus a burst shield of 40 requests/second. Both are far above what a synchronized POS needs — approaching either usually means an event loop reacting to itself. Over a limit you get 429 with Retry-After; successful partner responses carry X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset for the per-minute budget.

8. Integration checklist

  1. Get an API key from the restaurant (Settings → Integrations); store it in a vault, never in client code.
  2. Smoke-test with GET /partner/v1/orders?per_page=1.
  3. Register your webhook endpoint; verify the HMAC against the raw body; dedup on X-Sahtein-Delivery.
  4. Acknowledge within 8 seconds; queue heavy work on your side.
  5. PATCH /orders/{id}/status at each kitchen step; ignore the webhook echoes of your own transitions.
  6. Optional: pull /menu daily and push sold-out flags as stock runs out.

Questions: [email protected] — include the X-Sahtein-Delivery id or order_id you're asking about.