Concepts
Webhooks
Subscribe to events and verify the signature.
A webhook sends a POST to your URL when something happens in the store, for example order.paid. Webhooks belong to an app, and each topic needs the matching read scope (order topics need read_orders). Managing webhooks needs write_webhooks.
Subscribe
curl https://api.storelyne.com/admin/2026-11/webhooks \
-H "Authorization: Bearer pat_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks","topics":["order.created","order.paid"]}'
The response has the signing secret (whsec_...). It is shown once. URLs must use https and a public address.
What we send
{ "id": "evt_...", "type": "order.paid", "api_version": "2026-11",
"created_at": "2026-11-04T10:00:00.000Z", "store_id": "sto_...", "data": { "id": "ord_...", ... } }
Headers: StoreLyne-Signature, StoreLyne-Event-Id and StoreLyne-Topic. Fetch the full resource from the API when you need more than data. Delivery is at least once: use StoreLyne-Event-Id to ignore repeats.
Verify the signature
StoreLyne-Signature: t=1793830000,v1=5f2a.... Compute HMAC-SHA256 of t + "." + raw body with your secret, compare it with v1 in constant time, and reject timestamps older than 5 minutes.
// Node
const crypto = require('node:crypto');
function verify(secret, rawBody, header) {
const p = Object.fromEntries(header.split(',').map((x) => x.split('=')));
if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
const want = crypto.createHmac('sha256', secret).update(p.t + '.' + rawBody).digest('hex');
return want.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(p.v1));
}
# Python
import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
p = dict(x.split("=", 1) for x in header.split(","))
if abs(time.time() - int(p["t"])) > 300:
return False
want = hmac.new(secret.encode(), p["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, p["v1"])
Retries
Answer with any 2xx within 10 seconds. Anything else is tried again: 15 tries over about 3 days, with growing gaps. If the last try fails, the webhook is paused with the reason; fix the endpoint, resume it, and resend from the delivery log. POST /webhooks/{id}/test sends a webhook.test event at once.