Agent skill
Spree Webhooks Skill
Receive and verify Spree Commerce webhooks. Use when setting up Spree webhook endpoint handlers, debugging X-Spree-Webhook-Signature verification, or handling order.placed, order.paid, order.canceled, payment.completed, fulfillment.fulfilled, user.created and product.updated events.
Install this skill
npx skills add hookdeck/webhook-skills --skill spree-webhooks
When to Use This Skill
- How do I receive Spree Commerce (Spree 5/6) webhooks?
- How do I verify the Spree
X-Spree-Webhook-Signatureheader? - How do I handle
order.placed,order.paidorfulfillment.fulfilledevents? - Why is my Spree webhook signature verification failing?
- How do I create a Spree webhook endpoint and get its
secret_key?
Verification (core)
Every delivery is a POST with X-Spree-Webhook-Signature (the bare lowercase hex digest, no prefix) and X-Spree-Webhook-Timestamp (Unix seconds). The signature is HMAC-SHA256, keyed with the endpoint's secret_key (a 64-character hex string used as a UTF-8 string, not hex-decoded), over the bytes "<timestamp>." followed by the raw request body.
const crypto = require('crypto');
function verifySpreeSignature(rawBody, signature, timestamp, secret) {
if (!signature || !timestamp || !secret) return false;
if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding
const signed = Buffer.concat([Buffer.from(`${timestamp}.`, 'latin1'), rawBody]); // bytes, never a re-encoded string
const expected = crypto.createHmac('sha256', secret).update(signed).digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}
Respond 401 for a missing or empty signature or timestamp, a malformed value or a mismatch, and any 2xx for a valid delivery (anything else, a timeout or a connection error counts as a failure). Read the raw body first (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check.
Timestamp window is off by default. Hookdeck retries and replays resend the original headers and timestamp (and Hookdeck applies no tolerance itself), so a fixed window would reject legitimate retries. The examples offer an opt-in: set SPREE_ to enforce one (unset or 0 = off). Use the event id for idempotency instead (see below).
@spree/sdk/webhooks (1.2.1, the current release) exports verifyWebhookSignature(payload, signature, timestamp, secret, toleranceSeconds = 300), which takes the body as a string and rejects timestamps more than 300 seconds old by default (the constructWebhookEvent helper in Spree's docs is only in the 6.0 prerelease). The examples verify the raw bytes directly so they work the same in Express, Next.js and FastAPI.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Setup Note
Spree shows the endpoint secret only once, when you create the webhook endpoint. Paste it here.
Create the endpoint under Settings → Developer → Webhooks in the admin, or with POST /api/v3/admin/webhook_. See references/setup.md.
Common Event Types
Dispatch on the body's name field (also sent as X-Spree-Webhook-Event). The envelope is { id, name, created_; the record is in data. Spree has about 250 events; subscribe with patterns such as order.*.
| Event | Triggered When |
|---|---|
order.placed | A customer completed checkout |
order.paid | The order is fully paid |
order.canceled | The order was canceled |
payment.completed | A payment succeeded |
fulfillment.fulfilled | One parcel went out |
user.created | A new customer registered |
product.updated | A product changed |
webhook.test | Synthetic delivery from send_test |
A re-sent delivery carries the same event id; use it to deduplicate. A failed delivery is tried up to 5 times in total over about six minutes, and 15 consecutive failures disable the endpoint.
Environment Variables
# Endpoint secret_key (64-character hex string, shown only once at creation)
SPREE_WEBHOOK_SECRET=your_spree_webhook_endpoint_secret_key
# Optional: reject deliveries older than N seconds (unset or 0 = off)
SPREE_WEBHOOK_TOLERANCE_SECONDS=0
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 spree --path /webhooks/spree
Use the printed public HTTPS URL as the endpoint url, then call send_test on the endpoint to fire a webhook.test delivery.
Reference Materials
- references/overview.md - How Spree webhooks work, events, payload shape
- references/setup.md - Create an endpoint and get the
secret_key - references/verification.md - Signature verification details and gotchas