# Estimates

> List and read estimates, create one with the simple layout, and email it to the customer.

- Canonical URL: https://couplrfsm.com/developers/api-reference/estimates
- Markdown: https://couplrfsm.com/developers/api-reference/estimates.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:** An estimate is an offer: a list of items and a total the customer can say yes to. You can read them all, hand me a new one with a simple list of items, and have me email it to the customer. They accept it on my page, and I tell you with the `estimate.accepted` event.

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 estimates

`GET /api/v1/estimates` · requires the `estimates: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`, `accepted`, `declined` | No | `open`, `accepted` or `declined`. Any other value is ignored. |

**Example request**

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

**Example response** (`200`)

```json
{
    "data": [
        {
            "id": 511,
            "layout": "simple",
            "title": "Replace condenser fan motor",
            "customer_id": 1842,
            "customer_name": "Dana Whitfield",
            "status": "accepted",
            "total": 389,
            "accepted_at": "2026-12-01T16:45:00+00:00",
            "declined_at": null,
            "created_at": "2026-11-30T14:10:00+00:00",
            "updated_at": "2026-12-01T16:45:00+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 an estimate

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

Creates an estimate with the `simple` layout: one list of items. It
is a draft unless you send `"send": true`, in which case Couplr
emails it to the customer as soon as it is ready. The customer reads
and accepts it on Couplr's page. Nothing is charged.

A line with `price_book_item_id` uses that price book item and its
name. A line without one uses the price book item of the same name,
or adds the name to the price book as a new item. An item can appear
once: for two of the same thing, send one line with a quantity.

Discounts, deposits, maintenance plans and the `sectioned` layout
are set in Couplr and cannot be created here. Sales tax follows the
account's tax settings.

**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 estimate always has the `simple` layout.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | integer | Yes |  |
| `title` | string or null | No | A name for the estimate. It is shown on the estimate in Couplr's lists and search. |
| `notes` | string or null | No |  |
| `send` | boolean | No | Email the estimate to the customer once it is ready. The customer must have an email address. Default: `false`. |
| `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 used. |
| `line_items[].name` | string or null | No | Required unless `price_book_item_id` is sent. |
| `line_items[].quantity` | integer | No | A whole number. Default: `1`. |
| `line_items[].unit_price` | number or null | No | Dollars. Required unless `price_book_item_id` is sent, when it defaults to the item's price. |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/estimates" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"customer_id":1842,"title":"New condenser fan motor","notes":"Price includes disposal of the old motor.","line_items":[{"name":"Condenser fan motor","quantity":1,"unit_price":289},{"price_book_item_id":771,"quantity":1}]}'
```

**Example response** (`201`)

```json
{
    "data": {
        "id": 511,
        "layout": "simple",
        "title": "Replace condenser fan motor",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "status": "accepted",
        "total": 389,
        "accepted_at": "2026-12-01T16:45:00+00:00",
        "declined_at": null,
        "created_at": "2026-11-30T14:10:00+00:00",
        "updated_at": "2026-12-01T16:45:00+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 estimate

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

**Path parameters**

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

**Example request**

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

**Example response** (`200`)

```json
{
    "data": {
        "id": 511,
        "layout": "simple",
        "title": "Replace condenser fan motor",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "status": "accepted",
        "total": 389,
        "accepted_at": "2026-12-01T16:45:00+00:00",
        "declined_at": null,
        "created_at": "2026-11-30T14:10:00+00:00",
        "updated_at": "2026-12-01T16:45:00+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 an estimate

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

Emails the estimate to its customer, the same as "Send" in the apps.
There is no request body. An estimate can be sent more than once.

Either layout can be sent. A `sectioned` estimate also goes to the
extra recipients saved on it in Couplr, and its `status` can still
read `draft` afterwards.

Refused with `422` on `estimate` when: it has already been
accepted or declined; the customer has no email address; or it was
created a moment ago and is still being prepared, in which case try
again shortly.

**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". |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/estimates/1842/send" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Example response** (`200`)

```json
{
    "data": {
        "id": 511,
        "layout": "simple",
        "title": "New condenser fan motor",
        "customer_id": 1842,
        "customer_name": "Dana Whitfield",
        "status": "sent",
        "total": 389,
        "accepted_at": null,
        "declined_at": null,
        "created_at": "2026-11-30T14:10:00+00:00",
        "updated_at": "2026-11-30T14:12:00+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. |

## Estimate object

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer |  |
| `layout` | string: `simple`, `sectioned` | `simple` is one list of items. `sectioned` groups the items under headings. |
| `title` | string or null |  |
| `customer_id` | integer |  |
| `customer_name` | string or null |  |
| `status` | string: `draft`, `sent`, `accepted`, `declined` |  |
| `total` | number or null | Dollars. |
| `accepted_at` | string (date-time) or null |  |
| `declined_at` | string (date-time) or null |  |
| `created_at` | string (date-time) or null |  |
| `updated_at` | string (date-time) or null |  |
| `line_items` | array of objects | On a single estimate with the `simple` layout 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. |
