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
- work_order.status_changed — a work order moved to any status; the payload carries new_status. Fires alongside work_order.completed, which is unchanged
- 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.low_stock_summary — one daily digest per company listing the parts at or below their reorder point, including parts that never moved
- part.out_of_stock_summary — the same daily digest, for the parts sitting at zero
- part.consumed — a part was consumed on, or returned from, a work order
- compliance.chain_divergence — the audit hash-chain broke (Enterprise)
- asset.downtime_started — a machine stop was opened
- asset.downtime_ended — a machine stop was closed; the duration can now be booked
- report.daily_summary — the day's completed work, once a day at the hour you choose
- purchase_order.created — a purchase order was created; the payload carries the order (reference, status, total_cost, currency, expected_date, erp_reference) and its vendor
- purchase_order.status_changed — a purchase order moved to another status (approved, ordered, cancelled…); the payload carries previous_status and new_status
- purchase_order.received — a delivery was received against a purchase order (sent for every delivery, partial ones included); the payload lists every line with quantity_ordered, quantity_received (this delivery), quantity_received_total and quantity_remaining, plus receipt_id, fully_received and closed_short; new_status is PARTIALLY_RECEIVED while goods are still expected and RECEIVED once complete or closed
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. The part also carries custom_reference: your own code for it, the customReference set through createPart or updatePart, so a system that owns the catalogue can match each line without keeping a table of FreeMaint ids.
{ "event": "part.consumed", "timestamp": "2026-08-19T21:04:11.000Z", "data": { "part": { "id": 812, "name": "Bearing 6204", "part_number": "RLM-204", "reference": "PART-812", "custom_reference": "BC-10045", "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.
Photos (optional)
When Include photos is on for a webhook (off by default), "work_order" and "request" objects carry a "photos" array: up to 4 HTTPS links to JPG, PNG, GIF or WebP images, newest first. Each link opens the image without signing in for 7 days, so store the file on your side if you need it longer. A replay sends the same links. Other events and other webhooks are unchanged. On "work_order.created" and "request.created", a webhook with Include photos is sent 90 seconds after the event (the "timestamp" field keeps the event's time), so that the photos uploaded right after the record are included.
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?