Skip to main content
Webhooks are part of the upcoming Spree 5.3 release slated for January 2026.

Overview

Webhooks allow your Spree store to send real-time HTTP POST notifications to external services when events occur. When an order is completed, a product is updated, or inventory changes, Spree can automatically notify your CRM, fulfillment service, analytics platform, or any other system. Webhooks are built on top of Spree’s event system, providing:
  • Multi-store support - Each store has its own webhook endpoints
  • Event filtering - Subscribe to specific events or patterns with wildcards
  • Secure delivery - HMAC-SHA256 signatures for payload verification
  • Automatic retries - Failed deliveries retry with exponential backoff
  • Full audit trail - Track every delivery attempt with response codes and timing

How Webhooks Work

Event Published → WebhookEventSubscriber → Find Matching Endpoints → Queue Delivery Jobs → HTTP POST with Signature
  1. An event is published (e.g., order.completed)
  2. The WebhookEventSubscriber receives all events
  3. It finds active webhook endpoints subscribed to that event
  4. For each endpoint, it creates a WebhookDelivery record and queues a job
  5. The job sends an HTTP POST request with the event payload and HMAC signature

Creating Webhook Endpoints

Via Admin Panel

Navigate to Settings → Developers → Webhooks in the admin panel to create and manage webhook endpoints.

Via Code

endpoint = Spree::WebhookEndpoint.create!(
  store: Spree::Store.default,
  url: 'https://example.com/webhooks/spree',
  subscriptions: ['order.*', 'product.created'],
  active: true
)

# The secret_key is auto-generated
endpoint.secret_key # => "a1b2c3d4e5f6..." (64-character hex string)

Endpoint Attributes

AttributeTypeDescription
urlStringThe HTTPS endpoint URL to receive webhooks
activeBooleanEnable/disable delivery to this endpoint
subscriptionsArrayEvent patterns to subscribe to
secret_keyStringAuto-generated key for HMAC signature verification
store_idIntegerThe store this endpoint belongs to

Event Subscriptions

The subscriptions attribute controls which events trigger webhooks to this endpoint.

Subscribe to Specific Events

endpoint.subscriptions = ['order.completed', 'order.canceled']

Subscribe to Event Patterns

Use wildcards to subscribe to multiple related events:
# All order events
endpoint.subscriptions = ['order.*']

# All creation events
endpoint.subscriptions = ['*.created']

# Multiple patterns
endpoint.subscriptions = ['order.*', 'payment.*', 'shipment.shipped']

Subscribe to All Events

Leave subscriptions empty or use * to receive all events:
endpoint.subscriptions = []    # All events
endpoint.subscriptions = ['*'] # All events (explicit)

Webhook Payload

Each webhook delivery sends a JSON payload with the following structure:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "order.completed",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 123,
    "number": "R123456789",
    "state": "complete",
    "total": "99.99",
    "item_count": 3
  },
  "metadata": {
    "spree_version": "5.1.0"
  }
}
FieldDescription
idUnique UUID for this event
nameEvent name (e.g., order.completed)
created_atISO8601 timestamp when event occurred
dataSerialized resource data
metadataAdditional context including Spree version

HTTP Request Details

Headers

Each webhook request includes these headers:
HeaderDescription
Content-Typeapplication/json
User-AgentSpree-Webhooks/1.0
X-Spree-Webhook-EventEvent name (e.g., order.completed)
X-Spree-Webhook-SignatureHMAC-SHA256 signature for verification

Verifying Webhook Signatures

To ensure webhooks are genuinely from your Spree store, verify the signature:
# In your webhook receiver
def verify_webhook(request)
  payload = request.body.read
  signature = request.headers['X-Spree-Webhook-Signature']
  secret_key = ENV['SPREE_WEBHOOK_SECRET'] # Your endpoint's secret_key

  expected = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)

  ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end
Example in a Rails controller:
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def receive
    unless verify_signature
      head :unauthorized
      return
    end

    event = JSON.parse(request.body.read)

    case event['name']
    when 'order.completed'
      handle_order_completed(event['data'])
    when 'product.updated'
      handle_product_updated(event['data'])
    end

    head :ok
  end

  private

  def verify_signature
    payload = request.body.read
    request.body.rewind

    signature = request.headers['X-Spree-Webhook-Signature']
    expected = OpenSSL::HMAC.hexdigest('SHA256', ENV['SPREE_WEBHOOK_SECRET'], payload)

    ActiveSupport::SecurityUtils.secure_compare(signature.to_s, expected)
  end
end

Delivery Status & Retries

Automatic Retries

Failed webhook deliveries automatically retry up to 5 times with exponential backoff. This handles temporary network issues and endpoint downtime.

Checking Delivery Status

endpoint = Spree::WebhookEndpoint.find(id)

# Recent deliveries
endpoint.webhook_deliveries.recent

# Filter by status
endpoint.webhook_deliveries.successful
endpoint.webhook_deliveries.failed
endpoint.webhook_deliveries.pending

# Filter by event
endpoint.webhook_deliveries.for_event('order.completed')

Delivery Attributes

AttributeDescription
event_nameName of the event delivered
payloadComplete webhook payload sent
response_codeHTTP status code (nil if pending)
successBoolean indicating 2xx response
execution_timeDelivery time in milliseconds
error_type'timeout', 'connection_error', or nil
request_errorsError message details
response_bodyResponse from endpoint (truncated)
delivered_atTimestamp of delivery attempt

Configuration

Enabling/Disabling Webhooks

Webhooks are enabled by default. To disable globally:
# config/initializers/spree.rb
Spree::Api::Config.webhooks_enabled = false

SSL Verification

SSL verification is enabled by default in production. In development, it’s disabled to allow testing with self-signed certificates:
# config/initializers/spree.rb
Spree::Api::Config.webhooks_verify_ssl = true  # Force SSL verification
Spree::Api::Config.webhooks_verify_ssl = false # Disable (not recommended for production)

Available Events

Webhooks can subscribe to any event in Spree’s event system. See Events for a complete list. Common webhook events include:
EventDescription
order.completedOrder checkout finished
order.canceledOrder was canceled
order.paidOrder is fully paid
shipment.shippedShipment was shipped
payment.paidPayment was completed
product.createdNew product created
product.updatedProduct was modified
customer.createdNew customer registered

Testing Webhooks

In Development

Use tools like ngrok or webhook.site to test webhooks locally:
# Create a test endpoint
endpoint = Spree::WebhookEndpoint.create!(
  store: Spree::Store.default,
  url: 'https://your-ngrok-url.ngrok.io/webhooks',
  subscriptions: ['order.*'],
  active: true
)

# Trigger an event
order = Spree::Order.complete.last
order.publish_event('order.completed')

# Check delivery
endpoint.webhook_deliveries.last.success?

In Tests

RSpec.describe 'Webhook delivery' do
  let(:store) { create(:store) }
  let(:endpoint) { create(:webhook_endpoint, store: store, subscriptions: ['order.completed']) }
  let(:order) { create(:completed_order_with_totals, store: store) }

  it 'delivers webhook when order completes' do
    stub_request(:post, endpoint.url).to_return(status: 200)

    expect {
      order.publish_event('order.completed')
    }.to have_enqueued_job(Spree::WebhookDeliveryJob)
  end
end

Best Practices

Respond quickly

Return a 2xx response as fast as possible. Process webhook data asynchronously in a background job.

Verify signatures

Always verify the X-Spree-Webhook-Signature header to ensure the webhook is authentic.

Handle duplicates

Use the event id to detect and handle duplicate deliveries. Webhooks may be retried.

Subscribe selectively

Only subscribe to events you need. Use specific patterns rather than * when possible.

Troubleshooting

Webhooks Not Delivering

  1. Check that webhooks are enabled: Spree::Api::Config.webhooks_enabled
  2. Verify the endpoint is active: endpoint.active?
  3. Confirm the endpoint subscribes to the event: endpoint.subscribed_to?('order.completed')
  4. Check the event has a store_id matching the endpoint’s store

Signature Verification Failing

  1. Ensure you’re using the raw request body (not parsed JSON)
  2. Verify you’re using the correct secret_key for this endpoint
  3. Check that no middleware is modifying the request body

Deliveries Failing

Check the delivery record for details:
delivery = endpoint.webhook_deliveries.failed.last
delivery.error_type      # => 'timeout' or 'connection_error'
delivery.request_errors  # => Error message
delivery.response_code   # => HTTP status code
delivery.response_body   # => Response from your endpoint