Webhooks (Outbound)
Push FreeMaint events into your own systems in real time
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
Related articles
Was this page helpful?