Chapter I - Webhooks Engine
(avr. time for this chapter: 3 to 4 days)
In this chapter, you will build a mountable engine that exposes an API dedicated to receiving webhooks from external services. The engine shares the same database as your ebook store (the MVC application) and processes every webhook asynchronously with Sidekiq. Later in the chapter, the same engine will also send webhooks to sellers.
Continue building upon the application from the previous chapters.
Reference: Rails Guides - Getting Started with Engines
Exercise 1: Webhooks API
Simulate a payment provider (e.g., Stripe) that notifies the ebook store about the state of a purchase payment.
Requirements
- The engine is mountable, API only, lives inside the ebook store repository, and is mounted at
/webhooks - The engine uses the same database as the ebook store
- Every received webhook is stored (provider, event type, external id, payload, status, error, processed at)
- The engine does not reference host classes directly (e.g.,
Purchase); they are configured by the host - Endpoints:
POST /webhooks/payments- receives payment eventsGET /webhooks/health- health check for the provider- Events to support:
payment.succeeded- marks the purchase as paidpayment.failed- marks the purchase as failed and notifies the buyerpayment.refunded- marks the purchase as refunded and reverts the seller commission
Steps to implement:
- Add a payment status to the
Purchasemodel (this change belongs to the host, not to the engine) - The controller must only verify, store the event, enqueue a job, and respond
200 OKas fast as possible - Never run business logic inside the request: providers have short timeouts and retry when your application is slow
Exercise 2: Security and Idempotency
Requirements
- Every request is signed by the provider with an HMAC SHA256 of the raw body, sent in a header
- Requests with an invalid signature are rejected with
401 Unauthorized - The same event can arrive more than once and must never be processed twice (e.g., crediting the seller twice)
- Duplicated events are answered with
200 OKwithout being processed again - Secrets are never committed
Steps to implement:
- Use a constant-time comparison to validate signatures
- Guarantee uniqueness at the database level, not only with validations
- Create a Rake task that sends signed fake events to your local endpoint, to simulate the provider
Exercise 3: Processing with Sidekiq
Requirements
- Jobs live inside the engine but run in the same Sidekiq process as the host
- Events are processed in a dedicated
webhooksqueue - Each event type has its own handler
- Events end up as
processedorfailed(with the error message) - Purchase updates run inside a database transaction
- Existing host jobs are reused when needed (e.g.,
PurchaseNotificationJobafter a successful payment) - Temporary failures (e.g., database lock) are retried; unknown events or non-existent purchases are discarded and marked as
failed - Failed events are re-processed periodically
Exercise 4: Outgoing Webhooks
Now the ebook store also sends webhooks: sellers can register a URL to be notified in real time when one of their ebooks is sold. Your application becomes the provider, and must offer the same guarantees you expected from the payment provider.
Requirements
- Sellers can create, enable, disable, and delete their endpoints (URL and subscribed events)
- Each endpoint has a secret, shown to the seller only once
- The engine exposes a single public method for the host to trigger deliveries, e.g.:
Webhooks.deliver(owner: seller, event: "ebook.sold", payload: { purchase_id: purchase.id }) - Deliveries are sent by a Sidekiq job in a low priority queue, outside the purchase transaction
- Requests are signed (HMAC SHA256 with a timestamp) and include a unique delivery id
- Requests have short timeouts (e.g., 5 seconds)
- Every attempt is stored (status, response body, attempt number)
- Failed deliveries are retried with exponential backoff (e.g., 1 min, 5 min, 30 min, 2 h, 12 h)
- After several consecutive failures, the endpoint is disabled and the seller is notified by email
Note: Discuss with your tutor whether to use Sidekiq retries or manage attempts in the database, and what each option means for visibility and replays.
Reference: Standard Webhooks
Steps to test it manually:
- Use a request inspection service (e.g., webhook.site) as the endpoint URL
- Make a purchase and inspect the received request, headers, and signature
- Point the endpoint to an invalid URL and observe the retries in the Sidekiq Web UI
Exercise 5: Webhooks Administration Page
An API engine can also ship views.
Requirements
- Page at
/webhooks/admin, only for admin users - List incoming events and outgoing deliveries, with filters by status, provider, and event type
- Show the full payload, headers, attempts, and errors of each event or delivery
- "Replay" button that re-processes an event or re-sends a delivery
Exercise 6: Tests
Requirements
- The engine has its own RSpec suite, running against the dummy application
- Request specs for the API: valid signature, invalid signature, duplicated event
- Job specs for event processing and each handler
- Specs for outgoing deliveries with WebMock: signature, success, timeouts,
5xxresponses, endpoint disabled after failures - Integration spec in the host: a
payment.succeededwebhook marks the purchase as paid - The engine's suite runs in your CI pipeline
Checklist
Before moving to the next chapter, ensure you can:
- [ ] Explain the difference between a full engine and a mountable engine
- [ ] Share the host database with an engine
- [ ] Access host models from an engine without hard dependencies
- [ ] Verify webhook signatures with HMAC
- [ ] Explain why webhooks must be idempotent and processed asynchronously
- [ ] Send signed outgoing webhooks with retries and exponential backoff
- [ ] Test an engine in isolation using the dummy application