instxnt fires webhooks for order lifecycle events. Each delivery is HMAC-signed, retried on failure, and idempotency-keyed so you can safely deduplicate.
| Event | Fires when |
|---|---|
order.paid | Stripe confirms payment for an order. The buyer's card has cleared. |
order.fulfilled | Order has been forwarded to a fulfillment provider (Printify, CJ Dropshipping) or marked as shipped manually. |
order.refunded | Order has been fully refunded via Stripe. |
order.partially_refunded | Order has been partially refunded. Includes the refunded amount. |
order.dispute_opened | Stripe has informed us of a chargeback / dispute on this order. |
store.published | A draft store has gone live (subdomain is now serving the storefront). |
All events share the same envelope:
{
"id": "evt_01HXXXXXXXXXXXXXXX", // unique event id
"type": "order.paid",
"created_at": "2026-04-26T14:23:01.123Z",
"store_id": "st_abc123",
"data": {
"order": {
"id": "or_xyz789",
"stripe_payment_intent": "pi_3Q...",
"total_cents": 3000,
"currency": "USD",
"buyer": { "email": "buyer@example.com", "name": "Jane D." },
"shipping_address": { "line1": "...", "city": "...", "country": "US" },
"items": [
{ "product_id": "pr_...", "title": "Magnetic Phone Mount", "quantity": 1, "unit_price_cents": 3000 }
]
}
}
}Use idfor idempotency. instxnt may retry the same event after a network blip; if you've seen the id before, ignore.
Every webhook includes an X-Instxnt-Signature header containing an HMAC-SHA256 of the raw request body, signed with the secret you configured for the endpoint.
Format: t=<unix_timestamp>,v1=<hex_signature>
import crypto from 'node:crypto';
function verify(req: Request, rawBody: string, secret: string) {
const header = req.headers.get('x-instxnt-signature') || '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (!parts.t || !parts.v1) return false;
// Reject events older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - Number(parts.t);
if (age > 300) return false;
const signed = `${parts.t}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
// Constant-time compare
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
header = headers.get("X-Instxnt-Signature", "")
parts = dict(p.split("=") for p in header.split(","))
if not parts.get("t") or not parts.get("v1"):
return False
if int(time.time()) - int(parts["t"]) > 300:
return False
signed = f"{parts['t']}.{raw_body.decode()}".encode()
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])export async function verify(req: Request, rawBody: string, secret: string): Promise<boolean> {
const header = req.headers.get('x-instxnt-signature') ?? '';
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (!parts.t || !parts.v1) return false;
if (Math.floor(Date.now() / 1000) - Number(parts.t) > 300) return false;
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const signed = new TextEncoder().encode(`${parts.t}.${rawBody}`);
const sig = Uint8Array.from(parts.v1.match(/.{2}/g)!.map(h => parseInt(h, 16)));
return crypto.subtle.verify('HMAC', key, sig, signed);
}Always verify against the raw request body bytes, not a JSON-parsed-and-restringified version. Whitespace differences will break the signature.
Your endpoint must return a 2xx within 10 seconds. Anything else (3xx, 4xx, 5xx, timeout) triggers a retry.
Retry schedule, with exponential backoff:
After 6 failed attempts the event is marked dead. You can replay dead events from the dashboard at /dashboard/webhooks within 30 days; after that they are purged.
Use a tunnel like ngrok or Cloudflare Tunnel to expose your local server to the internet, then point your webhook endpoint at the tunnel URL.
# expose localhost:3000 over a tunnel
ngrok http 3000
# in the instxnt dashboard, add the ngrok URL as a webhook endpoint:
# https://<random-id>.ngrok.io/api/webhooks/instxnt
# trigger a test event
# (Webhooks → Send test event → "order.paid")id twice. Store seen ids for at least 24 hours.order.paid event. You do not need to subscribe to Stripe directly — listen to instxnt webhooks instead.Need a webhook event we don't have? Write us.
support@instxnt.xyz