# Webhook Endpoints

## List webhook endpoints

**get** `/v1/webhook_endpoints`

List this brand's endpoints, newest first. Scoped to your key's brand by construction: no parameter names a brand.

### Query Parameters

- `cursor: optional string`

  `next_cursor` from the previous page.

- `limit: optional number`

  Page size, 1–100. Defaults to 25.

### Returns

- `data: array of WebhookEndpoint`

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

- `next_cursor: string`

  Empty when the list is exhausted. Always present.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

#### Response

```json
{
  "data": [
    {
      "id": "wh_7f3a1c9e",
      "url": "https://hooks.example.com/messages",
      "description": "production receiver",
      "enabled_events": [
        "message.received",
        "message.sent",
        "message.failed"
      ],
      "status": "enabled",
      "created_at": "2026-08-13T02:41:09Z"
    }
  ],
  "next_cursor": ""
}
```

## Create a webhook endpoint

**post** `/v1/webhook_endpoints`

Registers a URL and returns its signing secret.

**The secret is returned once, here.** Nothing reads it back afterwards.

A brand may hold up to 16 endpoints; a seventeenth is refused `409`
`endpoint_limit_reached`.

### Body Parameters

- `enabled_events: array of string`

  The events this endpoint should receive. Must be non-empty, and every name must be one this API emits: a name we do not emit is refused rather than accepted and then matching nothing.

- `url: string`

  Absolute `https` URL. A private or loopback address is refused here rather than failing silently later.

- `description: optional string`

  Your own label, ≤200 bytes.

### Returns

- `WebhookEndpointWithSecret object { id, created_at, description, 4 more }`

  A newly created or newly rotated endpoint. **The `secret` is on this
  response and on no other**; store it now; it is never readable again, and
  a lost secret is replaced by rotating rather than by looking it up.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `secret: string`

    `whsec_…`. Sign-verify every delivery with it. Shown once.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{
          "enabled_events": [
            "message.received",
            "message.sent",
            "message.failed"
          ],
          "url": "https://hooks.example.com/messages"
        }'
```

#### Response

```json
{
  "id": "wh_7f3a1c9e",
  "url": "https://hooks.example.com/messages",
  "description": "production receiver",
  "enabled_events": [
    "message.received",
    "message.sent",
    "message.failed"
  ],
  "status": "enabled",
  "created_at": "2026-08-13T02:41:09Z",
  "secret": "whsec_9Qm2ZK8vTn1xR4pL7bYcW0sHdJfA6eUg3iOtN5rXvBk="
}
```

## Get a webhook endpoint

**get** `/v1/webhook_endpoints/{wh}`

Read one endpoint. Never returns the secret.

### Path Parameters

- `wh: string`

### Returns

- `WebhookEndpoint object { id, created_at, description, 3 more }`

  A URL your brand receives signed events on.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints/$WH \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

#### Response

```json
{
  "id": "wh_7f3a1c9e",
  "url": "https://hooks.example.com/messages",
  "description": "production receiver",
  "enabled_events": [
    "message.received",
    "message.sent",
    "message.failed"
  ],
  "status": "enabled",
  "created_at": "2026-08-13T02:41:09Z"
}
```

## Update a webhook endpoint

**patch** `/v1/webhook_endpoints/{wh}`

Change the URL, the subscribed events, the label, or whether it is delivering. Omitted fields are left alone.

### Path Parameters

- `wh: string`

### Body Parameters

- `description: optional string`

- `disabled: optional boolean`

  `true` stops deliveries without deleting the endpoint; `false` resumes them.

- `enabled_events: optional array of string`

- `url: optional string`

### Returns

- `WebhookEndpoint object { id, created_at, description, 3 more }`

  A URL your brand receives signed events on.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints/$WH \
    -X PATCH \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{}'
```

#### Response

```json
{
  "id": "wh_7f3a1c9e",
  "url": "https://hooks.example.com/messages",
  "description": "production receiver",
  "enabled_events": [
    "message.received",
    "message.failed"
  ],
  "status": "enabled",
  "created_at": "2026-08-13T02:41:09Z"
}
```

## Delete a webhook endpoint

**delete** `/v1/webhook_endpoints/{wh}`

Stops deliveries permanently and removes the endpoint from your list.

The record itself is retained so the deliveries it already received stay
attributable; it stops receiving, stops being listed, and stops
counting against your endpoint limit.

### Path Parameters

- `wh: string`

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints/$WH \
    -X DELETE \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

## Rotate a webhook endpoint's secret

**post** `/v1/webhook_endpoints/{wh}/rotate_secret`

Issues a new signing secret and returns it; once.

The old secret keeps verifying for an overlap window (default 24h, max
168h) so deliveries in flight and receivers mid-deploy do not fail. During
the window `webhook-signature` carries BOTH values, space-separated, and a
receiver that accepts either is correct throughout.

**One rotation at a time.** While a previous overlap window is still open, this
answers `409 rotation_in_flight`. A second window would push the OLDEST secret out
while your receivers may still be verifying with it: the stranding the overlap
exists to prevent, so the refusal is the guarantee rather than a limitation.

### Path Parameters

- `wh: string`

### Query Parameters

- `overlap_hours: optional number`

  How long the previous secret keeps verifying, in hours. 1–168, default 24.

### Returns

- `WebhookEndpointWithSecret object { id, created_at, description, 4 more }`

  A newly created or newly rotated endpoint. **The `secret` is on this
  response and on no other**; store it now; it is never readable again, and
  a lost secret is replaced by rotating rather than by looking it up.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `secret: string`

    `whsec_…`. Sign-verify every delivery with it. Shown once.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints/$WH/rotate_secret \
    -X POST \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY"
```

#### Response

```json
{
  "id": "wh_7f3a1c9e",
  "url": "https://hooks.example.com/messages",
  "description": "production receiver",
  "enabled_events": [
    "message.received",
    "message.sent",
    "message.failed"
  ],
  "status": "enabled",
  "created_at": "2026-08-13T02:41:09Z",
  "secret": "whsec_2Ld8YpVx0QaZ6nMbT3wKcR9jEsHf1uGi7oPvN4rXyBk="
}
```

## Replay past events to a webhook endpoint

**post** `/v1/webhook_endpoints/{wh}/replay`

Deliver past events to this endpoint.

An endpoint receives events that happen while it exists. Register one on day 5
and days 1 to 4 are not waiting for it. This is how you fetch them: name the
events, or a time range, and every matching event with no delivery to this
endpoint gets one.

**A replayed delivery arrives behind live traffic, not in front of it.** Within a
chat, deliveries arrive in order and each waits for the one before it. A replay of
an old event would otherwise sort ahead of everything current and hold up the
traffic you are serving right now, so replays queue after live work instead. The
in-order guarantee still holds for live traffic; a replay is the one thing that
arrives out of its original place, by design.

**Sending the same request twice creates nothing the second time.** Each call gets
its own `request_id`, so two calls are two requests, but a delivery that already
exists is not made again. The response counts `created` and `skipped` separately
so a retry after a timeout tells you what actually happened.

**A range larger than `max` is refused, not trimmed.** A trimmed replay looks like
a finished one, and you would believe you had caught up. Narrow the range or raise
`max`, then send it again.

**It follows this endpoint's `enabled_events`.** An event type this endpoint does
not subscribe to is not in scope for its replay, the same way it would not have
been delivered live. Narrow or widen the subscription to change what a replay can
reach. An endpoint that is disabled, or whose brand's agent is paused, answers
`409 webhook_endpoint_not_receiving` rather than reporting nothing to do.

Every delivery this creates carries `replay_of` set to the `request_id`, so you can
tell a replay from live traffic on arrival.

### Path Parameters

- `wh: string`

### Body Parameters

- `event_ids: optional array of string`

  Replay exactly these events, at most 1000 per call. Send this or a range,
  not both. They ask two different questions, and answering the wrong one is
  worse than refusing.

- `max: optional number`

  Most events one call may replay from a range. 1-1000, default 500. Send
  it with a range, not with `event_ids`: a list you named is already its
  own bound, so a `max` beside it would have nothing to do.

- `since: optional string`

  Replay events at or after this instant.

- `type: optional string`

  Narrow a range to one event type, e.g. `message.received`. It narrows;
  it does not select. Sent on its own it is refused, because a bare type
  would replay your whole history of that type.

- `until: optional string`

  Replay events at or before this instant.

### Returns

- `created: number`

  Deliveries created. These are queued behind whatever live traffic the chat has.

- `not_found: number`

  Ids you named with no event of yours behind them: a typo, or an event old
  enough to have been swept. Always 0 for a range.

- `not_subscribed: number`

  Events you named that this endpoint does not subscribe to, so they were never
  in scope for it. Always 0 for a range, where the subscription narrows the
  window rather than rejecting something you pointed at.

- `request_id: string`

  This request's id, `rpl_…`. Every delivery it created carries it as `replay_of`.

- `skipped: number`

  Events skipped because this endpoint already has a delivery for them.

### Example

```http
curl https://messages.api.linqapp.com/v1/webhook_endpoints/$WH/replay \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{}'
```

#### Response

```json
{
  "request_id": "rpl_9c2f1a7b",
  "created": 128,
  "skipped": 4,
  "not_subscribed": 0,
  "not_found": 0
}
```

## Domain Types

### Webhook Endpoint

- `WebhookEndpoint object { id, created_at, description, 3 more }`

  A URL your brand receives signed events on.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Webhook Endpoint With Secret

- `WebhookEndpointWithSecret object { id, created_at, description, 4 more }`

  A newly created or newly rotated endpoint. **The `secret` is on this
  response and on no other**; store it now; it is never readable again, and
  a lost secret is replaced by rotating rather than by looking it up.

  - `id: string`

    `wh_…`.

  - `created_at: string`

  - `description: string`

    Your own label. Free text, ≤200 bytes, never interpreted.

  - `enabled_events: array of string`

    The event types this endpoint receives. Exact names: no wildcards.

  - `secret: string`

    `whsec_…`. Sign-verify every delivery with it. Shown once.

  - `status: "enabled" or "disabled"`

    `enabled` while it receives deliveries, `disabled` once you turn it off.
    A disabled endpoint keeps its secret and its history and resumes on
    `PATCH {"disabled": false}`.

    - `"enabled"`

    - `"disabled"`

  - `url: string`

    Absolute `https` URL every delivery for this endpoint is POSTed to.

### Webhook Endpoint List Response

- `WebhookEndpointListResponse object { data, next_cursor }`

  - `data: array of WebhookEndpoint`

    - `id: string`

      `wh_…`.

    - `created_at: string`

    - `description: string`

      Your own label. Free text, ≤200 bytes, never interpreted.

    - `enabled_events: array of string`

      The event types this endpoint receives. Exact names: no wildcards.

    - `status: "enabled" or "disabled"`

      `enabled` while it receives deliveries, `disabled` once you turn it off.
      A disabled endpoint keeps its secret and its history and resumes on
      `PATCH {"disabled": false}`.

      - `"enabled"`

      - `"disabled"`

    - `url: string`

      Absolute `https` URL every delivery for this endpoint is POSTed to.

  - `next_cursor: string`

    Empty when the list is exhausted. Always present.

### Webhook Endpoint Replay Response

- `WebhookEndpointReplayResponse object { created, not_found, not_subscribed, 2 more }`

  What a replay did.

  When you name `event_ids`, the four counts add up to the number of DISTINCT ids
  you sent, so nothing you pointed at goes unaccounted for. Naming the same id
  twice names one event.

  - `created: number`

    Deliveries created. These are queued behind whatever live traffic the chat has.

  - `not_found: number`

    Ids you named with no event of yours behind them: a typo, or an event old
    enough to have been swept. Always 0 for a range.

  - `not_subscribed: number`

    Events you named that this endpoint does not subscribe to, so they were never
    in scope for it. Always 0 for a range, where the subscription narrows the
    window rather than rejecting something you pointed at.

  - `request_id: string`

    This request's id, `rpl_…`. Every delivery it created carries it as `replay_of`.

  - `skipped: number`

    Events skipped because this endpoint already has a delivery for them.
