# Jobs

> List, create and update jobs.

- Canonical URL: https://couplrfsm.com/developers/api-reference/jobs
- Markdown: https://couplrfsm.com/developers/api-reference/jobs.md
- API version: v1 (base URL https://couplrfsm.com/api/v1)
- OpenAPI specification: https://couplrfsm.com/developers/openapi.yaml
- All documentation pages: https://couplrfsm.com/llms.txt

> **Flip:** A job is a visit on the schedule. Create one and it shows up on the board, and I may assign a technician to it. The ids a job refers to, such as its type or its technician, come from the lookup lists. You can also tell me a job is done. When you do, I send the customer what the company sends for any finished job.

Base URL: `https://couplrfsm.com/api/v1`. Conventions shared by every endpoint (examples, ids, dates, money) are on the [API reference](https://couplrfsm.com/developers/api-reference) page. Errors, pagination and idempotency are in [Errors, limits and retries](https://couplrfsm.com/developers/errors-and-limits).

## List jobs

`GET /api/v1/jobs` · requires the `jobs:read` ability

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | No | Default: `1`. |
| `per_page` | integer | No | Values above 100 are served as 100. Default: `25`. |
| `updated_since` | string (date-time) | No | Only records changed at or after this time. ISO 8601, for example `2026-11-14T15:00:00Z`. |
| `customer_id` | integer | No |  |
| `status` | string: `open`, `completed` | No | `open` or `completed`. Any other value is ignored. |
| `scheduled_from` | string (date) | No |  |
| `scheduled_to` | string (date) | No |  |

**Example request**

```bash
curl "https://couplrfsm.com/api/v1/jobs" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Example response** (`200`)

```json
{
    "data": [
        {
            "id": 9312,
            "customer_id": 1842,
            "customer_name": "Dana Whitfield",
            "job_type": "Repair",
            "scheduled_date": "2026-12-02",
            "scheduled_start_time": "09:00:00",
            "time_window_start": null,
            "time_window_end": null,
            "duration_minutes": null,
            "is_completed": false,
            "completed_at": null,
            "notes": "No cooling upstairs. Gate code 4410.",
            "source": null,
            "created_at": "2026-11-14T15:04:40+00:00",
            "updated_at": "2026-11-14T15:04:40+00:00"
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    }
}
```

**Errors**

| Status | When |
| --- | --- |
| `401` | No key (`api_key_missing`), the key is unknown or revoked (`api_key_invalid`), the key had an end date that has passed (`api_key_expired`), or the key belongs to a private trial sandbox whose access window has ended (`sandbox_expired`). |
| `403` | The key lacks the ability this endpoint needs (`ability_missing`), no admin user is left to act for the key (`no_acting_admin`), or the company's account is not yet verified. |
| `422` | A parameter or field is invalid (`validation_failed`). `errors` maps each field to its messages. |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Create a job

`POST /api/v1/jobs` · requires the `jobs:write` ability · accepts `Idempotency-Key`

Creating a job behaves as it does in the app: it appears on the
schedule, may be auto-dispatched to a technician, and fires the
`job.created` webhook. A second identical request (same customer, job
type and day) within 30 seconds returns the first job with `201`
rather than creating another; send an `Idempotency-Key` for a longer
window.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Makes a retry of this request safe. See "Retries and idempotency". |

**Request body** (`application/json`)

Fields not listed are ignored. Every id must belong to the key's company.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | integer | Yes |  |
| `job_type_id` | integer | No | Required unless `job_type` is sent. |
| `job_type` | string | No | A job type name, matched case-insensitively. See `GET /job-types`. |
| `address_id` | integer or null | No | Required when the customer has more than one address. |
| `scheduled_date` | string (date) or null | No | Omit to leave the job unscheduled. |
| `scheduled_start_time` | string or null | No | Company-local time, e.g. `09:00`. |
| `use_time_window` | boolean | No | `true` books an arrival window (`time_window_*`) instead of a start time. |
| `time_window_start` | string or null | No |  |
| `time_window_end` | string or null | No |  |
| `duration_minutes` | integer or null | No |  |
| `days` | array of objects or null | No | A multi-day job, one entry per day, instead of `scheduled_date`. |
| `days[].date` | string (date) | No |  |
| `days[].scheduled_start_time` | string or null | No |  |
| `days[].time_window_start` | string or null | No |  |
| `days[].time_window_end` | string or null | No |  |
| `days[].duration_minutes` | integer or null | No |  |
| `notes` | string or null | No |  |
| `job_summary` | string or null | No |  |
| `kind` | string: `service`, `estimate` | No | An `estimate` visit must be scheduled. Default: `service`. |
| `estimate_id` | integer or null | No | The estimate an estimate visit is for. |
| `is_maintenance` | boolean | No | Links the job to the customer's active maintenance plan. |
| `market_segment_id` | integer or null | No |  |
| `lead_source_id` | integer or null | No |  |
| `branch_id` | integer or null | No |  |
| `business_unit_id` | integer or null | No |  |
| `assigned_tech_id` | integer or null | No | A single technician. Omit every technician field to auto-dispatch. |
| `assigned_tech_ids` | array of integers or null | No |  |
| `lead_tech_id` | integer or null | No |  |
| `tag_ids` | array of integers | No |  |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/jobs" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"customer_id":1842,"job_type":"Repair","scheduled_date":"2026-12-02","scheduled_start_time":"09:00","notes":"No cooling upstairs. Gate code 4410."}'
```

**Example response** (`201`)

```json
{
    "data": {
        "id": 9312,
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_type": "Repair",
        "scheduled_date": "2026-12-02",
        "scheduled_start_time": "09:00:00",
        "time_window_start": null,
        "time_window_end": null,
        "duration_minutes": null,
        "is_completed": false,
        "completed_at": null,
        "notes": "No cooling upstairs. Gate code 4410.",
        "source": null,
        "created_at": "2026-11-14T15:04:40+00:00",
        "updated_at": "2026-11-14T15:04:40+00:00"
    }
}
```

**Errors**

| Status | When |
| --- | --- |
| `401` | No key (`api_key_missing`), the key is unknown or revoked (`api_key_invalid`), the key had an end date that has passed (`api_key_expired`), or the key belongs to a private trial sandbox whose access window has ended (`sandbox_expired`). |
| `403` | The key lacks the ability this endpoint needs (`ability_missing`), no admin user is left to act for the key (`no_acting_admin`), or the company's account is not yet verified. |
| `422` | A parameter or field is invalid (`validation_failed`). `errors` maps each field to its messages. |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Get a job

`GET /api/v1/jobs/{id}` · requires the `jobs:read` ability

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | Yes |  |

**Example request**

```bash
curl "https://couplrfsm.com/api/v1/jobs/1842" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Example response** (`200`)

```json
{
    "data": {
        "id": 9312,
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_type": "Repair",
        "scheduled_date": "2026-12-02",
        "scheduled_start_time": "09:00:00",
        "time_window_start": null,
        "time_window_end": null,
        "duration_minutes": null,
        "is_completed": false,
        "completed_at": null,
        "notes": "No cooling upstairs. Gate code 4410.",
        "source": null,
        "created_at": "2026-11-14T15:04:40+00:00",
        "updated_at": "2026-11-14T15:04:40+00:00"
    }
}
```

**Errors**

| Status | When |
| --- | --- |
| `401` | No key (`api_key_missing`), the key is unknown or revoked (`api_key_invalid`), the key had an end date that has passed (`api_key_expired`), or the key belongs to a private trial sandbox whose access window has ended (`sandbox_expired`). |
| `403` | The key lacks the ability this endpoint needs (`ability_missing`), no admin user is left to act for the key (`no_acting_admin`), or the company's account is not yet verified. |
| `404` | No such record in this company (`not_found`). |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Update a job

`PATCH /api/v1/jobs/{id}` · requires the `jobs:write` ability · accepts `Idempotency-Key`

Reschedule a job, move it to another of the customer's addresses,
change the crew or edit its notes. Only the fields you send change.
The customer cannot be changed. A job is completed with
`POST /jobs/{id}/complete` and cannot be deleted through the API.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | Yes |  |

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Makes a retry of this request safe. See "Retries and idempotency". |

**Request body** (`application/json`)

Fields not listed are ignored. Every id must belong to the key's company.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `job_type_id` | integer | No |  |
| `job_type` | string | No | A job type name, matched case-insensitively. See `GET /job-types`. |
| `address_id` | integer or null | No | One of the job's customer's addresses. |
| `scheduled_date` | string (date) | No | Moves a single-day job to this day. |
| `scheduled_start_time` | string or null | No | Company-local time, e.g. `13:00`. |
| `use_time_window` | boolean | No | `true` books an arrival window (`time_window_*`) instead of a start time. |
| `time_window_start` | string or null | No |  |
| `time_window_end` | string or null | No |  |
| `duration_minutes` | integer or null | No |  |
| `days` | array of objects | No | Replaces the job's days with this list, for a multi-day job. |
| `days[].date` | string (date) | No |  |
| `days[].scheduled_start_time` | string or null | No |  |
| `days[].time_window_start` | string or null | No |  |
| `days[].time_window_end` | string or null | No |  |
| `days[].duration_minutes` | integer or null | No |  |
| `unschedule` | boolean | No | `true` takes the job off the calendar. Not allowed for estimate visits or completed jobs. |
| `notes` | string or null | No |  |
| `job_summary` | string or null | No |  |
| `market_segment_id` | integer or null | No |  |
| `lead_source_id` | integer or null | No |  |
| `branch_id` | integer or null | No |  |
| `business_unit_id` | integer or null | No |  |
| `assigned_tech_ids` | array of integers | No | Replaces the crew. An empty list clears it. See `GET /technicians`. |
| `lead_tech_id` | integer or null | No | Sent with `assigned_tech_ids`. |

**Example request**

```bash
curl -X PATCH "https://couplrfsm.com/api/v1/jobs/1842" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"scheduled_date":"2026-12-04","scheduled_start_time":"13:00"}'
```

**Example response** (`200`)

```json
{
    "data": {
        "id": 9312,
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_type": "Repair",
        "scheduled_date": "2026-12-02",
        "scheduled_start_time": "09:00:00",
        "time_window_start": null,
        "time_window_end": null,
        "duration_minutes": null,
        "is_completed": false,
        "completed_at": null,
        "notes": "No cooling upstairs. Gate code 4410.",
        "source": null,
        "created_at": "2026-11-14T15:04:40+00:00",
        "updated_at": "2026-11-14T15:04:40+00:00"
    }
}
```

**Errors**

| Status | When |
| --- | --- |
| `401` | No key (`api_key_missing`), the key is unknown or revoked (`api_key_invalid`), the key had an end date that has passed (`api_key_expired`), or the key belongs to a private trial sandbox whose access window has ended (`sandbox_expired`). |
| `403` | The key lacks the ability this endpoint needs (`ability_missing`), no admin user is left to act for the key (`no_acting_admin`), or the company's account is not yet verified. |
| `404` | No such record in this company (`not_found`). |
| `422` | A parameter or field is invalid (`validation_failed`). `errors` maps each field to its messages. |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Complete a job

`POST /api/v1/jobs/{id}/complete` · requires the `jobs:complete` ability · accepts `Idempotency-Key`

Marks a job done, the same as completing it in the apps. The
`job.completed` event is delivered (`estimate.completed` for an
estimate visit) and the crew is credited for the work.

**The customer may be contacted.** Completing a job sends whatever
the company has switched on for a finished job: the completion text
and, if the company asks for reviews automatically, a review
request. That is why this has its own ability.

`notes`, if sent, is added to the job's notes log.

Refused with `422` on `job` when the job is already completed; when
it is a maintenance plan visit; when an invoice is being held until
the job is done; or when the job type has a checklist that must be
filled in. Complete those in Couplr.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | Yes |  |

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | No | Makes a retry of this request safe. See "Retries and idempotency". |

**Request body** (`application/json`)

Fields not listed are ignored.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `notes` | string or null | No | Added to the job's notes log. |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/jobs/1842/complete" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"notes":"Replaced the capacitor. System cooling normally."}'
```

**Example response** (`200`)

```json
{
    "data": {
        "id": 9312,
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_type": "Repair",
        "scheduled_date": "2026-12-02",
        "scheduled_start_time": "09:00:00",
        "time_window_start": null,
        "time_window_end": null,
        "duration_minutes": null,
        "is_completed": false,
        "completed_at": null,
        "notes": "No cooling upstairs. Gate code 4410.",
        "source": null,
        "created_at": "2026-11-14T15:04:40+00:00",
        "updated_at": "2026-11-14T15:04:40+00:00"
    }
}
```

**Errors**

| Status | When |
| --- | --- |
| `401` | No key (`api_key_missing`), the key is unknown or revoked (`api_key_invalid`), the key had an end date that has passed (`api_key_expired`), or the key belongs to a private trial sandbox whose access window has ended (`sandbox_expired`). |
| `403` | The key lacks the ability this endpoint needs (`ability_missing`), no admin user is left to act for the key (`no_acting_admin`), or the company's account is not yet verified. |
| `404` | No such record in this company (`not_found`). |
| `422` | A parameter or field is invalid (`validation_failed`). `errors` maps each field to its messages. |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Job object

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer |  |
| `customer_id` | integer |  |
| `customer_name` | string or null |  |
| `job_type` | string or null |  |
| `scheduled_date` | string (date) or null |  |
| `scheduled_start_time` | string or null | Company-local time of day, `HH:MM:SS`. |
| `time_window_start` | string or null | Company-local time of day. |
| `time_window_end` | string or null | Company-local time of day. |
| `duration_minutes` | integer or null |  |
| `is_completed` | boolean |  |
| `completed_at` | string or null | `YYYY-MM-DD HH:MM:SS` in UTC (not ISO 8601). |
| `notes` | string or null |  |
| `source` | string or null |  |
| `created_at` | string (date-time) or null |  |
| `updated_at` | string (date-time) or null |  |
