Webhooks

Coming Soon

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.

Beta Notice: Webhook support is currently in development. The event types and payload formats described below represent our planned implementation. We'll announce availability via email and in-app notification.

How Webhooks Work

1

Register your endpoint

Configure a webhook URL in your API Key settings. Your server must accept HTTPS POST requests.

2

Events occur

When relevant events happen (order status change, package arrival, etc.), we queue a webhook delivery.

3

We send the payload

An HTTP POST request with a JSON payload is sent to your endpoint, signed with your webhook secret.

4

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:

HeaderDescription
X-Webhook-IdUnique delivery ID (for deduplication)
X-Webhook-TimestampUnix timestamp of the delivery attempt
X-Webhook-SignatureHMAC-SHA256 signature for verification
Content-Typeapplication/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', 200

Event 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:

AttemptDelayTime after first attempt
1st retry1 minute~1 min
2nd retry5 minutes~6 min
3rd retry30 minutes~36 min
4th retry2 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.