Integration Recipes
Real-world, copy-paste-ready webhook integration patterns that solve actual facility management pain points. Each recipe is designed to be implemented in an afternoon.
Recipe Overview
| Recipe | Pain Point | Solution | Events Used |
|---|---|---|---|
| Slack Alerts | Urgent work orders buried in email | Push Critical/High priority work orders to Slack in real-time | workorder.created, workorder.status.changed |
| Digital Twin Sync | Polling 500+ sensors burns 15M API calls/month | Subscribe to telemetry events, push to BMS in real-time | asset.telemetry.updated |
| Auto-Create Tickets | Manual duplication into ServiceNow/Jira causes sync lag | Auto-create external tickets from Infodeck work orders | workorder.created |
| Multi-Site Sensor Dashboard | No single view of offline sensors across 20 buildings | Real-time sensor status tracking per location | asset.status.changed |
| Procurement Automation | Technicians discover parts shortages on-site | Auto-check stock levels and trigger procurement | workorder.created |
Recipe 1: Push Critical Work Order Alerts to Slack
Pain Point: Facility managers receive dozens of emails daily. Critical work orders get lost in the noise and are discovered hours later -- too late.
Solution: Subscribe to work order creation and status changes, filter by priority, and post actionable alerts to a dedicated Slack channel.
Setup
- Create a Slack incoming webhook at https://api.slack.com/messaging/webhooks
- Copy the webhook URL (e.g.,
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX) - Register an Infodeck webhook endpoint
Node.js Implementation
const express = require('express');
const https = require('https');
const crypto = require('crypto');
const app = express();
// Signature verification (see Webhook Security guide)
function verifyWebhookSignature(payload, header, secret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Post to Slack
function postToSlack(webhookUrl, message) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ text: message });
const req = https.request(new URL(webhookUrl), {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length }
});
req.on('error', reject);
req.on('response', res => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => resolve(body));
});
req.write(data);
req.end();
});
}
app.post('/webhooks/slack-alerts', express.raw({ type: 'application/json' }), async (req, res) => {
// Verify signature
try {
const sig = req.headers['x-infodeck-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch (err) {
return res.status(401).json({ error: 'Signature verification failed' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
handleWorkOrderEvent(event).catch(err => {
console.error(`Error processing event ${event.id}:`, err);
});
});
async function handleWorkOrderEvent(event) {
const { type, data } = event;
const wo = data.object;
// Only alert on Critical and High priority
if (!['Critical', 'High'].includes(wo.priority)) {
return;
}
let message = '';
if (type === 'workorder.created') {
message = `NEW URGENT WORK ORDER\n`;
message += `ID: ${wo.id}\n`;
message += `Title: ${wo.title}\n`;
message += `Priority: ${wo.priority}\n`;
message += `Location: ${wo.location?.name || 'Unknown'}\n`;
message += `Assigned to: ${wo.assignee?.name || 'Unassigned'}\n`;
message += `Link: https://app.infodeck.io/work-orders/${wo.id}`;
} else if (type === 'workorder.status.changed') {
// Only alert if status changed TO Open or Back to Open
if (wo.status !== 'Open') {
return;
}
message = `WORK ORDER REOPENED\n`;
message += `ID: ${wo.id}\n`;
message += `Title: ${wo.title}\n`;
message += `Priority: ${wo.priority}\n`;
message += `Link: https://app.infodeck.io/work-orders/${wo.id}`;
}
if (message) {
await postToSlack(process.env.SLACK_WEBHOOK_URL, message);
}
}
app.listen(3000, () => console.log('Webhook listener running on port 3000'));
Webhook Registration (curl)
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/slack-alerts",
"events": ["workorder.created", "workorder.status.changed"],
"description": "Critical work order Slack alerts",
"filtering": {
"mode": "include",
"criteria": {
"priority": ["Critical", "High"]
}
}
}'
Slack supports rich message formatting. Use the blocks API for better formatting:
const data = JSON.stringify({
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: "*URGENT WORK ORDER*\n" + message }
}
]
});
Recipe 2: Sync IoT Telemetry to a Digital Twin / BMS Platform
Pain Point: Building management teams poll Infodeck's API every 30 seconds for 500+ sensors, consuming 15M+ API calls per month and burning rate limits. Real-time data arrives 30 seconds late.
Solution: Subscribe to asset.telemetry.updated events. Enrich telemetry with asset type definitions (cached locally), and push to your BMS/digital twin platform in real-time.
Architecture
Infodeck IoT Sensors
↓
[webhook endpoint]
↓
[enrich with unit cache]
↓
[push to BMS/digital twin]
Node.js Implementation
const express = require('express');
const crypto = require('crypto');
const app = express();
const INFODECK_API = 'https://app.infodeck.io/api/v2';
// In-memory cache of asset types (refresh every hour)
let assetTypeCache = {};
const CACHE_TTL = 3600 * 1000; // 1 hour
let cacheTimestamp = 0;
function verifyWebhookSignature(payload, header, secret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Refresh asset type cache every hour
async function refreshAssetTypeCache() {
const now = Date.now();
if (now - cacheTimestamp < CACHE_TTL) {
return assetTypeCache; // Cache is fresh
}
try {
const res = await fetch(`${INFODECK_API}/organizations/${process.env.ORG_ID}/asset-types`, {
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
});
const { data } = await res.json();
// Build a map: assetTypeId -> { name, telemetryFields }
assetTypeCache = {};
data.forEach(type => {
assetTypeCache[type.id] = {
name: type.name,
telemetryFields: type.telemetryFields // Array of {name, unit, dataType}
};
});
cacheTimestamp = now;
console.log(`Asset type cache refreshed (${Object.keys(assetTypeCache).length} types)`);
} catch (err) {
console.error('Failed to refresh asset type cache:', err);
// Keep using stale cache if refresh fails
}
return assetTypeCache;
}
// Get the unit for a telemetry field
function getFieldUnit(assetTypeId, fieldName) {
const assetType = assetTypeCache[assetTypeId];
if (!assetType) return null;
const field = assetType.telemetryFields.find(f => f.name === fieldName);
return field ? field.unit : null;
}
app.post('/webhooks/digital-twin-sync', express.raw({ type: 'application/json' }), async (req, res) => {
try {
// Verify signature
const sig = req.headers['x-infodeck-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch (err) {
return res.status(401).json({ error: 'Signature verification failed' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
syncTelemetryToBMS(event).catch(err => {
console.error(`Failed to sync event ${event.id}:`, err);
});
});
async function syncTelemetryToBMS(event) {
const { data } = event;
const { asset, telemetry, timestamp } = data.object;
// Refresh cache if needed
await refreshAssetTypeCache();
// Enrich telemetry with units
const enrichedTelemetry = {};
Object.entries(telemetry).forEach(([key, value]) => {
const unit = getFieldUnit(asset.assetTypeId, key);
enrichedTelemetry[key] = {
value,
unit: unit || 'unknown'
};
});
// Push to your BMS/digital twin platform
const bmsPayload = {
assetId: asset.id,
assetName: asset.name,
locationId: asset.locationId,
timestamp: timestamp || Date.now(),
telemetry: enrichedTelemetry
};
// Example: POST to your BMS API
await fetch(process.env.BMS_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bmsPayload)
});
console.log(`Synced telemetry for asset ${asset.id}`);
}
app.listen(3000, () => console.log('Digital twin sync listening on port 3000'));
Webhook Registration with Location Filter
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/digital-twin-sync",
"events": ["asset.telemetry.updated"],
"description": "Real-time telemetry sync to BMS",
"filtering": {
"mode": "include",
"criteria": {
"locationId": ["loc_hq_01", "loc_branch_02"]
}
}
}'
To scope to a specific site, add a locationId filter as shown above. This reduces webhook volume and ensures your endpoint only receives telemetry from the buildings you care about.
Recipe 3: Auto-Create External Tickets from Work Orders
Pain Point: Operations teams manually duplicate work orders into ServiceNow, Jira, or Freshdesk, causing sync lag and data entry errors. Critical details are missed or entered inconsistently.
Solution: Subscribe to workorder.created, map Infodeck fields to your external ticketing system, and automatically create tickets via their REST API.
Node.js Implementation (Generic)
const express = require('express');
const crypto = require('crypto');
const app = express();
function verifyWebhookSignature(payload, header, secret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Map Infodeck work order to external ticket format
function mapToExternalTicket(wo) {
return {
title: wo.title,
description: wo.description || `Work Order ${wo.id}`,
priority: mapPriority(wo.priority),
assignee: wo.assignee?.email, // Adjust based on external system
location: wo.location?.name,
dueDate: wo.dueDate,
tags: ['infodeck', `wo_${wo.id}`],
customFields: {
infodeck_wo_id: wo.id,
infodeck_org_id: wo.organizationId,
asset_id: wo.asset?.id
}
};
}
// Map priority between systems
function mapPriority(infoddeckPriority) {
const map = {
'Critical': 'highest',
'High': 'high',
'Medium': 'medium',
'Low': 'low'
};
return map[infoddeckPriority] || 'medium';
}
// Create ticket in external system (example: generic REST API)
async function createExternalTicket(ticket) {
const response = await fetch(process.env.EXTERNAL_TICKETING_URL + '/tickets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EXTERNAL_API_KEY}`
},
body: JSON.stringify(ticket)
});
if (!response.ok) {
throw new Error(`External API returned ${response.status}: ${await response.text()}`);
}
const created = await response.json();
return created;
}
// Store mapping between Infodeck and external ticket IDs
async function storeMappings(infoddeckId, externalId) {
// Store in your database
// Example: db.put({ infoddeckId, externalId, syncedAt: Date.now() })
console.log(`Mapped ${infoddeckId} to ${externalId}`);
}
app.post('/webhooks/auto-ticket', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const sig = req.headers['x-infodeck-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch (err) {
return res.status(401).json({ error: 'Signature verification failed' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
createTicketFromWorkOrder(event).catch(err => {
console.error(`Failed to create external ticket for ${event.id}:`, err);
});
});
async function createTicketFromWorkOrder(event) {
const { type, data } = event;
if (type !== 'workorder.created') {
return;
}
const wo = data.object;
// Map to external format
const externalTicket = mapToExternalTicket(wo);
// Create in external system
const created = await createExternalTicket(externalTicket);
// Store the mapping for future updates
await storeMappings(wo.id, created.id);
console.log(`Created external ticket ${created.id} for work order ${wo.id}`);
}
app.listen(3000, () => console.log('Auto-ticket listener running on port 3000'));
Field Mapping Reference
| Infodeck Field | External Ticket Field | Notes |
|---|---|---|
id | custom field | Store for bidirectional sync |
title | title/subject | 1:1 mapping |
description | description/body | May need formatting adjustments |
priority | priority | See mapPriority() for translation |
assignee.email | assignee | Depends on external system's user lookup |
location.name | tags or custom field | Some systems use tags, others custom fields |
dueDate | due_date | Convert Unix timestamp to ISO format if needed |
asset.id | custom field | For asset-ticket relationship |
Bidirectional Sync (Optional)
To update Infodeck when the external ticket status changes, subscribe to status change webhooks from your external system and call the Infodeck Work Order API:
curl -X PATCH https://app.infodeck.io/api/v2/organizations/{orgId}/work-orders/{workOrderId} \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{ "status": "In Progress" }'
For bidirectional sync, maintain a mapping table: { infodeck_wo_id, external_ticket_id, syncedAt }. Use this to correlate updates from both systems and prevent circular sync loops.
Recipe 4: Multi-Site Sensor Health Dashboard
Pain Point: A property portfolio manager with 20 buildings and 500+ IoT sensors has no single view of which sensors are offline. Technicians discover issues reactively when they can't see real-time data.
Solution: Subscribe to asset.status.changed events, maintain a real-time status map in your database, and expose a simple dashboard showing online/offline counts per location.
Node.js + Express + SQLite
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const crypto = require('crypto');
const app = express();
// Initialize SQLite database
const db = new sqlite3.Database('./sensor_status.db');
db.run(`
CREATE TABLE IF NOT EXISTS sensor_status (
assetId TEXT PRIMARY KEY,
assetName TEXT,
locationId TEXT,
locationName TEXT,
status TEXT,
lastStatusChange INTEGER,
createdAt INTEGER
)
`);
db.run(`
CREATE TABLE IF NOT EXISTS location_summary (
locationId TEXT PRIMARY KEY,
locationName TEXT,
online INTEGER DEFAULT 0,
offline INTEGER DEFAULT 0,
lastUpdated INTEGER
)
`);
function verifyWebhookSignature(payload, header, secret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/webhooks/sensor-health', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const sig = req.headers['x-infodeck-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch (err) {
return res.status(401).json({ error: 'Signature verification failed' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
updateSensorStatus(event).catch(err => {
console.error(`Failed to update sensor status for ${event.id}:`, err);
});
});
function updateSensorStatus(event) {
return new Promise((resolve, reject) => {
const { data } = event;
const asset = data.object;
// Update sensor status in database
db.run(
`INSERT OR REPLACE INTO sensor_status (assetId, assetName, locationId, locationName, status, lastStatusChange, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
asset.id,
asset.name,
asset.locationId,
asset.location?.name || 'Unknown',
asset.status, // 'Online' or 'Offline'
Date.now(),
Date.now()
],
err => {
if (err) return reject(err);
// Recompute location summary
recomputeLocationSummary(asset.locationId, (err) => {
if (err) return reject(err);
console.log(`Updated status for sensor ${asset.id} to ${asset.status}`);
resolve();
});
}
);
});
}
function recomputeLocationSummary(locationId, callback) {
db.all(
`SELECT status FROM sensor_status WHERE locationId = ?`,
[locationId],
(err, rows) => {
if (err) return callback(err);
const online = rows.filter(r => r.status === 'Online').length;
const offline = rows.filter(r => r.status === 'Offline').length;
db.run(
`INSERT OR REPLACE INTO location_summary (locationId, locationName, online, offline, lastUpdated)
SELECT locationId, locationName, ?, ?, ? FROM sensor_status WHERE locationId = ? LIMIT 1`,
[online, offline, Date.now(), locationId],
callback
);
}
);
}
// Dashboard endpoint: returns current status summary
app.get('/dashboard/sensor-health', (req, res) => {
db.all(
`SELECT locationId, locationName, online, offline, lastUpdated FROM location_summary ORDER BY locationName ASC`,
(err, rows) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json({
summary: rows.map(row => ({
location: row.locationName,
online: row.online,
offline: row.offline,
total: row.online + row.offline,
offlinePercentage: row.online + row.offline > 0
? ((row.offline / (row.online + row.offline)) * 100).toFixed(1)
: 0,
lastUpdated: new Date(row.lastUpdated).toISOString()
})),
generatedAt: new Date().toISOString()
});
}
);
});
// Detailed view: all sensors for a specific location
app.get('/dashboard/location/:locationId', (req, res) => {
db.all(
`SELECT assetId, assetName, status, lastStatusChange FROM sensor_status WHERE locationId = ? ORDER BY assetName ASC`,
[req.params.locationId],
(err, rows) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.json({
sensors: rows.map(row => ({
id: row.assetId,
name: row.assetName,
status: row.status,
lastChanged: new Date(row.lastStatusChange).toISOString()
}))
});
}
);
});
app.listen(3000, () => console.log('Sensor health dashboard listening on port 3000'));
Webhook Registration with Location Filter
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/sensor-health",
"events": ["asset.status.changed"],
"description": "Multi-site sensor health tracking",
"filtering": {
"mode": "include",
"criteria": {
"assetType": ["IoT Sensor", "Edge Gateway"]
}
}
}'
Sample Dashboard Output
{
"summary": [
{
"location": "Building A - HQ",
"online": 142,
"offline": 3,
"total": 145,
"offlinePercentage": "2.1",
"lastUpdated": "2026-03-26T14:32:15Z"
},
{
"location": "Building B - Branch",
"online": 98,
"offline": 12,
"total": 110,
"offlinePercentage": "10.9",
"lastUpdated": "2026-03-26T14:28:42Z"
}
],
"generatedAt": "2026-03-26T14:35:00Z"
}
Recipe 5: Trigger Procurement When Parts Hit Shortage
Pain Point: Stores teams discover parts shortages only when a technician arrives on-site and can't complete the job. By then, emergency procurement is expensive and delays repairs.
Solution: Subscribe to workorder.created events, check material reservations against inventory levels via the API, and trigger procurement workflows automatically.
Node.js Implementation
const express = require('express');
const crypto = require('crypto');
const app = express();
const INFODECK_API = 'https://app.infodeck.io/api/v2';
function verifyWebhookSignature(payload, header, secret) {
const [tPart, v1Part] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Check inventory levels for required materials
async function checkMaterialAvailability(orgId, materialsRequired) {
const shortages = [];
for (const material of materialsRequired) {
// Fetch part from inventory
const res = await fetch(
`${INFODECK_API}/organizations/${orgId}/inventory/parts/${material.partId}`,
{
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
}
);
if (!res.ok) {
console.warn(`Part ${material.partId} not found in inventory`);
continue;
}
const { data: part } = await res.json();
// Check if we have enough stock
const availableQuantity = part.quantityOnHand - part.quantityReserved;
if (availableQuantity < material.quantityRequired) {
shortages.push({
partId: material.partId,
partName: part.name,
required: material.quantityRequired,
available: availableQuantity,
shortage: material.quantityRequired - availableQuantity
});
}
}
return shortages;
}
// Trigger procurement workflow
async function triggerProcurement(orgId, shortages, woId) {
const procurementRequest = {
workOrderId: woId,
shortages: shortages,
requestedAt: Date.now(),
status: 'pending_approval'
};
// Option 1: Send email to procurement team
await sendProcurementEmail(shortages, woId);
// Option 2: Create a task in your project management tool
if (process.env.PM_TOOL_WEBHOOK_URL) {
await fetch(process.env.PM_TOOL_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: `Procurement: Parts shortage for WO ${woId}`,
description: formatShortageDescription(shortages, woId),
priority: 'high',
tags: ['procurement', 'urgent']
})
});
}
// Option 3: Post to Slack
if (process.env.SLACK_WEBHOOK_URL) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `URGENT PROCUREMENT NEEDED for WO ${woId}\n` +
shortages.map(s => `- ${s.partName}: need ${s.shortage} more`).join('\n')
})
});
}
}
function formatShortageDescription(shortages, woId) {
let desc = `Parts shortage detected for Work Order ${woId}\n\n`;
shortages.forEach(s => {
desc += `${s.partName}\n`;
desc += ` Needed: ${s.required}\n`;
desc += ` Available: ${s.available}\n`;
desc += ` Shortage: ${s.shortage}\n\n`;
});
return desc;
}
async function sendProcurementEmail(shortages, woId) {
const subject = `Parts Shortage Alert - Work Order ${woId}`;
const body = formatShortageDescription(shortages, woId);
// Use your email service (SendGrid, AWS SES, etc.)
// Example:
// await sendEmail({
// to: process.env.PROCUREMENT_EMAIL,
// subject,
// body
// });
console.log(`Would send email to ${process.env.PROCUREMENT_EMAIL}: ${subject}`);
}
app.post('/webhooks/procurement-trigger', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const sig = req.headers['x-infodeck-signature'];
if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
} catch (err) {
return res.status(401).json({ error: 'Signature verification failed' });
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
checkAndProcure(event).catch(err => {
console.error(`Failed to process procurement for ${event.id}:`, err);
});
});
async function checkAndProcure(event) {
const { type, data } = event;
if (type !== 'workorder.created') {
return;
}
const wo = data.object;
const orgId = wo.organizationId;
// Only process work orders that have material reservations
if (!wo.materialsRequired || wo.materialsRequired.length === 0) {
return;
}
console.log(`Checking materials for WO ${wo.id}...`);
// Check inventory
const shortages = await checkMaterialAvailability(orgId, wo.materialsRequired);
if (shortages.length > 0) {
console.log(`Shortage detected: ${shortages.map(s => s.partName).join(', ')}`);
await triggerProcurement(orgId, shortages, wo.id);
} else {
console.log(`All materials available for WO ${wo.id}`);
}
}
app.listen(3000, () => console.log('Procurement trigger listening on port 3000'));
Webhook Registration
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/procurement-trigger",
"events": ["workorder.created"],
"description": "Auto-trigger procurement on material shortage"
}'
Integration Options
| Option | Pros | Cons |
|---|---|---|
| Simple, works everywhere | Slow, requires manual action | |
| Slack | Real-time, collaborative | Requires manual creation in PM tool |
| Jira/Linear/Azure DevOps | Automated task creation | Requires API key, more setup |
| Email + Auto-PO | Fully automated if you have vendor API | Risky if not carefully controlled |
Be cautious with fully automated purchase orders. Implement approval workflows to prevent accidental over-ordering. Start with email or Slack notifications for human review.
Next Steps
- Webhook Overview -- Understand webhook concepts and architecture
- Quick Start -- Set up your first webhook in 5 minutes
- Event Catalog -- Full payload reference for all event types
- Best Practices -- Production-ready patterns for reliability and monitoring
- Webhook Security -- Signature verification and secret rotation
- Webhook Management API -- Create and manage webhooks programmatically
Support
Stuck on an integration? Email support@infodeck.io or contact our integration team.