# Webhook subscriptions

> Add, change and remove the destinations Couplr sends events to.

- Canonical URL: https://couplrfsm.com/developers/api-reference/webhook-subscriptions
- Markdown: https://couplrfsm.com/developers/api-reference/webhook-subscriptions.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:** This is how an app tells me where to send events, without anyone pasting a URL into the Integrations page. I show the signing secret once, when the subscription is created.

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 webhook subscriptions

`GET /api/v1/webhook-subscriptions` · requires the `webhooks:manage` ability

Every outbound webhook destination of the company, including ones added on the Integrations page. Not paginated.

**Example request**

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

**Example response** (`200`)

```json
{
    "data": [
        {
            "id": 14,
            "url": "https://example.com/couplr-events",
            "description": "Accounting sync",
            "events": [
                "invoice.paid",
                "job.completed"
            ],
            "active": true,
            "disabled_reason": null,
            "last_delivery_at": null,
            "created_at": "2026-11-14T15:10: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. |
| `429` | Too many requests (`rate_limited`). Wait `Retry-After` seconds. |

## Create a webhook subscription

`POST /api/v1/webhook-subscriptions` · requires the `webhooks:manage` ability · accepts `Idempotency-Key`

Couplr will `POST` each chosen event to the URL. The response includes
the signing `secret`, which is shown only this once: store it and use
it to verify `X-Hvacpro-Signature` on every delivery.

The subscription is tied to the API key that created it. Revoking that
key turns the subscription off (`active: false`, `disabled_reason:
"Creating API key revoked"`), and while the key is expired nothing is
delivered.

**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 |
| --- | --- | --- | --- |
| `url` | string | Yes | Must start with `https://` and use a public hostname. |
| `description` | string or null | No |  |
| `events` | array of strings | Yes | Event names from the outbound webhook event list, or `*` for every event. |
| `active` | boolean | No |  |

**Example request**

```bash
curl -X POST "https://couplrfsm.com/api/v1/webhook-subscriptions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"url":"https://example.com/couplr-events","events":["invoice.paid","job.completed"],"description":"Accounting sync"}'
```

**Example response** (`201`)

```json
{
    "data": {
        "id": 14,
        "url": "https://example.com/couplr-events",
        "description": "Accounting sync",
        "events": [
            "invoice.paid",
            "job.completed"
        ],
        "active": true,
        "disabled_reason": null,
        "last_delivery_at": null,
        "created_at": "2026-11-14T15:10:00+00:00",
        "secret": "YOUR_SIGNING_SECRET"
    }
}
```

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

## Update a webhook subscription

`PATCH /api/v1/webhook-subscriptions/{id}` · requires the `webhooks:manage` ability · accepts `Idempotency-Key`

Change the URL, the events or the description, or turn it off and on. Turning one back on clears its failure count.

**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 |
| --- | --- | --- | --- |
| `url` | string | No | Must start with `https://` and use a public hostname. |
| `description` | string or null | No |  |
| `events` | array of strings | No | Event names from the outbound webhook event list, or `*` for every event. |
| `active` | boolean | No |  |

**Example request**

```bash
curl -X PATCH "https://couplrfsm.com/api/v1/webhook-subscriptions/1842" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -d '{"active":false}'
```

**Example response** (`200`)

```json
{
    "data": {
        "id": 14,
        "url": "https://example.com/couplr-events",
        "description": "Accounting sync",
        "events": [
            "invoice.paid",
            "job.completed"
        ],
        "active": true,
        "disabled_reason": null,
        "last_delivery_at": null,
        "created_at": "2026-11-14T15:10: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`). |
| `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. |

## Delete a webhook subscription

`DELETE /api/v1/webhook-subscriptions/{id}` · requires the `webhooks:manage` ability

Stops deliveries and removes the subscription and its delivery history.

**Path parameters**

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

**Example request**

```bash
curl -X DELETE "https://couplrfsm.com/api/v1/webhook-subscriptions/1842" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

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

## WebhookSubscription object

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer |  |
| `url` | string |  |
| `description` | string or null |  |
| `events` | array of strings | Event names, or `*` for every event. |
| `active` | boolean | `false` when turned off, or turned off automatically after repeated failed deliveries. |
| `disabled_reason` | string or null |  |
| `last_delivery_at` | string (date-time) or null |  |
| `created_at` | string (date-time) or null |  |
