# Invoices

> List and read invoices with their line items, and create draft invoices.

- Canonical URL: https://couplrfsm.com/developers/api-reference/invoices
- Markdown: https://couplrfsm.com/developers/api-reference/invoices.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:** You can read every invoice here, and you can hand me a draft. I keep drafts in Couplr for someone to check, and a key with the send ability can have me email one to the customer with my payment link. I never take a payment, or mark one as taken, because an API call told me to.

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 invoices

`GET /api/v1/invoices` · requires the `invoices: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`. |
| `status` | string | No | Exact status match, e.g. `paid`. |
| `customer_id` | integer | No |  |
| `created_from` | string (date) | No |  |
| `created_to` | string (date) | No |  |

**Example request**

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

**Example response** (`200`)

```json
{
    "data": [
        {
            "id": "9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21",
            "invoice_number": "1047",
            "customer_id": 1842,
            "customer_name": "Dana Whitfield",
            "job_id": 9312,
            "status": "paid",
            "amount": "389.00",
            "notes": null,
            "hosted_invoice_url": null,
            "created_at": "2026-12-02T17:20:05+00:00",
            "updated_at": "2026-12-02T18:01:44+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 draft invoice

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

Creates an invoice with status `draft`. Nothing is sent to the
customer and nothing is charged. The draft appears in Couplr, where
someone reviews it and sends it, or you send it with
`POST /invoices/{id}/send`. An invoice cannot be marked as paid or
charged through the API.

A line with `price_book_item_id` uses that price book item. A line
without one is matched to a price book item with the same name, or
added to the price book as a new item. A job can have one draft:
creating another for the same job replaces the first.

**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. The invoice is always created as a draft.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | integer | Yes |  |
| `job_id` | integer or null | No | A job of this customer to attach the invoice to. |
| `notes` | string or null | No |  |
| `due_days` | integer or null | No | Payment terms in days. Omit to use the company default. |
| `line_items` | array of objects | Yes |  |
| `line_items[].price_book_item_id` | integer or null | No | An existing price book item. When sent, `name` is not needed. |
| `line_items[].name` | string | No | Required without `price_book_item_id`. |
| `line_items[].description` | string or null | No |  |
| `line_items[].quantity` | number | No | Default: `1`. |
| `line_items[].unit_price` | number | No | Dollars. Required without `price_book_item_id`. With one, defaults to the price book price. |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/invoices" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"customer_id":1842,"job_id":9312,"notes":"Replaced condenser fan motor.","line_items":[{"name":"Condenser fan motor","quantity":1,"unit_price":289},{"name":"Service call","quantity":1,"unit_price":100}]}'
```

**Example response** (`201`)

```json
{
    "data": {
        "id": "9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21",
        "invoice_number": "1047",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_id": 9312,
        "status": "paid",
        "amount": "389.00",
        "notes": null,
        "hosted_invoice_url": null,
        "created_at": "2026-12-02T17:20:05+00:00",
        "updated_at": "2026-12-02T18:01:44+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 an invoice

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

Includes `line_items`.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes |  |

**Example request**

```bash
curl "https://couplrfsm.com/api/v1/invoices/9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Example response** (`200`)

```json
{
    "data": {
        "id": "9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21",
        "invoice_number": "1047",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_id": 9312,
        "status": "paid",
        "amount": "389.00",
        "notes": null,
        "hosted_invoice_url": null,
        "created_at": "2026-12-02T17:20:05+00:00",
        "updated_at": "2026-12-02T18:01:44+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. |

## Send a draft invoice

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

Sends a draft invoice to the customer by email with Couplr's payment
link, the same as "Send invoice" in the apps. The customer pays
through that link. Nothing is charged by this call, and there is no
way to record a payment made any other way.

A draft that was saved in Couplr as a bank-only or a financing
invoice is sent that way, and a deposit already credited on the
draft stays on it. You cannot choose how the customer pays or set a
deposit.

There is no request body. The draft is sent as it stands: its
quantities, prices, notes and extra recipients are not changed. Each
line goes out under its price book item's current name, and sales tax
is worked out again from the account's current tax settings, as it is
whenever an invoice is sent from Couplr. Read `amount` in the
response for the amount billed.

The response has status `processing` while Couplr prepares the
payment link. A moment later the invoice becomes `sent`,
`hosted_invoice_url` is filled in and the `invoice.sent` event is
delivered.

Refused with `422` on `invoice` when: the invoice is not a draft; the
customer has no email address; online payments are not set up for the
account; or the draft was set up in Couplr with a payment option the
API does not send (a charge to a saved card, or split payments); a
line uses a price book item that has
been archived; or another send of the same invoice is in progress.
Send those from Couplr.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | Yes |  |

**Headers**

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

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/invoices/9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21/send" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Example response** (`200`)

```json
{
    "data": {
        "id": "9d3f6a52-0c1e-4c57-9d0a-3a0f8a4a7b21",
        "invoice_number": "1047",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "job_id": 9312,
        "status": "processing",
        "amount": "389.00",
        "notes": "Replaced condenser fan motor.",
        "hosted_invoice_url": null,
        "created_at": "2026-12-02T17:20:05+00:00",
        "updated_at": "2026-12-02T18:01:44+00:00",
        "line_items": [
            {
                "name": "Condenser fan motor",
                "quantity": 1,
                "rate": "289.00"
            },
            {
                "name": "Service call",
                "quantity": 1,
                "rate": "100.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. |

## Invoice object

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (uuid) |  |
| `invoice_number` | string or null |  |
| `customer_id` | integer or null |  |
| `customer_name` | string or null |  |
| `job_id` | integer or null |  |
| `status` | string or null |  |
| `amount` | string or null | Decimal string in dollars, e.g. `"125.00"`. |
| `notes` | string or null |  |
| `hosted_invoice_url` | string or null | The customer-facing payment page. Treat as a secret link. |
| `created_at` | string (date-time) or null |  |
| `updated_at` | string (date-time) or null |  |
| `line_items` | array of objects | On a single invoice only, not in lists. |
| `line_items[].name` | string or null |  |
| `line_items[].quantity` | number or null |  |
| `line_items[].rate` | string or null | Decimal string in dollars. |
