Best Practices
Follow these patterns to build a reliable, production-ready webhook integration with Infodeck.
Return 200 Quickly
Your endpoint must return a 200 status code within 29 seconds. If processing takes longer, acknowledge receipt immediately and handle the event asynchronously.
Good -- acknowledge and queue:
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously (queue, background job, etc.)
processEventAsync(event).catch(err => {
console.error(`Failed to process event ${event.id}:`, err);
});
});
Bad -- blocking processing before responding:
// DO NOT do this
app.post('/webhooks', async (req, res) => {
const event = JSON.parse(req.body);
await updateDatabase(event); // 2 seconds
await notifySlack(event); // 3 seconds
await syncExternalSystem(event); // 10 seconds
res.status(200).json({ received: true }); // Too slow!
});
If your endpoint does not respond within 29 seconds, Infodeck treats the delivery as failed and schedules a retry. Slow endpoints will accumulate retries and eventually be marked as "Failing".
Handle Duplicates with Idempotency
Infodeck guarantees at-least-once delivery. In rare cases (network issues, retries), your endpoint may receive the same event more than once. Use the event.id field to deduplicate.
const processedEvents = new Set(); // In production, use Redis or a database
async function handleEvent(event) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log(`Skipping duplicate event: ${event.id}`);
return;
}
// Process the event
await doWork(event);
// Mark as processed
processedEvents.add(event.id);
}
Production-grade approach using a database:
async function handleEvent(event) {
try {
// Use a conditional write to atomically check-and-insert
await db.put({
TableName: 'ProcessedWebhookEvents',
Item: {
eventId: event.id,
processedAt: Date.now(),
ttl: Math.floor(Date.now() / 1000) + 7 * 86400 // 7-day TTL
},
ConditionExpression: 'attribute_not_exists(eventId)'
});
} catch (err) {
if (err.name === 'ConditionalCheckFailedException') {
console.log(`Duplicate event: ${event.id}`);
return; // Already processed
}
throw err;
}
await doWork(event);
}
Retry Behavior
When a delivery fails (your endpoint returns 4xx/5xx or times out), Infodeck retries with exponential backoff:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1st retry | 1 minute | 1 minute |
| 2nd retry | 5 minutes | 6 minutes |
| 3rd retry | 30 minutes | 36 minutes |
| 4th retry | 2 hours | ~2.5 hours |
| 5th retry | 24 hours | ~26.5 hours |
After 5 consecutive failures for a single delivery, that delivery is marked as failed.
If a webhook endpoint accumulates multiple failed deliveries, its status changes to Failing. You will see this in Settings > Webhooks and receive an email notification.
A webhook in "Failing" status continues to receive deliveries. Infodeck does not automatically disable webhooks. Fix the underlying issue and the status will return to "Active" after successful deliveries.
Event Ordering
Infodeck preserves event ordering within the same entity. For example, if an asset is created and then updated, you will always receive asset.created before asset.updated for that specific asset.
However, events across different entities may arrive in any order. Do not assume asset.created for Asset A will arrive before asset.created for Asset B.
Handling out-of-order events:
Use the created timestamp on each event to determine the true order. If you receive an event with an older timestamp than one you have already processed for the same resource, you can safely skip it.
Monitor Delivery Logs
Infodeck keeps delivery logs for 90 days. Check them regularly in Settings > Webhooks > [Your Webhook] > Delivery Logs.
Each log entry shows:
- Event ID and type
- HTTP status code returned by your endpoint
- Response time (ms)
- Request and response headers
- Request body (the event payload)
- Retry count (if applicable)
You can also query delivery logs via the API:
curl https://app.infodeck.io/api/v2/organizations/{orgId}/webhooks/{webhookId}/deliveries \
-H "Authorization: Bearer {token}"
Rotate Secrets Regularly
Rotate your signing secret every 90 days. Use graceful rotation for zero-downtime transitions.
Quick rotation workflow:
- Call the rotate endpoint with
mode: "graceful" - Deploy the new secret to your server
- During the 24-hour overlap, both old and new secrets work
- After 24 hours, the old secret expires automatically
Filter Events at the Source
Only subscribe to the events your integration actually needs. This reduces load on your endpoint and simplifies your handler logic.
# Good -- subscribe to specific events
curl -X POST https://app.infodeck.io/api/v2/organizations/{orgId}/webhooks \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks",
"events": ["workorder.created", "workorder.completed"]
}'
You can update your event subscriptions at any time without rotating your secret:
curl -X PATCH https://app.infodeck.io/api/v2/organizations/{orgId}/webhooks/{webhookId} \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"events": ["workorder.created", "workorder.completed", "workorder.status.changed"]
}'
Error Handling Patterns
Structure your handler to isolate failures and provide useful debugging information:
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify signature
const sig = req.headers['x-infodeck-signature'];
if (!sig || !verifyWebhookSignature(req.body.toString(), sig, SECRET)) {
console.warn('Webhook signature verification failed');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
// 2. Acknowledge receipt
res.status(200).json({ received: true });
// 3. Route and process
handleEvent(event).catch(err => {
console.error({
message: 'Webhook processing failed',
eventId: event.id,
eventType: event.type,
error: err.message,
stack: err.stack
});
// Alert your monitoring system (Datadog, PagerDuty, etc.)
});
});
async function handleEvent(event) {
switch (event.type) {
case 'asset.created':
return handleAssetCreated(event.data.object);
case 'asset.updated':
return handleAssetUpdated(event.data.object, event.data.previousAttributes);
case 'workorder.completed':
return handleWorkOrderCompleted(event.data.object);
case 'webhook.test':
console.log('Test event received successfully');
return;
default:
console.log(`Unhandled event type: ${event.type}`);
}
}
Always include a default case in your event handler. As Infodeck adds new event types, your endpoint should gracefully ignore events it does not recognize rather than throwing errors.
Summary Checklist
| Practice | Why |
|---|---|
| Return 200 within 29 seconds | Prevents unnecessary retries |
| Verify HMAC signature | Confirms authenticity |
Deduplicate with event.id | Handles at-least-once delivery |
| Process asynchronously | Keeps response times fast |
| Subscribe to specific events | Reduces noise and load |
| Monitor delivery logs | Catches failures early |
| Rotate secrets every 90 days | Limits exposure window |
| Handle unknown event types | Future-proofs your integration |
| Use HTTPS only | Required by Infodeck |
| Store secrets in env vars | Keeps credentials out of source code |