Webhooks (Outbound)

Push FreeMaint events into your own systems in real time

4 min read
Print this section

Webhooks are the push half of integrations: instead of your system polling FreeMaint, FreeMaint POSTs to your URL the moment something happens. Set them up in Company Settings > Webhooks (/company-settings/webhooks). Available from Starter (100 deliveries/day; Business and above unlimited).

Available events

  • work_order.created โ€” a work order was created
  • work_order.completed โ€” a work order moved to Completed
  • request.created โ€” a maintenance request was submitted
  • request.status_changed โ€” a request was approved, rejected or cancelled
  • part.low_stock โ€” a part fell to or below its reorder point
  • part.out_of_stock โ€” a part hit zero
  • part.consumed โ€” a part was consumed on, or returned from, a work order
  • compliance.chain_divergence โ€” the audit hash-chain broke (Enterprise)
  • report.daily_summary โ€” the day's completed work, once a day at the hour you choose

part.consumed โ€” mirroring stock into an ERP

This is the event to subscribe to when another system (ERP, billing, inventory, accounting) must reflect what maintenance actually used. quantity is signed: negative when the part is issued to the job, positive when it is returned by removing it from the work order โ€” so a reversal in FreeMaint produces a matching reversal document downstream. stock_movement_id is stable across retries and replays: use it as your idempotency key and you will never post the same issue twice. One thing to know when the traffic runs both ways: the GraphQL adjustStock mutation does NOT fire this event. That is deliberate โ€” a stock correction your own system writes into FreeMaint must never come back to you as an echo you post twice. Only consumption recorded on a work order inside FreeMaint raises part.consumed.

{ "event": "part.consumed", "timestamp": "2026-08-19T21:04:11.000Z", "data": { "part": { "id": 812, "name": "Bearing 6204", "part_number": "RLM-204", "reference": "PART-812", "unit_cost": 12.5, "unit_of_measure": "ea" }, "quantity": -2, "previous_quantity": 48, "new_quantity": 46, "work_order": { "id": 5521, "reference": "WO-2026-0412", "title": "Bearing replacement" }, "asset": { "id": 91, "name": "Separator 2" }, "location": { "id": 7, "name": "Plant warehouse" }, "user": { "id": 5627, "name": "A. Technician" }, "stock_movement_id": 88213, "occurred_at": "2026-08-19T21:04:11.000Z" } }

Delivery and signature

Each delivery is a POST with a JSON body and an X-FreeMaint-Signature header in the form t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of the string "<t>.<raw body>" keyed with your webhook secret. Verify against the RAW body, before any JSON parsing or re-serialisation. We also send X-FreeMaint-Event and X-FreeMaint-Delivery-Id. A 2xx marks the delivery delivered; network errors and 5xx are retried up to 3 times with exponential backoff (about 2s, 4s, 8s); a 4xx is treated as a permanent rejection and is NOT retried.

Replaying a delivery

Every attempt is kept in the delivery history on the webhook, with its status and response code. Any settled delivery can be replayed from there โ€” it re-sends the stored payload unchanged, so the business keys inside it (including stock_movement_id) stay the same and a receiver that deduplicates correctly will not double-post. This is how you recover after your endpoint was down or rejected a request by mistake.

URL requirements

  • https:// only โ€” plain http is rejected
  • Port 443 or 8443 only
  • The host must be publicly reachable โ€” loopback, private and carrier-grade NAT ranges are refused
  • No credentials in the URL (https://user:pass@host is rejected)
  • A system that only exists inside your own network needs a public endpoint or a relay (n8n, Make, Zapier) in front of it

Caps

  • Starter โ€” 100 deliveries/day per company
  • Business โ€” Unlimited
  • Enterprise โ€” Unlimited
  • The counter is per delivery attempt, so three webhooks subscribed to the same event consume three units
  • Daily counter resets at 00:00 UTC

Tip

Verify the X-FreeMaint-Signature header on every request, against the raw request body, so a spoofed call cannot move stock in your ERP.

Was this page helpful?