# 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**.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/spree-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/spree-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/spree-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/spree-webhooks/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.*`.

| 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

```bash
# 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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/spree-webhooks/references/overview.md) - How Spree webhooks work, events, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/spree-webhooks/references/setup.md) - Create an endpoint and get the `secret_key`
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/spree-webhooks/references/verification.md) - Signature verification details and gotchas