Skip to content
Back to skills

Spree Events Webhooks

ASecurity

Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, buil...

  • 39 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentstypescriptrubynextjsrailsapisecurityperformance

Works with

  • api

Security analysis

A100/100

Scanned May 29, 2026

npx -y skills add OrcaQubits/agentic-commerce-skills-plugins --skill spree-events-webhooks --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Spree Events Webhooks?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Spree Events Webhooks
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/orcaqubits-spree-events-webhooks/badge)](https://www.skillsdirectory.com/skills/orcaqubits-spree-events-webhooks)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: spree-events-webhooks
description: >
  Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication,
  `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching,
  lifecycle events (`{model}.created/.updated/.deleted` via
  `publishes_lifecycle_events`), the canonical event catalog (order.*,
  payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing
  (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job
  orchestration. Use when wiring event-driven business logic, building webhook
  consumers, or replacing ActiveSupport callback chains.
---

# Spree Events & Webhooks

## Before writing code

**Fetch live docs**:
1. Fetch https://spreecommerce.org/docs/developer/core-concepts/events for the event bus.
2. Fetch https://spreecommerce.org/docs/developer/core-concepts/webhooks for Webhooks 2.0 (HMAC, retries).
3. Inspect the live `lib/spree/event.rb` / `lib/spree/subscriber.rb` and `app/subscribers/` in the `spree` gem for the canonical event names per release.
4. Check the v5.4 announcement for any Webhooks 2.0 changes.
5. For verifying signatures, also check the `@spree/sdk` if you're consuming webhooks in TypeScript.

## Conceptual Architecture

### Why an Event Bus?

Spree's event bus (`Spree::Events`) replaces ad-hoc ActiveSupport::Notifications and `after_*` callbacks for cross-cutting concerns. Benefits:
- **Decoupled** — subscribers don't know about each other
- **Testable** — assert on event publication, not on side effects
- **Webhook-friendly** — Webhooks 2.0 piggybacks on the same events
- **Wildcard subscriptions** — `order.*`, `*.created`, `*` for cross-cutting logging

### Publishing Events

In core code:

```ruby
Spree::Bus.publish('order.completed', order: order, user: order.user)
```

Or via `publishes_lifecycle_events`:

```ruby
class Spree::Product < ApplicationRecord
  publishes_lifecycle_events  # auto-emits product.created/.updated/.deleted
end
```

### Subscribing

```ruby
# app/subscribers/order_completed_subscriber.rb
class OrderCompletedSubscriber < Spree::Subscriber
  subscribes_to 'order.completed'

  on 'order.completed', :handle_completed

  def handle_completed(event)
    order = event.order
    AccountingSync.enqueue(order_id: order.id)
  end
end
```

Subscribers in `app/subscribers/` auto-register on app boot. Otherwise:

```ruby
# config/initializers/spree.rb
Spree.subscribers << CustomSubscriber
```

### Wildcards

```ruby
subscribes_to 'order.*'   # all order events
subscribes_to '*.created' # all lifecycle creations
subscribes_to '*'         # everything (use for logging only)
```

### Canonical Event Catalog (verify against live source)

| Domain | Events |
|--------|--------|
| Order | `order.created`, `order.updated`, `order.completed`, `order.canceled`, `order.resumed`, `order.paid`, `order.shipped` |
| Payment | `payment.created`, `payment.updated`, `payment.paid` |
| Shipment | `shipment.created`, `shipment.updated`, `shipment.shipped`, `shipment.canceled`, `shipment.resumed` |
| Product | `product.activate`, `product.archive`, `product.out_of_stock`, `product.back_in_stock` |
| Lifecycle | `{model}.created`, `{model}.updated`, `{model}.deleted` for any model with `publishes_lifecycle_events` |
| Cart | `cart.add_item`, `cart.remove_item`, `cart.update` |
| User | `user.created`, `user.password_reset_requested` |

This list **isn't exhaustive** — releases add events. Always re-check.

### Event Payload Shape

An `Event` object exposes the payload keys as methods:

```ruby
on 'order.completed', :handle
def handle(event)
  event.order        # Spree::Order
  event.user         # Spree::User
  event.firing_class # Spree::Order (the publisher)
end
```

### Webhooks 2.0

Webhooks subscribe to Spree events and forward HMAC-signed POSTs to external URLs. Configured per Store in admin (Settings → Webhooks).

A webhook endpoint declares:
- **URL** — your receiver
- **Event subscriptions** — pick events (e.g., `order.completed`, `payment.paid`)
- **Secret** — used to sign payloads

### Delivery Mechanics

1. Spree event publishes
2. `WebhookEventSubscriber` matches active endpoints
3. For each match, enqueues a Sidekiq job
4. Worker POSTs to the endpoint URL with body `{ event, data, timestamp }`
5. Signs with `X-Spree-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 of body using shared secret)
6. Expects 2xx; otherwise retries with exponential backoff up to 5 attempts
7. After 5 failures, marks the delivery dead-letter for manual replay

### Signature Verification (Consumer Side)

```ruby
def verify_signature(body, signature_header, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
  ActiveSupport::SecurityUtils.secure_compare(expected, signature_header.sub(/^sha256=/, ''))
end
```

```typescript
import { createHmac, timingSafeEqual } from 'crypto';

function verify(body: string, header: string, secret: string) {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const received = header.replace(/^sha256=/, '');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
```

### Idempotency on the Consumer

Spree retries on non-2xx. Make your handler idempotent — keyed by event ID or order ID + state.

## Implementation Guidance

### Designing a Subscriber

Pattern: one subscriber class per concern, not per event.

```ruby
class AnalyticsSubscriber < Spree::Subscriber
  subscribes_to 'order.completed', 'order.canceled', 'product.activate'

  on 'order.completed', :track_purchase
  on 'order.canceled',  :track_cancellation
  on 'product.activate', :track_launch

  private

  def track_purchase(event)
    Analytics.track(
      user_id: event.order.user_id,
      event: 'purchase',
      properties: { revenue: event.order.total }
    )
  end

  def track_cancellation(event)
    Analytics.track(user_id: event.order.user_id, event: 'cancellation')
  end

  def track_launch(event)
    Analytics.track(event: 'product_launched', properties: { id: event.product.id })
  end
end
```

### Async Subscribers

Don't block the request — enqueue Sidekiq jobs:

```ruby
on 'order.completed', :handle

def handle(event)
  EmailJob.perform_later(order_id: event.order.id)
end
```

Webhooks 2.0 are already async via Sidekiq — your custom subscriber doesn't need to re-async unless it's heavy.

### Publishing Custom Events

For extension code:

```ruby
# In a service object
Spree::Bus.publish('my_app.special_discount_applied', order: order, amount: amount)

# Subscribe
class MyAppSubscriber < Spree::Subscriber
  subscribes_to 'my_app.special_discount_applied'
  on 'my_app.special_discount_applied', :log_it
end
```

Use a `my_app.` prefix to avoid collisions with core events.

### Wiring Webhooks 2.0

In admin → Settings → Webhooks:
- URL: `https://your-app.com/webhooks/spree`
- Events: pick from the catalog
- Secret: generated; store in your consumer's env

Verify against the live admin UI — Webhooks 2.0 management may have moved.

### Receiving a Webhook (Rails)

```ruby
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def spree
    body = request.body.read
    unless verify_signature(body, request.headers['X-Spree-Webhook-Signature'], ENV['SPREE_WEBHOOK_SECRET'])
      head :unauthorized and return
    end

    payload = JSON.parse(body)
    case payload['event']
    when 'order.completed' then OrderCompletedHandler.perform_later(payload['data'])
    end

    head :ok
  end
end
```

Respond 2xx **immediately** — process async. Slow handlers hit the retry threshold.

### Receiving a Webhook (Next.js)

```typescript
// app/api/webhooks/spree/route.ts
export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('x-spree-webhook-signature') ?? '';
  if (!verify(body, signature, process.env.SPREE_WEBHOOK_SECRET!)) {
    return new Response('Unauthorized', { status: 401 });
  }
  const payload = JSON.parse(body);
  // Enqueue async — Inngest, Trigger.dev, BullMQ, etc.
  return new Response('OK');
}
```

### Common Pitfalls

- **Subscriber that doesn't enqueue async work** — slows down the request and stalls the bus.
- **Forgetting to register a subscriber** — non-`app/subscribers/` location requires `Spree.subscribers << ...`.
- **Hardcoding event names** — make a constant, since event names occasionally rename across releases.
- **Verifying signature against the wrong secret** — multi-store deployments have a secret per Webhook endpoint, not per Store.
- **Slow webhook receiver** — Spree retries; you get a duplicate-handling problem. Always 2xx fast.
- **Subscribing to `*` in production** — performance hazard. Use for dev/diagnostics only.
- **Assuming event payload shape** — verify the publisher's call site; lifecycle vs custom events have different keys.

Always cross-reference the live `app/subscribers/` directory and the published events in the `spree` gem source — the event taxonomy evolves with new features.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…