Almost every real store talks to other systems: an order management system (OMS), a warehouse (WMS), an ERP, a CRM, a marketing platform. In Spree, the integration surface is the events system — models publish events as things happen, and you react to them without touching core code.
There are two ways to consume events, and they serve different audiences:
| Mechanism | Code lives | Best for |
|---|---|---|
| Subscriber | In your Spree app | Calling external APIs with your own client code, internal side effects, anything needing app context |
| Webhook | In the external system | Letting a third party receive HTTP callbacks — no Ruby in your app, endpoints managed from the admin |
Step 1: Subscribe to a Core Event
When an order completes, send it to the OMS. Generate a subscriber:config/initializers/spree.rb (injected into the existing after_initialize block every Spree app ships with). Subscribers are not auto-discovered; a subscriber that never gets appended to Spree.subscribers is a silent no-op, which is why the generator owns that step (re-runs are idempotent, and each new subscriber appends to the same initializer).
Fill in the handler:
app/subscribers/oms_order_sync_subscriber.rb
- Payloads carry prefixed IDs, not raw database IDs — always look records up with
find_by_prefix_id. The payload itself is the resource serialized with its v3 API serializer, soevent.payload['number'],['email'], etc. are available directly when you don’t need the full record. - Subscribers run async by default — each
handlecall is an ActiveJob on the events queue, so a slow OMS API never blocks checkout. Passsubscribes_to 'order.completed', async: falseonly when you genuinely need synchronous execution.
spree logs worker — or your job backend’s UI).
Step 2: Give Brand Its Own Lifecycle Events
Core models like Payment and Shipment publish*.created / *.updated / *.deleted events automatically. Your models can too — add one line to the Brand model:
app/models/spree/brand.rb
brand.created, brand.updated, and brand.deleted fire after the matching transactions commit — and because you created Spree::Api::V3::BrandSerializer in the API step, the payloads automatically use it (the events system resolves the serializer by naming convention). Anyone — subscriber or webhook — can react to brand changes with the same JSON shape your API serves.
app/subscribers/brand_sync_subscriber.rb
Step 3: Publish a Custom Event
Lifecycle events cover persistence; custom events express domain moments. Say featuring a brand should notify the marketing platform:Step 4: Outbound Webhooks — No Code Required
When the consumer is an external system you don’t deploy code into, use webhooks. In the admin, go to Settings → Webhooks, add an endpoint with the destination URL, and pick the events to deliver —order.completed, brand.created, anything publishing in your store. The endpoint’s signing secret is shown once on creation — store it in the receiving system.
Each delivery is an HTTP POST with this envelope:
| Header | Contents |
|---|---|
X-Spree-Webhook-Event | The event name |
X-Spree-Webhook-Timestamp | Unix timestamp of the delivery |
X-Spree-Webhook-Signature | HMAC-SHA256(secret, "{timestamp}.{body}") |
POST /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveries/:id/redeliver. After 15 consecutive failures the endpoint auto-disables and store staff get an email; a successful delivery resets the counter.
Testing a Subscriber
Subscribers are plain Ruby — testhandle directly with a constructed event:
spec/subscribers/oms_order_sync_subscriber_spec.rb
Related Documentation
- Events — the full event catalog, subscriber DSL, and delivery guarantees
- Webhooks — endpoint management, security, and the per-event payload schemas
- Extending Core Models — when a decorator is the right tool instead

