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.md
source 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)
  • 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

IssueCauseSolution
401 on webhook registrationInvalid API keyVerify
MAINTAINX_API_KEY
Webhook not firingURL not reachableEnsure HTTPS, check firewall, test with ngrok
Duplicate eventsRetries from MaintainXImplement idempotency with event ID deduplication
Signature mismatchWrong secret or body mutationVerify 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();
}