Webhooks
Receive real-time notifications when order, payout, and wallet statuses change
Webhooks let you receive real-time HTTP notifications when events occur on your account - such as an order being delivered or a payout completing. Instead of polling the API, your server receives a POST request with the event data the moment it happens.
Event Catalogue by Vertical
Each vertical fires its own event types - the envelope (id, type, created_at, data) is shared, but the data payload differs.
| Vertical | Events | Page |
|---|---|---|
| Vouchers | order.delivered, order.partially_delivered, order.failed, order.cancelled, wallet.credited, wallet.debited | Voucher Webhooks |
| Topups | topup.delivered, topup.failed, topup.cancelled (plus the underlying (status, sub_status) matrix) | Topup Webhooks |
| eSIM | esim.delivered, esim.failed, esim.cancelled, esim.installed, esim.activated, esim.depleted | eSIM Webhooks |
This page covers the delivery mechanics that apply to all of them: configuration, payload envelope, HTTP headers, retries, and idempotency. For HMAC signing details, see Signature Verification.
How It Works
1. An event occurs (e.g. order delivered)
2. We send a POST request to your registered URL
3. Your server processes the event and returns 2xx
4. If delivery fails, we retry with exponential backoffConfiguration
Webhooks are configured through your client dashboard. For each webhook you provide:
| Field | Description |
|---|---|
| URL | Your HTTPS endpoint that receives events (HTTPS required) |
| Events | Which event types to subscribe to |
You can register multiple webhook URLs, each subscribed to different events. Deliveries are only sent for the events a webhook is subscribed to.
When you create a webhook, Octopus Cards generates a unique signing key for it. View and copy it from that webhook in your dashboard - it's the secret used to verify the X-Signature on every delivery (see Signature Verification). You can rotate the key at any time from the dashboard; the previous key stops working immediately.
Payload Format
Every webhook delivers a JSON payload with a standard envelope:
{
"id": "evt_abc123",
"type": "order.delivered",
"created_at": "2025-01-15T14:30:00Z",
"data": {
// Event-specific fields
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID - use this for idempotent processing |
type | string | Event type (e.g. order.delivered) |
created_at | string | When the event was created (RFC 3339) |
data | object | Event-specific payload (varies by event type) |
HTTP Request
Webhooks are sent as POST requests with these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | octopusrewards.com/webhook-agent |
X-Webhook-ID | Webhook configuration ID |
X-Event-ID | Unique event ID (same as id in payload) |
X-Event-Type | Event type (same as type in payload) |
X-Timestamp | Unix timestamp of when the webhook was sent |
X-Signature | HMAC-SHA256 signature of the payload body |
The signing key is never transmitted in a header. It's used only to compute X-Signature - verify deliveries by recomputing the HMAC with the key from your dashboard.
Delivery & Retries
| Parameter | Value |
|---|---|
| Timeout | 30 seconds |
| Success | Any HTTP 2xx status code |
| Max attempts | 5 attempts total (the initial send + 4 retries) |
| Backoff | Exponential between retries - 1 min, 2 min, 4 min, 8 min |
If all attempts fail, the delivery is marked as permanently failed. You can view delivery history and failure details in your dashboard.
Your endpoint must respond within 30 seconds with a 2xx status code. If it returns a non-2xx status or times out, the delivery is retried. Make sure your webhook handler is fast - offload heavy processing to a background queue.
Idempotency
Each event has a unique id (e.g. evt_abc123), also sent as the X-Event-ID header. The same id is used across retries and delivery recoveries, so the same event may arrive more than once. Always check the event id against your records and skip duplicates to ensure idempotent processing.
// Example: deduplicate by event ID
if alreadyProcessed(event.ID) {
return http.StatusOK // Acknowledge but skip
}
processEvent(event)
markProcessed(event.ID)