Webhooks
jsys signs every webhook with HMAC-SHA-256 and a per-endpoint shared secret. The signature format mirrors Stripe's — easy to verify with any HMAC library.
Event types
order.pending— order accepted, awaiting processingorder.processing— worker started fulfillmentorder.fulfilled— all line items succeededorder.failed— all retries exhausted; wallet auto-refundedorder.cancelled— cancelled while PENDINGorder.refunded— manual refund issuedwallet.topup.confirmed— admin confirmed your wire transferwallet.low_balance.warning— balance entered the 10% buffer above your thresholdwallet.low_balance.critical— balance dropped below your threshold
↳ How to verify a webhook signature — required reading before you point this at a production receiver.
Signature header
X-Jsys-Signature: t=1739644800,v1=8f3a6b…Where the signed payload is {timestamp}.{rawBody} and v1=hex(hmac_sha256(secret, signedPayload)).
Verifying in Node
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 ?? '', 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}Delivery semantics
At-least-once. We retry on any non-2xx response (or transport error) with exponential backoff up to WEBHOOK_MAX_DELIVERY_ATTEMPTS (default 8). Receivers should be idempotent — the envelope's id field is unique per delivery and stable across retries.
Secret rotation
Rotate from Webhooks → endpoint → Rotate secret. The previous secret stays valid for WEBHOOK_SIGNING_SECRET_ROTATION_GRACE seconds (default 24h), so you can roll receivers without downtime.