Webhook Best Practices
Return 200 Quickly
Your webhook endpoint should return a 200 status code as fast as possible. Do the heavy processing asynchronously.
app.post('/webhooks/inflow', express.json(), (req, res) => {
// Respond immediately
res.status(200).json({ received: true });
// Process the event asynchronously
processEvent(req.body).catch(console.error);
});If your endpoint doesn't respond with a 200 within a few seconds, the delivery may be considered failed.
Handle Duplicate Events
Your endpoint may receive the same event more than once. Always implement idempotent processing:
async function processEvent(event) {
// Check if you've already processed this event
const existing = await db.webhookEvents.findOne({ eventId: event.id });
if (existing) {
console.log('Event already processed:', event.id);
return;
}
// Process the event
await handlePayment(event.payload);
// Record that you've processed this event
await db.webhookEvents.create({ eventId: event.id, processedAt: new Date() });
}Verifying Webhook Signature
Inflow signs every webhook using Svix. You must verify the signature before processing the payload. You can use the Svix SDK (npm install svix) to verify automatically, or implement verification manually as described below.
Webhook Headers
Each webhook request includes three headers:
| Header | Description |
|---|---|
| svix-id | Unique message identifier. The same when the same webhook is resent (e.g. after a previous failure). |
| svix-timestamp | Timestamp in seconds since the epoch. |
| svix-signature | Base64-encoded list of signatures (space-delimited). |
Constructing the Signed Content
The signed content is the concatenation of id, timestamp, and payload separated by a full stop (.). Do not modify the body before verifying — the signature is sensitive to any change.
// Use the raw request body (e.g. express.raw() in Express, not express.json())
const signedContent = `${headers['svix-id']}.${headers['svix-timestamp']}.${payload}`;Use the raw body as received; we recommend JSON.stringify(payload) only if you need to build the string from a parsed object that matches the exact bytes sent.
Determining the Expected Signature
Inflow uses HMAC with SHA-256. Use the base64 portion of your signing secret (the part after the whsec_ prefix) as the key.
Example: for secret whsec_MfKKr9g8GKYq7wJP0B1PLPZtOzLaLaSw, use MfKKr9g8GKYq7wJP0B1PLPZtOzLaLaSw (decoded from base64 as the HMAC key).
Node.js example:
const crypto = require('crypto');
const signedContent = `${headers['svix-id']}.${headers['svix-timestamp']}.${payload}`;
const secret = 'whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH';
// Decode the base64 portion of the secret
const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
// Generate the HMAC signature
const expectedSignature = crypto
.createHmac('sha256', secretBytes)
.update(signedContent)
.digest('base64');The generated signature should match one of the signatures in the svix-signature header.
Signature Format
The svix-signature header contains space-delimited entries. Each entry has a version prefix and a signature, e.g. v1,<base64>:
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo=
Remove the version prefix (e.g. v1,) before comparing: take the base64 part and check that your computed signature matches one of them.
Verifying the Timestamp
Compare svix-timestamp to your system time and reject the request if it is outside your tolerance (e.g. a few minutes). This prevents replay attacks.
Example (test vectors)
You can use this example to verify your implementation (note: verification may fail if the timestamp is too old).
| Field | Value |
|---|---|
| Secret | whsec_plJ3nmyCDGBKInavdOK15jsl |
| Payload | '{"event_type":"ping","data":{"success":true}}' |
| Message ID | msg_loFOjxBNrRLzqYUf |
| Timestamp | 1731705121 |
| Expected signature | v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0= |
Always reject requests with invalid or missing signatures. Never process unverified webhook payloads.
Handle All Event Types
Even if you only care about specific events, your endpoint should handle unexpected event types gracefully:
const { data } = req.body;
for (const event of data) {
switch (event.eventType) {
case 'payment_created':
case 'payment_status_updated':
handlePayment(event.payload);
break;
default:
console.log('Unhandled event type:', event.eventType);
break;
}
}Always return 200 even for event types you don't process — otherwise Inflow may consider the delivery failed.
Use HTTPS
Your webhook URL must use HTTPS. HTTP endpoints are not supported.
Monitor Webhook Health
Periodically check your webhook status:
curl https://api.inflowpay.xyz/api/webhook/{webhookId}/status \
-H "X-Inflow-Api-Key: inflow_priv_your_key"If the status shows disabled or a failureReason, investigate and fix the issue, then re-enable the webhook.
Recommended Architecture
Inflow Webhook
|
v
[Your Endpoint] --> [Message Queue / Job] --> [Business Logic]
|
v
Return 200
- Receive the webhook event.
- Acknowledge immediately with
200. - Queue the event for asynchronous processing.
- Process the event in a background worker.
This architecture ensures you never miss events due to processing delays.
Checklist
- Endpoint uses HTTPS
- Returns 200 within 5 seconds
- Handles duplicate events (idempotent)
- Processes events asynchronously
- Handles unknown event types gracefully
- Logs all received events for debugging
- Monitors webhook status regularly
Updated 1 day ago