Skip to main content

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

RecipePain PointSolutionEvents Used
Slack AlertsUrgent work orders buried in emailPush Critical/High priority work orders to Slack in real-timeworkorder.created, workorder.status.changed
Digital Twin SyncPolling 500+ sensors burns 15M API calls/monthSubscribe to telemetry events, push to BMS in real-timeasset.telemetry.updated
Auto-Create TicketsManual duplication into ServiceNow/Jira causes sync lagAuto-create external tickets from Infodeck work ordersworkorder.created
Multi-Site Sensor DashboardNo single view of offline sensors across 20 buildingsReal-time sensor status tracking per locationasset.status.changed
Procurement AutomationTechnicians discover parts shortages on-siteAuto-check stock levels and trigger procurementworkorder.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

  1. Create a Slack incoming webhook at https://api.slack.com/messaging/webhooks
  2. Copy the webhook URL (e.g., https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX)
  3. 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"]
}
}
}'
info

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"]
}
}
}'
tip

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 FieldExternal Ticket FieldNotes
idcustom fieldStore for bidirectional sync
titletitle/subject1:1 mapping
descriptiondescription/bodyMay need formatting adjustments
priorityprioritySee mapPriority() for translation
assignee.emailassigneeDepends on external system's user lookup
location.nametags or custom fieldSome systems use tags, others custom fields
dueDatedue_dateConvert Unix timestamp to ISO format if needed
asset.idcustom fieldFor 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" }'
info

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

OptionProsCons
EmailSimple, works everywhereSlow, requires manual action
SlackReal-time, collaborativeRequires manual creation in PM tool
Jira/Linear/Azure DevOpsAutomated task creationRequires API key, more setup
Email + Auto-POFully automated if you have vendor APIRisky if not carefully controlled
danger

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

Support

Stuck on an integration? Email support@infodeck.io or contact our integration team.

Was this page helpful?