Webhooks
Webhooks allow your application to receive real-time notifications when events occur in the system. Configure webhook endpoints through the UI to start receiving events.
Webhook Types
By default, all webhooks are asynchronous unless otherwise specified in the event documentation. There are two types:
- Asynchronous webhooks: Fire-and-forget events with retry support. Used for notifications after actions complete.
- Synchronous webhooks: Block the operation until a response is received. Can abort the operation by returning an error. No retries are performed.
Authentication
All webhook requests include a signature for verification. The signature is generated using HMAC-SHA256 with your webhook signature key.
Headers
Every webhook request includes this headers:
- Content-Type: application/json
- x-webhook-signature: Base64-encoded HMAC-SHA256 signature
- x-webhook-timestamp: Unix timestamp (seconds) when the webhook was sent
- x-webhook-event: The event type (e.g., payment_processing, order_created)
Request Format
- Method: POST
- Timeout: 20 seconds
- Body: JSON payload specific to each event type. The event type is also included in the body.
Response
Your endpoint must return:
- HTTP 200: Success - webhook processed correctly
- Any other status: Will trigger a retry (for asynchronous webhooks) or abort the operation (for synchronous webhooks)
Retry Policy
Failed asynchronous webhooks are retried up to 3 times with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: After 30 seconds
- Attempt 3: After 60 seconds
- Attempt 4: After 120 seconds
Synchronous webhooks are not retried. A non-200 response will abort the operation immediately.
Error Handling
- Ensure your webhook endpoint responds within 20 seconds
- Return HTTP 200 as quickly as possible, process asynchronously if needed
- Non-200 responses or timeouts will trigger retries (for asynchronous webhooks only)
- All webhook attempts (successful and failed) are logged for debugging
Testing Webhooks
We provide a test endpoint to help you validate your webhook implementation before going live:
Endpoint: POST /webhooks/test.json
This endpoint:
- Accepts any JSON payload
- Validates the webhook signature if headers are provided
- Stores the request for inspection in the UI
- Returns validation results immediately
Use this endpoint to:
- Test your signature calculation
- Verify header formatting
- Debug payload issues
- Ensure your implementation is correct before receiving live webhooks
Best Practices
- Always verify signatures to ensure requests come from our servers
- Use HTTPS for your webhook endpoints
- Store your signature key securely and rotate it periodically
- Implement idempotency - the same webhook may be delivered multiple times
- Respond quickly - acknowledge receipt immediately, then process asynchronously
- Test first - use the test endpoint to validate your implementation
Examples
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
function verifyWebhookSignature(payload, signature, timestamp, secretKey) {
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(timestamp + payload)
.digest('base64');
return signature === expectedSignature;
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const data = JSON.parse(payload);
console.log('Webhook received:', data);
res.status(200).send('OK');
});
app.listen(3000);
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"os"
)
func verifyWebhookSignature(payload, signature, timestamp, secretKey string) bool {
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(timestamp + payload))
expectedSignature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature == expectedSignature
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
signature := r.Header.Get("x-webhook-signature")
timestamp := r.Header.Get("x-webhook-timestamp")
secretKey := os.Getenv("WEBHOOK_SECRET")
if !verifyWebhookSignature(string(body), signature, timestamp, secretKey) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the webhook
var data map[string]interface{}
json.Unmarshal(body, &data)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":3000", nil)
}