Webhooks¶
Enterprise feature
Webhooks are available on Enterprise plans. Compare plans to find the right fit for your team.
A webhook tells your own system the moment something happens in FSM Navigator. When a job is completed, for example, we send an HTTPS POST with the job's details to an address you choose. Your accounting tool, data warehouse or Zapier workflow can act on it straight away instead of checking for changes.
You can set webhooks up in two ways:
- From the Webhooks page in your dashboard — for your own receiving system.
- Through the API (REST hooks) — this is how Zapier and similar apps subscribe for you.
Who can manage webhooks¶
The account Owner and Managers can open the Webhooks page from the side menu. Other roles do not see it.
Add an endpoint¶
- Open Webhooks from the side menu.
- Select Add endpoint.
- Enter the address that will receive events. It must be a public
https://address — addresses on a private or internal network are refused. - Optionally add a description, such as "ERP sync".
- Tick the events you want, then select Save endpoint.
- Copy the signing secret that appears. It is shown only once. Store it in the receiving system so it can check that each request really came from us.
Use Send test at any time to send a ping event and see whether your endpoint accepts it.
Events you can subscribe to¶
| Event | When it is sent |
|---|---|
job.created | A new job is created |
job.updated | A job's details (name, description, schedule, priority and similar), its required skills, its linked assets or its required parts change |
job.status_changed | A job moves to a new status, for example Assigned → In-Transit |
job.completed | A job is completed (sent in addition to job.status_changed) |
job.deleted | A job is deleted (it can still be restored) |
job.restored | A deleted job is restored |
invoice.paid | An invoice becomes fully paid |
customer.created | A new customer is added |
service_request.created | A customer or website visitor submits a service request |
invoice.paid and service_request.created are available to endpoints added on the Webhooks page. API subscriptions (REST hooks) can use the job and customer events.
What we send¶
Every request is a JSON body like this:
{
"id": "0192f0c4-7b1e-7cc3-9a51-3c2f6d0e8a41",
"type": "job.completed",
"api_version": "2026-10-01",
"occurred_at": "2026-10-01T14:32:07.118Z",
"source": "mobile",
"data": {
"object": {
"job_id": 12345,
"job_number": "JOB-1042",
"job_title": "Furnace tune-up",
"job_status": "Completed",
"customer": { "customer_id": 881, "customer_name": "Acme HVAC" }
},
"previous": { "status": "In-Progress" },
"version": 7
}
}
data.objecthas the same fields as the matching Jobs API or Customers API response, read at the moment we send it. Invoice amounts are whole cents. If the record no longer exists,data.objectholds only its id and"deleted": true.idis the event id. It is the same on every retry and on a manual resend.sourcetells you where the change came from:ui(web dashboard),mobile(mobile app),apiorapi_key:<id>(Enterprise API),scheduled(recurring jobs and other scheduled work),auto_assignment_rebalance(automatic assignment),portal(customer portal) orbooking(online booking).
Each request also carries these headers:
| Header | Meaning |
|---|---|
X-FSM-Event-Id | The event id — use it to ignore duplicates |
X-FSM-Event-Type | The event type |
X-FSM-Delivery-Id | This particular delivery attempt |
X-FSM-Timestamp | When we signed the request (Unix seconds) |
X-FSM-Signature | t=<timestamp>,v1=<signature> |
Verify the signature¶
The signature is an HMAC-SHA256 of the timestamp, a full stop, and the raw request body, using your signing secret as the key. Compare it with the v1= value before trusting the request, and reject requests whose timestamp is more than five minutes old.
$header = $_SERVER['HTTP_X_FSM_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $signingSecret);
$valid = abs(time() - (int) $parts['t']) <= 300
&& preg_match_all('/v1=([0-9a-f]{64})/', $header, $m)
&& in_array(true, array_map(fn ($s) => hash_equals($expected, $s), $m[1]), true);
When you create a new secret with New secret, we sign with both the new and the old secret for 24 hours, so the header carries two v1= values. Accept the request if either one matches.
Delivery, retries and ordering¶
- Answer with any
2xxstatus within 10 seconds to confirm delivery. We read at most 64 KB of your response. - If your endpoint times out, cannot be reached, or answers
408,429or5xx, we try again with growing gaps, up to eight attempts over about 15 hours, before giving up. - Redirects are not followed. Other
4xxanswers are treated as final. - Answering
410 Gonetells us to stop: an API subscription is removed and a dashboard endpoint is switched off. - The same event can arrive more than once. Use
X-FSM-Event-Idto skip ones you have already handled. - Events can arrive out of order. Use
occurred_atanddata.versionto ignore an older update that arrives after a newer one.
If an endpoint keeps failing, it is switched off and the Owner and Managers receive an email and an in-app notification. Fix the receiving system, then select Turn on — we send a test event first and only turn it back on if it is accepted.
Delivery log¶
The Delivery log tab lists every delivery with its status, response code and timing. Filter by endpoint or status, and use Send again to resend an event with the same event id. The log keeps 30 days of history.
REST hooks for Zapier and other apps¶
Apps that follow the REST Hooks pattern subscribe through the API with an API key that has the webhooks:manage scope, plus the read scope for the event (jobs:read for job events, customers:read for customer.created). See API authentication for creating keys.
A key can only see and remove the subscriptions it created. Revoking the key removes its subscriptions. These subscriptions also appear, read-only, on the Webhooks page under Connected through the API.