Webhook Notifications
Webhooks allow your application to receive real-time notifications when events occur on the Fulfillbot platform. Instead of polling the API for status updates, you can register a webhook URL and we'll send HTTP POST requests to your server whenever relevant events happen.
How Webhooks Work
Register your endpoint
Configure a webhook URL in your API Key settings. Your server must accept HTTPS POST requests.
Events occur
When relevant events happen (order status change, package arrival, etc.), we queue a webhook delivery.
We send the payload
An HTTP POST request with a JSON payload is sent to your endpoint, signed with your webhook secret.
You acknowledge
Your server responds with a 2xx status code within 10 seconds. If not, we retry up to 5 times with exponential backoff.
Payload Format
All webhook payloads follow this structure:
{
"id": "whk_01HX1234567890ABCDEF",
"event": "order.status_changed",
"createdAt": "2024-01-15T08:30:00Z",
"data": {
// Event-specific data
}
}Headers included with every webhook:
| Header | Description |
|---|---|
| X-Webhook-Id | Unique delivery ID (for deduplication) |
| X-Webhook-Timestamp | Unix timestamp of the delivery attempt |
| X-Webhook-Signature | HMAC-SHA256 signature for verification |
| Content-Type | application/json |
Signature Verification
Always verify the webhook signature to ensure the request came from Fulfillbot and hasn't been tampered with.
Node.js verification example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
const signedContent = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
// Timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware
app.post('/webhooks/fulfillbot', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
// Reject if timestamp is older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(400).send('Timestamp too old');
}
if (!verifyWebhookSignature(req.body.toString(), signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log(`Received event: ${event.event}`, event.data);
// Process the event...
res.status(200).send('OK');
});Python verification example
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
# Reject old timestamps (replay protection)
if abs(time.time() - int(timestamp)) > 300:
return False
signed_content = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(), signed_content.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhooks/fulfillbot', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
return 'Unauthorized', 401
event = request.json
# Process event...
return 'OK', 200Event Types
Purchase Orders
order.createdA new purchase order was placed{
"id": "whk_01HX...",
"event": "order.created",
"createdAt": "2024-01-15T08:30:00Z",
"data": {
"orderId": "ORD-20240115-001",
"status": "pending",
"totalAmount": 599.00,
"currency": "CNY",
"itemCount": 2
}
}order.status_changedOrder status was updated{
"id": "whk_01HX...",
"event": "order.status_changed",
"createdAt": "2024-01-16T14:00:00Z",
"data": {
"orderId": "ORD-20240115-001",
"previousStatus": "pending",
"newStatus": "purchased",
"updatedAt": "2024-01-16T14:00:00Z"
}
}order.cancelledOrder was cancelled{
"id": "whk_01HX...",
"event": "order.cancelled",
"createdAt": "2024-01-16T10:00:00Z",
"data": {
"orderId": "ORD-20240115-002",
"reason": "Product out of stock",
"refundAmount": 299.00,
"currency": "CNY"
}
}Warehouse
package.arrivedPackage arrived at warehouse and passed inspection{
"id": "whk_01HX...",
"event": "package.arrived",
"createdAt": "2024-01-18T10:00:00Z",
"data": {
"packageId": 101,
"orderId": "ORD-20240115-001",
"weight": 850,
"inspectionResult": "passed",
"photoUrls": ["https://..."]
}
}package.inspection_failedPackage failed quality inspection{
"id": "whk_01HX...",
"event": "package.inspection_failed",
"createdAt": "2024-01-18T10:00:00Z",
"data": {
"packageId": 102,
"orderId": "ORD-20240115-001",
"reason": "Item damaged during shipping",
"photoUrls": ["https://..."]
}
}Shipping
shipping.createdShipping order was created{
"id": "whk_01HX...",
"event": "shipping.created",
"createdAt": "2024-01-20T09:00:00Z",
"data": {
"shippingOrderId": "SHP-20240120-001",
"channelName": "EMS Express",
"totalWeight": 1650,
"shippingFee": 185.00,
"currency": "CNY",
"recipientCountry": "US"
}
}shipping.dispatchedPackage was dispatched from warehouse{
"id": "whk_01HX...",
"event": "shipping.dispatched",
"createdAt": "2024-01-21T15:00:00Z",
"data": {
"shippingOrderId": "SHP-20240120-001",
"trackingNumber": "EMS1234567890CN",
"carrier": "EMS",
"estimatedDelivery": "2024-02-01"
}
}shipping.deliveredPackage was delivered (carrier confirmation){
"id": "whk_01HX...",
"event": "shipping.delivered",
"createdAt": "2024-01-30T12:00:00Z",
"data": {
"shippingOrderId": "SHP-20240120-001",
"trackingNumber": "EMS1234567890CN",
"deliveredAt": "2024-01-30T11:45:00Z"
}
}Wallet
wallet.balance_changedWallet balance was updated (charge, refund, recharge){
"id": "whk_01HX...",
"event": "wallet.balance_changed",
"createdAt": "2024-01-15T08:30:00Z",
"data": {
"type": "charge",
"amount": -599.00,
"currency": "CNY",
"balance": 1401.00,
"description": "Purchase order ORD-20240115-001",
"referenceId": "ORD-20240115-001"
}
}Retry Policy
If your endpoint doesn't respond with a 2xx status code within 10 seconds, we'll retry the delivery with exponential backoff:
| Attempt | Delay | Time after first attempt |
|---|---|---|
| 1st retry | 1 minute | ~1 min |
| 2nd retry | 5 minutes | ~6 min |
| 3rd retry | 30 minutes | ~36 min |
| 4th retry | 2 hours | ~2.5 hours |
| 5th retry (final) | 8 hours | ~10.5 hours |
After 5 failed attempts, the webhook delivery is marked as failed. You can view failed deliveries in the API Key settings and manually replay them.
Best Practices
Respond quickly
Return a 200 immediately, then process the event asynchronously. Don't do heavy processing before responding.
Handle duplicates
Due to retries, your endpoint may receive the same event multiple times. Use the id field for deduplication.
Verify signatures
Always verify the HMAC signature before processing. Reject requests with invalid or missing signatures.
Use HTTPS
Webhook URLs must use HTTPS. We will not deliver to HTTP endpoints.
Check timestamps
Reject webhook deliveries with timestamps older than 5 minutes to prevent replay attacks.