Sign in

← Integrations

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

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

FieldRequiredDescription
ticketIdyesUnique id of the ticket in the till. Sending the same ticket again books nothing twice.
typenosale (default), refund or void (cancels a whole ticket).
occurredAtnoISO 8601 time of the sale, with time zone. Empty = now.
refundOfnoFor refund or void: the original ticketId.
lines[].itemIdone of threeItem id in the till (PLU, SKU). Stokyo remembers which product it is linked to.
lines[].barcodeone of threeEAN code. If it matches a product in Stokyo, it is linked automatically.
lines[].nameone of threeName as on the ticket. Used for suggestions when linking.
lines[].qtyyesQuantity, greater than 0. Also positive for a refund.
lines[].unitPricenoSelling 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 }
CodeMeaningWhat to do
401Signature missing, wrong or too oldCheck the key and the clock. Don’t resend without a change.
404Unknown addressCopy the address from Stokyo again.
400 / 422Invalid JSON or wrong fields (details says which)Fix the body.
413Too large (over 100 events or 1 MB)Split it up.
503Temporarily unavailableRetry, e.g. after 1, 5 and 30 minutes. Sending twice does no harm.

7. Good to know