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-signatureheader? - How do I handle
NEW_WORK_ORDERorWORK_ORDER_STATUS_CHANGEevents? - 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_, 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).
| Event | Triggered When |
|---|---|
NEW_WORK_ORDER | A work order is created |
WORK_ORDER_CHANGE | A work order's properties, assignees, or teams change |
WORK_ORDER_STATUS_CHANGE | A work order moves between OPEN, IN_PROGRESS, ON_HOLD, and DONE |
NEW_WORK_REQUEST | A work request is submitted |
ASSET_STATUS_CHANGE | An 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
- references/overview.md - What MaintainX webhooks are, common events
- references/setup.md - Creating a webhook, getting the secret
- references/verification.md - Signature verification details and gotchas