Agent skill
Snipcart Webhooks Skill
Receive and verify Snipcart webhooks (snipcart.com, the HTML/JS embeddable shopping cart). Use when setting up a Snipcart webhook handler, debugging the X-Snipcart-RequestToken callback validation, or handling events like order.completed, order.status.changed, order.paymentStatus.changed, order.refund.created, v3/subscription.invoice.payment.succeeded, or the synchronous shippingrates.fetch and taxes.calculate webhooks. Snipcart does NOT sign payloads — there is no HMAC and no signature header; authenticity is proved by calling Snipcart's request-validation API with your secret API key.
Install this skill
npx skills add hookdeck/webhook-skills --skill snipcart-webhooks
When to Use This Skill
- How do I receive Snipcart webhooks?
- How do I verify a Snipcart webhook is authentic? (there is no signature — read below)
- What is the
X-Snipcart-RequestTokenheader and how do I validate it? - Why does
https://app.snipcart.com/api/requestvalidation/{token}return 404 or 401? - How do I handle
order.completedorv3/subscription.invoice.payment.succeeded? - How do I implement the
shippingrates.fetch/taxes.calculatewebhooks?
Verification (core) — callback validation, NOT HMAC
Snipcart does not sign webhook payloads. There is no signature header, no HMAC, no shared webhook secret and no timestamp header. Every outbound request instead carries a random token in X-Snipcart-RequestToken, valid for one hour, which you prove genuine by calling Snipcart's API with your secret API key (HTTP Basic, key as the username, trailing colon, no password). 200 = genuine.
const PATTERN = /^[A-Za-z0-9_-]{1,128}$/; // token is attacker-controlled: format-check
// BEFORE it reaches the URL ('..' would
// resolve to a different API endpoint)
async function validateRequestToken(token, secretKey) {
if (!secretKey) throw new Error('SNIPCART_SECRET_API_KEY is not set'); // never fail open
const t = (token || '').trim();
if (!PATTERN.test(t)) return false; // missing/malformed: reject, no API call
const res = await fetch(`https://app.snipcart.com/api/requestvalidation/${t}`, {
headers: {
Authorization: `Basic ${Buffer.from(`${secretKey}:`).toString('base64')}`,
Accept: 'application/json',
},
redirect: 'manual', // never forward the secret key to a redirect
signal: AbortSignal.timeout(5000),
}).catch(() => null); // network error/timeout -> fail closed
return res?.status === 200; // 404 = unknown/expired/already validated
} // 401 = secret key missing/wrong (per Snipcart support)
Only after a 200 should you JSON.parse the raw body and dispatch. Respond 200 with a JSON body — Snipcart requires Content-Type: application/json and status 200.
For complete handlers with route wiring, event dispatch, the synchronous shipping/taxes webhooks, and tests, see:
Event Envelope
Every webhook shares the same envelope. There is no event id and no delivery id — for idempotency, key on the order token + eventName (+ createdOn). The order token is content.token on Order-content events, content.orderToken on refund / notification / withdrawal events, and content.order.token / content.subscription.id on subscription events.
{
"eventName": "order.completed",
"mode": "Live",
"createdOn": "2026-09-28T14:03:11.000Z",
"content": { }
}
Snipcart may add new fields to payloads at any time without notice or a new version; existing fields are not renamed or removed. Ignore unknown fields — do not apply strict schema validation.
Common Event Types
| Event | Fires when | content |
|---|---|---|
order.completed | A new order is completed | Order |
order.status.changed | Order status changes (adds top-level from / to) | Order |
order.paymentStatus.changed | Payment status changes (adds top-level from / to) | Order |
order.trackingNumber.changed | Tracking set (adds top-level trackingNumber / trackingUrl) | Order |
order.refund.created | Order refunded | Refund |
order.notification.created | Notification added to an order | Notification |
order.withdrawal.created | EU withdrawal request submitted | Withdrawal |
v3/subscription.invoice.payment.succeeded | Recurring subscription payment succeeds | { order, subscription } |
v3/subscription.invoice.payment.failed | Recurring subscription payment fails | { order, subscription } |
v3/subscription.state.cancellationRequested | A merchant or customer cancels (stays in this state until the end of the billing cycle) | { subscription } |
v3/subscription.state.cancelled | Subscription is cancelled | { subscription } |
The
v3/prefix is part of the event name string — do not strip it. Subscription payment events do not fire for the first payment, only recurring ones.
Synchronous webhooks (configured separately, response body consumed at checkout): shippingrates.fetch → respond {"rates":[…]}; taxes.calculate → respond {"taxes":[…]}. See references/overview.md.
Environment Variables
SNIPCART_SECRET_API_KEY=your_secret_api_key # a SECRET key created in your merchant dashboard (never the public key)
Keys are per-mode: a Test-mode key cannot read Live data and vice versa. Configure the key matching the mode of the webhooks you receive (the payload's mode is "Test" or "Live").
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 snipcart --path /webhooks/snipcart
Hookdeck's SNIPCART source type takes the Snipcart secret API key and performs this same token validation at ingestion. If Hookdeck verifies the token for you, your destination should not re-validate the same token (it may already be consumed, and will be expired on a retry more than an hour later) — verify the x-hookdeck-signature header at the destination instead.
shippingrates.fetch and taxes.calculate must point directly at your app: Hookdeck cannot synchronously return a destination's response to the client.
Reference Materials
- references/overview.md - Snipcart webhook concepts, full event list, payloads
- references/setup.md - Dashboard configuration and API key
- references/verification.md - Request-token validation details and gotchas