Claude-code-plugins-plus-skills maintainx-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/maintainx-pack/skills/maintainx-webhooks-events" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-maintainx-webhooks-events && rm -rf "$T"
manifest:
plugins/saas-packs/maintainx-pack/skills/maintainx-webhooks-events/SKILL.mdsource content
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)
environment variable configuredMAINTAINX_API_KEY
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 |
| 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(); }