Generic webhook: sending sales to Stokyo
With the generic webhook, any till, web shop or integrator sends its sales to Stokyo, which takes them off the stock automatically. This page is for whoever builds the connection.
- 1. Address and secret key
- 2. Sending a sale
- 3. Fields
- 4. Signing (HMAC-SHA256)
- 5. Refunds and voids
- 6. Responses
- 7. Good to know
1. Address and secret key
The business owner finds both in Stokyo under More → Connections → Generic connection → Show address and key. Each business gets its own address (ending in /pos-webhook/ep_…) and its own secret key (starting with whsec_). Keep the key on the server only, never in an app or web page.
2. Sending a sale
Send a POST with a JSON body. One event (without an events list) or up to 100 at once.
POST https://tcphaxqfzdfgvkxixbzk.supabase.co/functions/v1/pos-webhook/ep_…
Content-Type: application/json
Stokyo-Signature: t=1793400000,v1=5f2b…c91e
{
"events": [
{
"ticketId": "1287",
"type": "sale",
"occurredAt": "2026-10-30T21:14:00+01:00",
"lines": [
{ "itemId": "PILS25", "name": "Pils 25 cl", "qty": 2, "unitPrice": 3.20, "barcode": "5410228142218" },
{ "itemId": "MOJ", "name": "Mojito", "qty": 1, "unitPrice": 9.50 }
]
}
]
}3. Fields
| Field | Required | Description |
|---|---|---|
ticketId | yes | Unique id of the ticket in the till. Sending the same ticket again books nothing twice. |
type | no | sale (default), refund or void (cancels a whole ticket). |
occurredAt | no | ISO 8601 time of the sale, with time zone. Empty = now. |
refundOf | no | For refund or void: the original ticketId. |
lines[].itemId | one of three | Item id in the till (PLU, SKU). Stokyo remembers which product it is linked to. |
lines[].barcode | one of three | EAN code. If it matches a product in Stokyo, it is linked automatically. |
lines[].name | one of three | Name as on the ticket. Used for suggestions when linking. |
lines[].qty | yes | Quantity, greater than 0. Also positive for a refund. |
lines[].unitPrice | no | Selling price per unit, for information. |
4. Signing (HMAC-SHA256)
Every request carries a header Stokyo-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body> using the secret key. Sign exactly the bytes you send. The timestamp may be off by at most 5 minutes, so an intercepted request can’t be replayed later.
Node.js
import crypto from "node:crypto";
const body = JSON.stringify(payload); // send exactly this text
const t = Math.floor(Date.now() / 1000);
const v1 = crypto.createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex");
await fetch(URL, {
method: "POST",
headers: { "Content-Type": "application/json", "Stokyo-Signature": `t=${t},v1=${v1}` },
body,
});Python
import hmac, hashlib, json, time, requests
body = json.dumps(payload, separators=(",", ":"))
t = int(time.time())
v1 = hmac.new(SECRET.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
requests.post(URL, data=body, headers={
"Content-Type": "application/json",
"Stokyo-Signature": f"t={t},v1={v1}",
})Shell (curl)
BODY='{"events":[{"ticketId":"T-1","lines":[{"itemId":"PILS25","qty":1}]}]}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "$URL" -H "Content-Type: application/json" \
-H "Stokyo-Signature: t=$T,v1=$SIG" --data "$BODY"5. Refunds and voids
A refund puts the listed quantities back into stock. A void reverses everything booked for the original ticket. If a void arrives before the ticket itself, Stokyo waits and processes it as soon as the ticket is in.
{ "events": [
{ "ticketId": "1288", "type": "refund", "refundOf": "1287",
"lines": [ { "itemId": "PILS25", "qty": 1 } ] },
{ "ticketId": "1287", "type": "void" }
] }6. Responses
200 means: safely stored. The response tells you what happened:
{ "ok": true, "accepted": 1, "duplicates": 0, "booked": 1, "needsMapping": 0, "waiting": 0, "queuedForRetry": 0 }duplicates: tickets that were already in (nothing booked twice).needsMapping: tickets with items Stokyo doesn’t know yet. They appear in the app under Still to link and are booked as soon as the owner links them.queuedForRetry: not processed for now; Stokyo retries by itself.
| Code | Meaning | What to do |
|---|---|---|
| 401 | Signature missing, wrong or too old | Check the key and the clock. Don’t resend without a change. |
| 404 | Unknown address | Copy the address from Stokyo again. |
| 400 / 422 | Invalid JSON or wrong fields (details says which) | Fix the body. |
| 413 | Too large (over 100 events or 1 MB) | Split it up. |
| 503 | Temporarily unavailable | Retry, e.g. after 1, 5 and 30 minutes. Sending twice does no harm. |
7. Good to know
- Composite products (a cocktail or a menu) are set up in Stokyo itself: one till item can reduce several stock products.
- Creating a new key in Stokyo makes the old one invalid straight away.
- Questions, or suggesting a direct connection: contact@stokyo.app.