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-Signature header?
  • How do I handle order.placed, order.paid or fulfillment.fulfilled events?
  • 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_WEBHOOK_TOLERANCE_SECONDS=300 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_endpoints. 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_at, data, metadata }; the record is in data. Spree has about 250 events; subscribe with patterns such as order.*.

EventTriggered When
order.placedA customer completed checkout
order.paidThe order is fully paid
order.canceledThe order was canceled
payment.completedA payment succeeded
fulfillment.fulfilledOne parcel went out
user.createdA new customer registered
product.updatedA product changed
webhook.testSynthetic 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →