技能 编程开发 MaintainX Webhook事件处理

MaintainX Webhook事件处理

v20260423
maintainx-webhooks-events
本技能指南用于实现与MaintainX系统的实时、事件驱动集成。它指导用户如何配置和处理工作订单状态变更、资产更新等实时事件。内容涵盖了Webhook的注册、签名验证(HMAC)、幂等性处理,以及本地开发环境(如使用ngrok)的搭建,确保数据同步的可靠性。
获取技能
374 次下载
概览

MaintainX Webhooks & Events

Overview

Build real-time integrations with MaintainX using webhooks for work order updates, asset changes, and maintenance notifications. MaintainX fires webhook events when key resources change.

Prerequisites

  • MaintainX account with API access
  • HTTPS endpoint accessible from the internet (ngrok for local dev)
  • MAINTAINX_API_KEY environment variable configured

Instructions

Step 1: Register a Webhook

curl -X POST https://api.getmaintainx.com/v1/webhooks \
  -H "Authorization: Bearer $MAINTAINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/maintainx",
    "events": [
      "workorder.created",
      "workorder.updated",
      "workorder.status_changed",
      "workorder.completed"
    ]
  }'

Step 2: Webhook Receiver (Express)

// src/webhook-server.ts
import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.json({ limit: '1mb' }));

// Signature verification middleware
function verifySignature(secret: string) {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const signature = req.headers['x-maintainx-signature'] as string;
    if (!signature) {
      return res.status(401).json({ error: 'Missing signature header' });
    }

    const expected = crypto
      .createHmac('sha256', secret)
      .update(JSON.stringify(req.body))
      .digest('hex');

    if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    next();
  };
}

const WEBHOOK_SECRET = process.env.MAINTAINX_WEBHOOK_SECRET!;

app.post(
  '/webhooks/maintainx',
  verifySignature(WEBHOOK_SECRET),
  async (req, res) => {
    const { event, data, timestamp } = req.body;

    console.log(`[${timestamp}] Event: ${event}, Resource ID: ${data.id}`);

    // Idempotency check
    const eventId = req.headers['x-maintainx-event-id'] as string;
    if (await isProcessed(eventId)) {
      return res.status(200).json({ status: 'already_processed' });
    }

    try {
      await routeEvent(event, data);
      await markProcessed(eventId);
      res.status(200).json({ status: 'ok' });
    } catch (err) {
      console.error('Webhook handler error:', err);
      res.status(500).json({ error: 'Processing failed' });
    }
  },
);

app.listen(3000, () => console.log('Webhook server listening on :3000'));

Step 3: Event Router

// src/event-handlers.ts

type EventHandler = (data: any) => Promise<void>;

const handlers: Record<string, EventHandler> = {
  'workorder.created': async (data) => {
    console.log(`New work order: #${data.id} "${data.title}"`);
    // Notify Slack, create ticket, etc.
    if (data.priority === 'HIGH') {
      await sendSlackAlert(`High priority WO created: ${data.title}`);
    }
  },

  'workorder.status_changed': async (data) => {
    console.log(`WO #${data.id}: ${data.previousStatus} → ${data.status}`);
    if (data.status === 'COMPLETED') {
      await syncCompletionToERP(data);
    }
  },

  'workorder.completed': async (data) => {
    console.log(`WO #${data.id} completed at ${data.completedAt}`);
    await generateCompletionReport(data);
  },

  'workorder.updated': async (data) => {
    await syncWorkOrderToDataWarehouse(data);
  },
};

export async function routeEvent(event: string, data: any) {
  const handler = handlers[event];
  if (handler) {
    await handler(data);
  } else {
    console.warn(`Unhandled event type: ${event}`);
  }
}

Step 4: Idempotency Store

// src/idempotency.ts
// Use Redis in production; Map for dev/testing
const processed = new Map<string, boolean>();

export async function isProcessed(eventId: string): Promise<boolean> {
  return processed.has(eventId);
}

export async function markProcessed(eventId: string): Promise<void> {
  processed.set(eventId, true);
  // In production: await redis.setex(`event:${eventId}`, 86400, '1');
}

Step 5: Local Development with ngrok

# Start your webhook server
npm run dev

# In another terminal, expose it with ngrok
ngrok http 3000
# Copy the https URL (e.g., https://abc123.ngrok-free.app)

# Register the ngrok URL as a webhook
curl -X POST https://api.getmaintainx.com/v1/webhooks \
  -H "Authorization: Bearer $MAINTAINX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok-free.app/webhooks/maintainx",
    "events": ["workorder.created", "workorder.status_changed"]
  }'

Step 6: List and Manage Webhooks

# List all registered webhooks
curl -s https://api.getmaintainx.com/v1/webhooks \
  -H "Authorization: Bearer $MAINTAINX_API_KEY" | jq .

# Delete a webhook (replace ID)
curl -X DELETE https://api.getmaintainx.com/v1/webhooks/456 \
  -H "Authorization: Bearer $MAINTAINX_API_KEY"

Output

  • Webhook endpoint with HMAC signature verification
  • Event router dispatching to type-specific handlers
  • Idempotency guard preventing duplicate processing
  • Local dev setup with ngrok for testing webhooks
  • Webhook registration and management via REST API

Error Handling

Issue Cause Solution
401 on webhook registration Invalid API key Verify MAINTAINX_API_KEY
Webhook not firing URL not reachable Ensure HTTPS, check firewall, test with ngrok
Duplicate events Retries from MaintainX Implement idempotency with event ID deduplication
Signature mismatch Wrong secret or body mutation Verify raw body is used for HMAC, check secret value

Resources

Next Steps

For performance optimization, see maintainx-performance-tuning.

Examples

Slack notification on high-priority work orders:

async function sendSlackAlert(message: string) {
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `:rotating_light: MaintainX Alert: ${message}`,
    }),
  });
}

Polling fallback when webhooks are unavailable:

// Poll every 60 seconds for status changes
async function pollWorkOrders(client: MaintainXClient, since: string) {
  const { workOrders } = await client.getWorkOrders({
    updatedAtGte: since,
    limit: 50,
  });
  for (const wo of workOrders) {
    await routeEvent('workorder.updated', wo);
  }
  return new Date().toISOString();
}
信息
Category 编程开发
Name maintainx-webhooks-events
版本 v20260423
大小 7.19KB
更新时间 2026-04-26
语言