Agent skill

Storekit Webhooks Skill

Receive and verify Storekit webhooks (restaurant online ordering, payments and in-store tooling, delivered via Svix). Use when setting up Storekit webhook handlers, debugging svix-signature verification, or handling events like order.created.v2, order.accepted, order.refund.succeeded, store.closed, item.out_of_stock, payment_link.paid, and printer.status.offline.

Install this skill

npx skills add hookdeck/webhook-skills --skill storekit-webhooks


Storekit is online ordering, payments and in-store tooling for restaurants: orders, store status, menu availability, payouts, payment links and cloud printers. It delivers webhooks through Svix, which implements Standard Webhooks. Every delivery is an HTTP POST with a JSON body and three signature headers: svix-id, svix-timestamp, and svix-signature. The body is { "event": "<type>", "data": { ... } }, with two documented exceptions covered below.

When to Use This Skill

  • How do I receive Storekit webhooks?
  • How do I verify Storekit webhook signatures (svix-signature)?
  • How do I handle order.created.v2, order.accepted or order.completed events?
  • How do I react to order.refund.succeeded, item.out_of_stock or payment_link.paid?
  • Why is my Storekit webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Svix signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use the official svix library, which compares in constant time. Read the svix-* headers, and when svix-id is absent fall back to the webhook-* headers (webhook-id, webhook-timestamp, webhook-signature), as Hookdeck does.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.STOREKIT_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["STOREKIT_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

The signing secret is per endpoint, starts with whsec_, and comes from the embedded webhooks portal in the Storekit dashboard (Settings > Webhooks). Keep the full whsec_... value in STOREKIT_WEBHOOK_SECRET. Storekit's manual sample strips the prefix before base64-decoding, so the key is the decoded part after whsec_. Handlers fail closed: with the variable unset they return 500 instead of accepting anything. Disable CSRF protection on the webhook route, and always verify the raw body.

Hookdeck retries and replays hit the 5-minute window

The svix library always enforces a fixed 5-minute timestamp window, and it can't be configured. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on svix-id instead of relying on the timestamp.

Payload Shapes

Read the event name from the top-level event field and the resource from data. Two documented exceptions:

  • order.refund.created, order.refund.succeeded, order.refund.failed have no data wrapper: order, customer, venue and refund sit beside event.
  • The legacy order.created sends the bare order object as the body, with no event at all. The examples recognise it by its order fields (id, orderType, items). Subscribe to order.created.v2 for new integrations. Both fire for every new order.

There is no event id in the body. svix-id is the message id and stays the same across retries, so deduplicate on it (keep ids for at least 72 hours).

Common Event Types

27 events are documented (the list may not be exhaustive). The examples handle all of these:

EventTriggered When
order.created.v2A new order is placed (use this for new integrations)
order.createdLegacy version of the same event, with the bare order as body
order.accepted / order.rejectedAn order is accepted or rejected at a store with Auto-accept off
order.canceledAn order is canceled
order.preparingAn order moves to preparing
order.ready_for_pickupAn order is marked ready for pickup
order.out_for_deliveryA delivery order is dispatched
order.completedAn order is marked completed
order.pos.dispatch.failedAn order could not be sent to the POS
order.rating.updatedA customer submits or updates a rating
order.refund.created / .succeeded / .failedA refund is issued, then confirmed or rejected by the gateway
store.opened / store.closedA store opens (or a snooze ends) or closes (or is snoozed)
store.opening_hours.updatedOpening hours change
item.out_of_stockA menu item becomes unavailable
payments.payout.createdA payout is sent to your bank account
bill.payment.createdA bill payment is created (no payload shown in the docs)
payment_link.created / payment_link.paidA payment link is created or paid
payment_link.refund.created / .succeeded / .failedA payment link refund is initiated, then confirmed or rejected
printer.status.offline / printer.status.onlineA cloud printer drops off or reconnects

See references/overview.md for payload fields and delivery rules.

Environment Variables

# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
STOREKIT_WEBHOOK_SECRET=whsec_xxxxx

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 storekit --path /webhooks/storekit

Add the tunnel URL as an endpoint in Settings > Webhooks, copy its signing secret into STOREKIT_WEBHOOK_SECRET, and send a test event from the portal. See references/setup.md.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →