Chapter 7.6 — Webhooks
1. Purpose
Webhooks are how travoBooks notifies partner systems of events without requiring them to poll. This chapter defines the event catalog, the delivery contract (signatures, retries, ordering, idempotency), and the operational tooling partners get to manage webhook configurations.
2. Why it matters
A reliable webhook system is the contract between travoBooks and partner-side systems. If a payment.succeeded event is silently dropped, the partner's CRM never knows the booking is paid. Reliability properties — at-least-once delivery, signature verification, replay protection, observable failure — turn webhooks from a nice-to-have into a production-grade integration.
3. Event taxonomy
Naming pattern: <resource>.<action> (e.g., booking.issued).
Bookings
booking.draft.createdbooking.heldbooking.pending_paymentbooking.pending_approvalbooking.issuedbooking.cancelled_before_issuebooking.cancelled_after_issuebooking.partially_refundedbooking.completedbooking.expiredbooking.schedule_changed
Tickets / EMDs
ticket.issuedticket.voidedticket.exchangedticket.refundedemd.issuedemd.refunded
Customer / Supplier
customer.createdcustomer.updatedcustomer.credit_limit_changedcustomer.credit_hold_placedcustomer.credit_hold_releasedsupplier.createdsupplier.bank_details_changed
Financial
invoice.generatedinvoice.sentinvoice.paidinvoice.overduepayment.receivedpayment.refundedpayment.failedpayout.initiatedpayout.completedrefund.requestedrefund.completedrefund.rejectedjournal_entry.posted
Memos
memo.received(ADM or ACM)memo.linkedmemo.disputedmemo.settled
Operational
import_run.completedimport_run.failedperiod.soft_closedperiod.closedperiod.reopened
(The full catalog is maintained in code; this list is illustrative.)
4. Event payload
{
"id": "evt_8c2f9...",
"type": "booking.issued",
"created_at": "2026-05-26T18:45:32.123456+06:00",
"partner_id": 42,
"data": {
"booking_id": 1009287,
"booking_ref": "TVB-2026-000123",
"state": "ISSUED",
"amount": "65400.00",
"currency": "BDT",
"customer_id": 4521,
"supplier_id": 17,
"primary_ticket_number": "176-2400000123",
"service_date_start": "2026-05-28",
"issued_at": "2026-05-26T18:45:30.000000+06:00"
},
"meta": {
"api_version": "v1",
"delivery_attempt": 1
}
}
data shape is the same as the corresponding API resource. Adding new fields to data is not breaking; consumers must tolerate unknown fields.
5. Delivery contract
| Property | Behaviour |
|---|---|
| Delivery semantics | At-least-once |
| Ordering | Not guaranteed. Consumers must tolerate out-of-order delivery (use created_at for ordering). |
| Timeout per attempt | 10 seconds |
| Expected response | HTTP 2xx |
Retry on 4xx (excluding 408, 429) |
No (treated as permanent rejection) |
Retry on 5xx, 408, 429 |
Yes with backoff |
| Backoff schedule | 1m, 5m, 30m, 2h, 12h, 24h, 24h, 24h (≈4 days) |
| Maximum attempts | 8 |
| After max attempts | Marked failed; alert to partner; not auto-retried |
6. Signature & verification
Each delivery carries a HMAC-SHA256 signature:
travoBooks-Signature: t=1716729932,v1=8d4e2f...
travoBooks-Webhook-Id: whd_...
travoBooks-Event-Id: evt_...
Computation:
1. travoBooks computes signed_payload = "{timestamp}.{raw_body}".
2. HMAC-SHA256 with the partner's webhook secret as key.
3. Send as v1=<hex>.
Consumer verification:
1. Read t and v1 from header.
2. Reject if |now - t| > 300s (replay protection).
3. Recompute HMAC over t.raw_body and constant-time compare to v1.
4. Reject on mismatch.
Multiple signatures (v1=...,v1=...) allowed during secret rotation. Consumers must accept any.
7. Idempotency
Every delivery has a unique webhook_delivery_id (in header travoBooks-Webhook-Id) and a unique evt_... id. Consumers should dedupe by evt_id — multiple delivery attempts of the same event share the same evt_id.
Consumer handler pattern:
on_webhook(request):
verify_signature(request)
event = parse(request.body)
if already_processed(event.id): # idempotency
return 200
process(event)
mark_processed(event.id)
return 200
8. Subscriptions
Per partner, subscriptions defined via API:
POST /v1/webhooks
{
"url": "https://partner.example/hooks/travobooks",
"description": "CRM sync",
"event_types": ["booking.*", "refund.completed"],
"active": true
}
Wildcards: booking.* matches all booking.<x>.
Multiple subscriptions per partner allowed; same event may be delivered to multiple URLs.
9. Secret management
On creation, a secret is generated (whsec_...); shown once in the response and never returned again. Stored hashed on travoBooks side; revealed plaintext only in initial response.
Rotation: - Partner generates new secret via API. - Returns both old and new secret active simultaneously for a 30-day window. - travoBooks signs with new; can also include legacy signature for verification compatibility. - Old secret revoked after 30 days.
10. Failure handling & resurrection
If 8 attempts fail: - Subscription auto-paused if 100% failure over 30 minutes (protect partner from flood when their endpoint is down). - Email alert to partner_admin. - "Resurrect" API: partner can request redelivery of all failed events in a time window after fixing their endpoint:
POST /v1/webhooks/{id}/redeliver
{ "since": "2026-05-26T00:00:00Z" }
11. Observability for partners
Per subscription, travoBooks exposes: - Last 100 deliveries with status, attempt count, response code, response body (truncated). - Aggregate success rate (7-day, 30-day). - Time-to-first-success and time-to-final-failure distributions.
Surfaced both via UI and API.
12. Sample event flow
13. Database tables touched
| Table | Role |
|---|---|
webhook_endpoints |
Subscriptions |
webhook_secrets |
Hashed secrets + rotation |
webhook_deliveries |
Per-event-per-endpoint record |
webhook_delivery_attempts |
Per attempt detail |
audit_logs |
Subscription create/update/delete |
14. Security considerations
- 🔒 Endpoint URLs must be HTTPS. HTTP rejected at subscription creation.
- 🔒 Domain validation: subscriptions to obvious internal/localhost URLs blocked.
- 🔒 Signature is the primary authn; partners must verify on every event.
- 🔒 Replay window: 5 minutes. Outside that → reject.
- 🔒 travoBooks never includes secrets in payloads.
15. Common pitfalls
- ⚠️ Trusting payloads without signature verification. Anyone could POST to the endpoint.
- ⚠️ Processing events out-of-order assuming order. Use timestamps; treat as eventually-consistent.
- ⚠️ Long-running handlers. 10s timeout. Acknowledge fast; do work async.
- ⚠️ Returning 200 before processing reliably. Risk losing the event. Acknowledge after persisting the event for later processing.
- ⚠️ Not deduplicating. At-least-once means duplicates happen.
- ⚠️ Ignoring 4xx from partner endpoint. A persistent 4xx means consumer-side bug; travoBooks stops retrying — partner must fix and redeliver.
- 🔒 Storing webhook secrets in version control. Use partner's secret manager.