Receber por email
← Voltar para o catálogo

spree-events-webhooks

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-

34estrelas
Atualizado há 5 meses

Ver no GitHub ↗ Licença: MIT

Como adicionar

/plugin marketplace add OrcaQubits/agentic-commerce-skills-plugins

O comando exato pode variar conforme o repositório. Confira o README no GitHub.

Para o autor da skill

Cole no README do seu repo

Mostra que sua skill está catalogada na Skillteca, gera backlink e tráfego rastreável.

[![Skillteca](https://www.skillteca.com.br/api/badge/spree-events-webhooks/svg)](https://www.skillteca.com.br/skills/spree-events-webhooks)

Alerta por categoria

Receba novas skills de Design e Frontend toda segunda

1 email curto, só com as skills novas de Design e Frontend. 4 minutos de leitura, sem spam, cancela com 1 clique.

Você confirma o email no primeiro envio. Sem spam. Cancela com 1 clique.

Ler descrição completa

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:

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

Or via publishes_lifecycle_events:

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

Subscribing

# 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:

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

Wildcards

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:

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)

def verify_signature(body, signature_header, secret)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
  ActiveSupport::SecurityUtils.secure_compare(expected, signature_header.sub(/^sha256=/, ''))
end
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.

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:

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:

# 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)

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)

// 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 p
Compartilhar X LinkedIn