Agent skill

MaintainX Webhooks Skill

Receive and verify MaintainX webhooks. Use when setting up MaintainX webhook handlers, debugging x-maintainx-webhook-body-signature verification, or handling maintenance events like NEW_WORK_ORDER, WORK_ORDER_STATUS_CHANGE, NEW_WORK_REQUEST, or ASSET_STATUS_CHANGE.

Install this skill

npx skills add hookdeck/webhook-skills --skill maintainx-webhooks


When to Use This Skill

  • How do I receive MaintainX webhooks?
  • How do I verify the x-maintainx-webhook-body-signature header?
  • How do I handle NEW_WORK_ORDER or WORK_ORDER_STATUS_CHANGE events?
  • Why is my MaintainX webhook signature verification failing?
  • Setting up MaintainX webhook handlers with replay protection

Verification (core)

MaintainX sends two headers on every webhook: x-maintainx-webhook-body-signature and x-maintainx-webhook-uri-signature. Both have the form t=<unix>,v1=<hex>. Verify the body signature: v1 is a hex HMAC-SHA256 over {t}.{raw body}, keyed with the webhook's secret (MaintainX Settings > Integrations > Copy secret, used as a raw string). The URI signature uses the same format over the full URI and is an optional extra. MaintainX has no SDK verification helper, so verify manually against the raw body (don't JSON.parse first). The docs recommend rejecting timestamps older than 5 minutes, but retries and replays (including Hookdeck's) carry the original timestamp, so the examples make that window opt-in (MAINTAINX_TIMESTAMP_TOLERANCE_SECONDS, default 0).

const crypto = require('crypto');

// toleranceSec = 0 (default) skips the timestamp window; a positive value opts in
function verifyMaintainxSignature(rawBody, header, secret, toleranceSec = 0) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const { t: timestamp, v1: signature } = parts;
  if (!timestamp || !signature) return false;
  if (!/^\d+$/.test(timestamp)) return false;
  if (toleranceSec > 0 && Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > toleranceSec) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`, 'utf8') // signed content = t + "." + raw body
    .digest('hex');

  try {
    return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
  } catch {
    return false; // length mismatch = invalid
  }
}

For complete handlers with route wiring, event dispatch, and tests, see:

Common Event Types

The event name is the eventType field in the JSON body (it matches the subscription's type).

EventTriggered When
NEW_WORK_ORDERA work order is created
WORK_ORDER_CHANGEA work order's properties, assignees, or teams change
WORK_ORDER_STATUS_CHANGEA work order moves between OPEN, IN_PROGRESS, ON_HOLD, and DONE
NEW_WORK_REQUESTA work request is submitted
ASSET_STATUS_CHANGEAn asset goes ONLINE, OFFLINE, or changes custom status

For the full event list (assets, parts, purchase orders, users, and more), see references/overview.md and MaintainX's webhook documentation.

Environment Variables

# Secret from MaintainX Settings > Integrations > Webhooks > Copy secret
MAINTAINX_WEBHOOK_SECRET=your_webhook_secret_here
# Optional replay window in seconds; 0 (default) is off, a positive value opts in
MAINTAINX_TIMESTAMP_TOLERANCE_SECONDS=0

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 maintainx --path /webhooks/maintainx

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →