Webhooks for Digital Goods Orders
Short Answer
Webhooks are HTTP push notifications sent from a supplier's system to your server when an order event occurs. For digital goods, the critical events are: order created, order fulfilled, order failed. Instead of polling the order status endpoint repeatedly to check if a code is ready, webhooks notify your server the moment fulfillment completes. This makes delivery faster, reduces API calls and eliminates polling-related delays.
Definition: A webhook for digital goods orders is an HTTP POST request sent by the supplier to your server endpoint when an order changes status β for example, when a code is ready for delivery. Your server receives the event and processes delivery without needing to poll.
Key takeaway: Polling the order status endpoint works at small scale. At high volume, polling hits rate limits and introduces latency. Webhooks are the production-grade solution: one event per order change, delivered as it happens.
Who This Guide Is For
- Developers integrating a digital goods API who need async order handling
- Store operators whose order fulfillment sometimes has a delay (pending orders)
- Anyone building a high-volume digital goods pipeline
Polling vs. Webhooks
| Factor | Polling | Webhooks |
|---|---|---|
| How it works | Your server asks "is the order done?" every N seconds | Supplier notifies your server when status changes |
| Latency | Depends on poll interval | Near-instant |
| API call volume | High (N calls per pending order) | One call per event |
| Rate limit risk | High at scale | None |
| Implementation | Simpler | Slightly more complex (endpoint + validation) |
| Best for | Development/testing | Production |
Webhook Event Types
For a digital goods supplier API, expect these event types:
| Event | When It Fires | Action on Your Server |
|---|---|---|
order.created |
Order received by supplier | Log; start SLA timer |
order.fulfilled |
Code ready; order complete | Deliver code to customer |
order.failed |
Fulfillment failed | Alert ops; trigger refund flow if payment was taken |
order.refunded |
Order reversed by supplier | Update status; notify customer |
balance.low |
Reseller balance below threshold | Trigger balance top-up process |
The most critical event is order.fulfilled β this is the trigger for code delivery.
Webhook Payload Format (Typical)
{
"event": "order.fulfilled",
"timestamp": "2026-05-01T14:23:11Z",
"data": {
"order_id": "SUP-99887",
"reference": "ORD-12345",
"status": "fulfilled",
"items": [
{
"sku": "steam-20-usd",
"code": "XXXXX-YYYYY-ZZZZZ",
"pin": null
}
]
},
"signature": "sha256=abc123..."
}
Always check the reference field β this is your internal order ID, which lets you match the supplier event to your database without storing the supplier's order IDs as primary keys.
Signature Validation
Suppliers sign webhook payloads so you can verify they came from a legitimate source. Without validation, anyone can POST to your endpoint.
HMAC-SHA256 validation (most common):
import hmac
import hashlib
def validate_webhook(payload_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload_body,
hashlib.sha256
).hexdigest()
received = signature_header.replace('sha256=', '')
return hmac.compare_digest(expected, received)
Rules:
- Validate before processing any payload
- Use constant-time comparison (
hmac.compare_digest) to prevent timing attacks - Return HTTP 400 if signature validation fails (do not return 200)
- Return HTTP 200 immediately if signature is valid β before processing the event
Async Processing
Process webhook events asynchronously. Your webhook endpoint must return HTTP 200 within the supplier's timeout window (typically 3β10 seconds). If your event processing takes longer β database writes, email sends, API calls β the supplier may mark the delivery as failed and retry.
Correct architecture:
Webhook endpoint receives POST
β Validate signature
β Return HTTP 200 immediately
β Push event to queue (Redis, SQS, DB queue table)
Background worker
β Pulls event from queue
β Processes: deliver code to customer, update order status
Wrong: Processing delivery inside the webhook handler before returning 200.
Retry Behavior
Suppliers retry webhook delivery if they don't receive HTTP 200 within the timeout. Typical retry schedules:
- 1st retry: 30 seconds after failure
- 2nd retry: 5 minutes
- 3rd retry: 30 minutes
- Beyond: varies (some suppliers retry for 24 hours)
Your idempotency handling must prevent processing the same order twice if a retry fires after you already processed the first attempt. Use the order reference as an idempotency key:
if order_already_delivered(reference):
return HTTP 200 # Acknowledge, do nothing
Fallback: Polling for Missing Events
Webhooks can fail. Your endpoint can go down, or a network issue can prevent delivery. Implement a polling fallback for orders that remain in pending status for more than N minutes:
Scheduled job (every 5 minutes):
β Find all orders with status = 'pending' and age > 5 minutes
β For each: call GET /orders/:id
β If status = fulfilled: process delivery
β If status = failed: alert ops
This ensures no orders are stuck indefinitely due to a missed webhook.
Implementation Checklist
- Register webhook endpoint URL with supplier (typically in API settings)
- Implement POST endpoint that accepts webhook payloads
- Read raw body before JSON parsing (for signature validation on raw bytes)
- Implement signature validation using supplier's algorithm and your shared secret
- Return HTTP 200 immediately after validation passes
- Push event to async queue
- Build background worker that processes events from queue
- Implement idempotency check using order reference
- Implement fallback polling for pending orders
- Test with supplier's test event payload
- Monitor for webhook delivery failures in supplier dashboard
