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-RequestToken header and how do I validate it?
  • Why does https://app.snipcart.com/api/requestvalidation/{token} return 404 or 401?
  • How do I handle order.completed or v3/subscription.invoice.payment.succeeded?
  • How do I implement the shippingrates.fetch / taxes.calculate webhooks?

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

EventFires whencontent
order.completedA new order is completedOrder
order.status.changedOrder status changes (adds top-level from / to)Order
order.paymentStatus.changedPayment status changes (adds top-level from / to)Order
order.trackingNumber.changedTracking set (adds top-level trackingNumber / trackingUrl)Order
order.refund.createdOrder refundedRefund
order.notification.createdNotification added to an orderNotification
order.withdrawal.createdEU withdrawal request submittedWithdrawal
v3/subscription.invoice.payment.succeededRecurring subscription payment succeeds{ order, subscription }
v3/subscription.invoice.payment.failedRecurring subscription payment fails{ order, subscription }
v3/subscription.state.cancellationRequestedA merchant or customer cancels (stays in this state until the end of the billing cycle){ subscription }
v3/subscription.state.cancelledSubscription 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Sep 28, 2026

View on GitHub →