Skip to main content

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 30 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', limit: '1mb' }), (req, res) => {
// Verify signature first
// See Webhook Security guide for verifyWebhookSignature() implementation
if (!verifyWebhookSignature(req.body.toString(), req.headers['x-infodeck-signature'], SECRET)) {
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!
});
danger

If your endpoint does not respond within 30 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.

The following in-memory example illustrates the concept. Do not use an in-memory Set in production -- it resets on restart and does not work across multiple server instances. See the database example below for a production-grade approach.

const processedEvents = new Set();

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 over approximately 24 hours. Retry intervals increase from minutes to hours between attempts.

After several 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.

info

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 a work order is created and then updated, you will always receive workorder.created before workorder.updated for that specific work order.

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:

  1. Call the rotate endpoint with mode: "graceful"
  2. Deploy the new secret to your server
  3. During the 24-hour overlap, both old and new secrets work
  4. 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. If one endpoint only serves one site, one contractor stream, or one asset family, add filtering there too.

# 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"],
"filtering": {
"mode": "include",
"criteria": {
"locationId": ["loc_hq_01"]
}
}
}'

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', limit: '1mb' }), (req, res) => {
// 1. Verify signature
const sig = req.headers['x-infodeck-signature'];
// See Webhook Security guide for verifyWebhookSignature() implementation
if (!sig || !verifyWebhookSignature(req.body.toString(), sig, process.env.INFODECK_WEBHOOK_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.status.changed':
return handleAssetStatusChanged(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}`);
}
}
tip

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.

Integration Patterns

Digital Twin / 3rd-Party Platform Sync

Webhooks replace polling for real-time IoT data. Instead of calling GET /assets/:id every 30 seconds for 500 sensors (181M API calls/month), subscribe to asset.telemetry.updated and receive data pushed to you.

Setup:

  1. Create a webhook subscribing to asset.telemetry.updated and asset.status.changed
  2. Fetch asset type definitions once per assetTypeId (for units and labels) and cache them
  3. Process incoming telemetry in real-time
// Cache asset type metadata (units, labels) — fetch once, reuse forever
const assetTypeCache = new Map();

async function getAssetTypeUnits(assetTypeId) {
if (assetTypeCache.has(assetTypeId)) return assetTypeCache.get(assetTypeId);

const res = await fetch(
`https://app.infodeck.io/api/v2/organizations/${ORG_ID}/asset-types/${assetTypeId}`,
{ headers: { 'Authorization': `Bearer ${TOKEN}` } }
);
const { data } = await res.json();

// Build key → unit map: { "Temperature": "°C", "CO2": "ppm" }
const units = {};
for (const prop of data.properties || []) {
units[prop.key] = { name: prop.name, unit: prop.unit, type: prop.type };
}
assetTypeCache.set(assetTypeId, units);
return units;
}

// Webhook handler
app.post('/webhooks', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
// Always verify signature first (see Webhook Security guide)
const sig = req.headers['x-infodeck-signature'];
if (!sig || !verifyWebhookSignature(req.body.toString(), sig, SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}

res.status(200).json({ received: true }); // Acknowledge immediately

const event = JSON.parse(req.body);

if (event.type === 'asset.telemetry.updated') {
const { assetTypeId, name, id } = event.data.object;
const telemetry = event.data.object.lastTelemetry;
const units = await getAssetTypeUnits(assetTypeId);

// Now you have values + units for your digital twin
// e.g., { Temperature: 24.5 } + { Temperature: { unit: "°C" } }
updateDigitalTwin(id, telemetry, units);
}
});

Summary Checklist

PracticeWhy
Return 200 within 30 secondsPrevents unnecessary retries
Verify HMAC signatureConfirms authenticity
Deduplicate with event.idHandles at-least-once delivery
Process asynchronouslyKeeps response times fast
Subscribe to specific eventsReduces noise and load
Monitor delivery logsCatches failures early
Rotate secrets every 90 daysLimits exposure window
Handle unknown event typesFuture-proofs your integration
Use HTTPS onlyRequired by Infodeck
Store secrets in env varsKeeps credentials out of source code

Was this page helpful?