> For the complete documentation index, see [llms.txt](https://docs.novelsystems.ca/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.novelsystems.ca/fsm-field-sync-api.md).

# FSM Field Sync API

The field service module is where a quote becomes a dispatched job. This API exposes job status to your dispatch board, and pushes webhooks the moment something changes in the field that your accounting package needs to know about — a change order approved on a tablet at 07:40 should not wait for someone to retype it at 17:00.

### List jobs

```http
GET /v1/fsm/jobs
```

#### Request

```bash
curl "https://api.novelsystems.ca/v1/fsm/jobs?status=in_progress&updated_since=2026-07-30T00:00:00Z&limit=50" \
  -H "Authorization: Bearer <API_TOKEN>"
```

#### Query parameters

| Parameter       | Type     | Notes                                                                                                             |
| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `status`        | string   | `scheduled`, `dispatched`, `in_progress`, `on_hold`, `complete`, `cancelled`. Repeatable.                         |
| `crew_id`       | string   | Restrict to one crew.                                                                                             |
| `site_id`       | string   | Restrict to one job site.                                                                                         |
| `updated_since` | RFC 3339 | Only jobs whose state changed after this instant. Use this for incremental sync rather than re-pulling the board. |
| `cursor`        | string   | Pass back the `next_cursor` from the previous page.                                                               |
| `limit`         | integer  | 1–200. Defaults to 50.                                                                                            |

#### Response — `200 OK`

```json
{
  "data": [
    {
      "job_id": "job_01J9ZD8P3T5RWQ2A",
      "quote_ref": "NS-2026-0418",
      "status": "in_progress",
      "project_type": "Commercial Installation",
      "site": {
        "site_id": "site_01J8QK7N",
        "name": "155 Wellington St W — Floors 12–14",
        "region": "ON"
      },
      "crew": {
        "crew_id": "crew_04",
        "foreman": "R. Okafor",
        "headcount": 4
      },
      "scheduled_start": "2026-07-30T11:00:00Z",
      "actual_start": "2026-07-30T11:14:00Z",
      "hours_logged": 26.75,
      "hours_estimated": 42.5,
      "percent_complete": 0.63,
      "open_change_orders": 1,
      "updated_at": "2026-07-30T18:41:09Z"
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjUwfQ",
  "has_more": true
}
```

### Retrieve one job

```http
GET /v1/fsm/jobs/{job_id}
```

Returns the same object with the full change order list, time entries, and material draws expanded. `404` with `type: job-not-found` if the ID is unknown to the tenant.

### Update job status

```http
PATCH /v1/fsm/jobs/{job_id}
```

```json
{ "status": "complete", "completed_at": "2026-07-31T21:05:00Z" }
```

Transitions are validated. Moving from `scheduled` straight to `complete` returns `409` with `type: invalid-transition` — dispatch history has to stay reconstructable for a change order dispute months later.

***

## Webhooks

Register endpoints under **Settings → Developer → Webhooks**. Every delivery is a `POST` of `application/json`.

### Verifying a delivery

Each request carries:

```http
X-Novel-Signature: t=1785614469,v1=8d2f...c41a
X-Novel-Event-Id: evt_01J9ZDQ4V8B7M2XN
```

`v1` is `HMAC-SHA256(secret, "{t}.{raw_body}")` in lowercase hex. Compare it with a constant-time comparison, and reject deliveries where `t` is more than five minutes old — that is what stops a captured payload being replayed at you later.

Respond `2xx` within 10 seconds. Anything else is retried with exponential backoff for 24 hours. Deliveries are at-least-once, so key your handler on `X-Novel-Event-Id` and ignore repeats.

### `change_order.approved`

The event commercial operators care most about. It fires when a change order is approved in the field, which is the moment the contract value actually moved.

```json
{
  "event": "change_order.approved",
  "event_id": "evt_01J9ZDQ4V8B7M2XN",
  "occurred_at": "2026-07-30T18:41:09Z",
  "data": {
    "change_order_id": "cor_01J9ZDP2M6K4",
    "job_id": "job_01J9ZD8P3T5RWQ2A",
    "quote_ref": "NS-2026-0418",
    "sequence": 3,
    "reason": "Added conduit run — Floor 13 east riser, unforeseen slab condition",
    "approved_by": {
      "name": "D. Marchetti",
      "role": "General Contractor PM",
      "method": "signature_on_device"
    },
    "delta": {
      "labour_hours": 9.5,
      "material_cost": 1180.00,
      "direct_cost": 1797.50,
      "sell": 2496.53,
      "currency": "CAD"
    },
    "revised_contract_value": {
      "previous": 8655.03,
      "current": 11151.56,
      "currency": "CAD"
    },
    "margin": {
      "applied": 0.28,
      "floor_engaged": false
    }
  }
}
```

`delta` is the change alone; `revised_contract_value` is the job after it. Post the `delta` to your accounting package and the two systems stay reconciled without anyone rekeying a figure.

### Other events

| Event                    | Fires when                                                      |
| ------------------------ | --------------------------------------------------------------- |
| `change_order.submitted` | A change order is raised in the field, before approval.         |
| `change_order.rejected`  | Approval is declined. Carries `rejected_by` and `reason`.       |
| `job.dispatched`         | A crew is assigned and the job is released to the field.        |
| `job.status_changed`     | Any status transition. Carries `from` and `to`.                 |
| `job.completed`          | Final sign-off, with `hours_logged` against `hours_estimated`.  |
| `quote.created`          | A calculated quote is persisted and given a proposal reference. |
| `invoice.generated`      | An invoice is produced from a completed job.                    |

### Failure handling

Failed deliveries are visible under **Settings → Developer → Webhooks → Deliveries** with the request, the response, and a replay button. After 24 hours of failures the endpoint is disabled and the tenant's technical contact is emailed — a webhook that has been quietly dead for a fortnight is worse than one that is loudly off.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.novelsystems.ca/fsm-field-sync-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
