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.acceptedororder.completedevents? - How do I react to
order.refund.succeeded,item.out_of_stockorpayment_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.failedhave nodatawrapper:order,customer,venueandrefundsit besideevent.- The legacy
order.createdsends the bare order object as the body, with noeventat all. The examples recognise it by its order fields (id,orderType,items). Subscribe toorder.created.v2for 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:
| Event | Triggered When |
|---|---|
order.created.v2 | A new order is placed (use this for new integrations) |
order.created | Legacy version of the same event, with the bare order as body |
order.accepted / order.rejected | An order is accepted or rejected at a store with Auto-accept off |
order.canceled | An order is canceled |
order.preparing | An order moves to preparing |
order.ready_for_pickup | An order is marked ready for pickup |
order.out_for_delivery | A delivery order is dispatched |
order.completed | An order is marked completed |
order.pos.dispatch.failed | An order could not be sent to the POS |
order.rating.updated | A customer submits or updates a rating |
order.refund.created / .succeeded / .failed | A refund is issued, then confirmed or rejected by the gateway |
store.opened / store.closed | A store opens (or a snooze ends) or closes (or is snoozed) |
store.opening_ | Opening hours change |
item.out_of_stock | A menu item becomes unavailable |
payments.payout.created | A payout is sent to your bank account |
bill.payment.created | A bill payment is created (no payload shown in the docs) |
payment_link.created / payment_link.paid | A payment link is created or paid |
payment_ / .succeeded / .failed | A payment link refund is initiated, then confirmed or rejected |
printer.status.offline / printer.status.online | A 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
- references/overview.md - Storekit webhook concepts, events, payload shapes, delivery rules
- references/setup.md - Enable webhooks, add an endpoint, get the secret, retry and replay
- references/verification.md - Svix verification details, header fallback, manual verifier, timestamp window