# Webhook Endpoints

## Register a webhook endpoint

**post** `/v1/webhook_endpoints`

Registers a destination for this brand's events and mints its signing
secret.

The `201` body carries the secret in plain text, and that is the only
time it is ever shown: store it where your receiver can read it before
you finish the call. Verify with any Standard Webhooks library.

The URL is checked here so an unusable one is refused synchronously
rather than becoming silent delivery failures — but the check is a
courtesy, not the boundary: every attempt re-checks the address it is
actually about to connect to, so a name that resolves somewhere else
later is still refused then.

Cleartext endpoints are never deliverable: `http://` is refused here
and the scheme is checked again before every attempt. Loopback and
private addresses are refused too, so a receiver running only on
localhost cannot use webhooks. Use `GET /v1/streams/events` while developing
locally, until you have a public TLS endpoint.

### Body Parameters

- `url: string`

  Where to POST events. Must be absolute and `https`, and must not carry
  credentials in the url.

  **What is checked WHEN.** Registration refuses the faults that are
  visible in the url itself: the scheme, credentials, and a literal IP
  address that is private, loopback or link-local. It does NOT resolve
  hostnames, so a NAME pointing at a private address is accepted here and
  refused later, at every delivery attempt, when the address it actually
  resolves to is checked immediately before the connection is made. That
  second check is the one that is load-bearing, and it cannot be done at
  registration: a name can resolve differently a second after you register
  it.

  So a synchronous `422` means the url can never work. A `201` means it
  looked fine — watch the endpoint's health for what happened next.

- `description: optional string`

  Your own label for this endpoint. Text a human reads: control characters are refused.

- `event_kinds: optional array of string`

  The event kinds to deliver here. Omit it — or send an empty array — for
  every kind, including kinds added later, which is what most integrations
  want. Kinds are the same values the stream's `event:` carries.

### Returns

- `EndpointCreated object { id, created_at, disabled_at, 10 more }`

  A newly registered endpoint, together with its signing secret. The secret
  appears in this response and never again.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `secret: string`

    The signing secret, in the Standard Webhooks presentation form
    (`whsec_` followed by base64). **Shown exactly once, here.** It is not
    recoverable from any later read — if you lose it, add a second secret
    and retire this one.

  - `secret_id: string`

    The id of the secret above — the handle you retire it by.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Example

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

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "disabled_at": "2019-12-27T18:11:19.117Z",
  "event_kinds": [
    "string"
  ],
  "health": {
    "consecutive_failures": 0,
    "last_failure_at": "2019-12-27T18:11:19.117Z",
    "last_failure_reason": "last_failure_reason",
    "last_failure_status": 0,
    "last_success_at": "2019-12-27T18:11:19.117Z"
  },
  "secret": "secret",
  "secret_id": "secret_id",
  "status": "status",
  "updated_at": "2019-12-27T18:11:19.117Z",
  "url": "url",
  "backlog_dropped": 0,
  "backlog_resume_cursor": "backlog_resume_cursor",
  "description": "description"
}
```

## Delete a webhook endpoint

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

Removes an endpoint. Its queued events are dropped, its signing secrets
are retired, and its URL becomes free to register again. The response is
the endpoint as it was left.

### Path Parameters

- `endpoint: string`

### Returns

- `Endpoint object { id, created_at, disabled_at, 8 more }`

  A destination this brand's events are delivered to.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Example

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

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "disabled_at": "2019-12-27T18:11:19.117Z",
  "event_kinds": [
    "string"
  ],
  "health": {
    "consecutive_failures": 0,
    "last_failure_at": "2019-12-27T18:11:19.117Z",
    "last_failure_reason": "last_failure_reason",
    "last_failure_status": 0,
    "last_success_at": "2019-12-27T18:11:19.117Z"
  },
  "status": "status",
  "updated_at": "2019-12-27T18:11:19.117Z",
  "url": "url",
  "backlog_dropped": 0,
  "backlog_resume_cursor": "backlog_resume_cursor",
  "description": "description"
}
```

## List webhook endpoints

**get** `/v1/webhook_endpoints`

Lists this brand's webhook endpoints, oldest first, each with its delivery health.

### Returns

- `EndpointList object { data }`

  The endpoints registered for this key's brand.

  - `data: array of Endpoint`

    - `id: string`

      The endpoint's id.

    - `created_at: string`

    - `disabled_at: string`

      When this endpoint was last disabled. `null` while it is enabled.

    - `event_kinds: array of string`

      The event kinds delivered here, or `null` for every kind (including kinds added later).

    - `health: EndpointHealth`

      How a destination has been behaving, taken from the deliveries actually
      attempted against it. Worth reading before assuming an integration is
      healthy: a destination that has been refusing events shows up here well
      before anyone notices they are missing.

      - `consecutive_failures: number`

        Failed attempts since the last success. Reset to 0 by any success.

      - `last_failure_at: string`

        When a delivery to this endpoint last failed. `null` if none ever has.

      - `last_failure_reason: string`

        A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

      - `last_failure_status: number`

        The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

      - `last_success_at: string`

        When a delivery to this endpoint last succeeded. `null` if none ever has.

    - `status: string`

      `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

    - `updated_at: string`

    - `url: string`

      Where events are POSTed. `https` only.

    - `backlog_dropped: optional number`

      How many undelivered events were dropped by THIS request, present only
      on the response to a request that disabled or deleted the endpoint.
      Disabling is a cutoff: everything still queued for this endpoint is
      dropped then and there rather than waiting to be replayed later.

    - `backlog_resume_cursor: optional string`

      Where to pick the dropped events back up: pass it as the `cursor` query
      parameter to `GET /v1/streams/events` and the first event you receive is the
      first one dropped. Present alongside `backlog_dropped` and only then.

      **Keep it if you rely on webhooks alone.** A webhook carries the event's
      identity but not its position in the stream, so this response is the only
      place a push-only integration is handed the position of the gap it just
      created. Reconnecting without a cursor starts at the present moment and
      skips the gap entirely.

      It stays valid while the events behind it are retained; past that the
      stream answers `410 cursor_expired` rather than pretending.

    - `description: optional string`

      Your own label for this endpoint.

### Example

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

#### Response

```json
{
  "data": [
    {
      "id": "id",
      "created_at": "2019-12-27T18:11:19.117Z",
      "disabled_at": "2019-12-27T18:11:19.117Z",
      "event_kinds": [
        "string"
      ],
      "health": {
        "consecutive_failures": 0,
        "last_failure_at": "2019-12-27T18:11:19.117Z",
        "last_failure_reason": "last_failure_reason",
        "last_failure_status": 0,
        "last_success_at": "2019-12-27T18:11:19.117Z"
      },
      "status": "status",
      "updated_at": "2019-12-27T18:11:19.117Z",
      "url": "url",
      "backlog_dropped": 0,
      "backlog_resume_cursor": "backlog_resume_cursor",
      "description": "description"
    }
  ]
}
```

## Get a webhook endpoint

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

Reads one endpoint, including its delivery health.

### Path Parameters

- `endpoint: string`

### Returns

- `Endpoint object { id, created_at, disabled_at, 8 more }`

  A destination this brand's events are delivered to.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Example

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

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "disabled_at": "2019-12-27T18:11:19.117Z",
  "event_kinds": [
    "string"
  ],
  "health": {
    "consecutive_failures": 0,
    "last_failure_at": "2019-12-27T18:11:19.117Z",
    "last_failure_reason": "last_failure_reason",
    "last_failure_status": 0,
    "last_success_at": "2019-12-27T18:11:19.117Z"
  },
  "status": "status",
  "updated_at": "2019-12-27T18:11:19.117Z",
  "url": "url",
  "backlog_dropped": 0,
  "backlog_resume_cursor": "backlog_resume_cursor",
  "description": "description"
}
```

## Update a webhook endpoint

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

Updates an endpoint: its destination, its label, which kinds it
receives, or whether it is enabled.

Read `status` before disabling anything — disabling drops what is
queued.

### Path Parameters

- `endpoint: string`

### Body Parameters

- `description: optional string`

  A new label. Text a human reads: control characters are refused.

- `event_kinds: optional array of string`

  Replaces the subscription list outright. An empty array means every kind.

- `status: optional string`

  `enabled` or `disabled`.

  **Disabling is a cutoff, not a pause.** Every event still queued for
  this endpoint is dropped when you disable it, and while it is disabled no
  new event is queued for it. Enabling it again resumes with what happens
  next — there is no backlog to replay, by design: a destination that has
  just come back up is the worst possible moment to hand it hours of stale
  events in one burst.

  **Nothing is lost, and the response tells you how to get it.**
  `backlog_dropped` says how many events were dropped and
  `backlog_resume_cursor` says where they start; pass that cursor to
  `GET /v1/streams/events` to read them. Keep it — a webhook does not carry a
  stream position, so this response is the only place a push-only
  integration is handed one.

  One delivery may still arrive after this call returns: an attempt already
  on the wire cannot be recalled, and it is counted in `backlog_dropped`
  even if it lands. The count is an upper bound on what was lost.

- `url: optional string`

  A new destination. Validated exactly as at registration. Events already queued go to the new url — a delivery in flight when you change it re-reads the endpoint before it dials, so it follows the change too.

### Returns

- `Endpoint object { id, created_at, disabled_at, 8 more }`

  A destination this brand's events are delivered to.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Example

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

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "disabled_at": "2019-12-27T18:11:19.117Z",
  "event_kinds": [
    "string"
  ],
  "health": {
    "consecutive_failures": 0,
    "last_failure_at": "2019-12-27T18:11:19.117Z",
    "last_failure_reason": "last_failure_reason",
    "last_failure_status": 0,
    "last_success_at": "2019-12-27T18:11:19.117Z"
  },
  "status": "status",
  "updated_at": "2019-12-27T18:11:19.117Z",
  "url": "url",
  "backlog_dropped": 0,
  "backlog_resume_cursor": "backlog_resume_cursor",
  "description": "description"
}
```

## Domain Types

### Endpoint

- `Endpoint object { id, created_at, disabled_at, 8 more }`

  A destination this brand's events are delivered to.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Endpoint Created

- `EndpointCreated object { id, created_at, disabled_at, 10 more }`

  A newly registered endpoint, together with its signing secret. The secret
  appears in this response and never again.

  - `id: string`

    The endpoint's id.

  - `created_at: string`

  - `disabled_at: string`

    When this endpoint was last disabled. `null` while it is enabled.

  - `event_kinds: array of string`

    The event kinds delivered here, or `null` for every kind (including kinds added later).

  - `health: EndpointHealth`

    How a destination has been behaving, taken from the deliveries actually
    attempted against it. Worth reading before assuming an integration is
    healthy: a destination that has been refusing events shows up here well
    before anyone notices they are missing.

    - `consecutive_failures: number`

      Failed attempts since the last success. Reset to 0 by any success.

    - `last_failure_at: string`

      When a delivery to this endpoint last failed. `null` if none ever has.

    - `last_failure_reason: string`

      A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

    - `last_failure_status: number`

      The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

    - `last_success_at: string`

      When a delivery to this endpoint last succeeded. `null` if none ever has.

  - `secret: string`

    The signing secret, in the Standard Webhooks presentation form
    (`whsec_` followed by base64). **Shown exactly once, here.** It is not
    recoverable from any later read — if you lose it, add a second secret
    and retire this one.

  - `secret_id: string`

    The id of the secret above — the handle you retire it by.

  - `status: string`

    `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

  - `updated_at: string`

  - `url: string`

    Where events are POSTed. `https` only.

  - `backlog_dropped: optional number`

    How many undelivered events were dropped by THIS request, present only
    on the response to a request that disabled or deleted the endpoint.
    Disabling is a cutoff: everything still queued for this endpoint is
    dropped then and there rather than waiting to be replayed later.

  - `backlog_resume_cursor: optional string`

    Where to pick the dropped events back up: pass it as the `cursor` query
    parameter to `GET /v1/streams/events` and the first event you receive is the
    first one dropped. Present alongside `backlog_dropped` and only then.

    **Keep it if you rely on webhooks alone.** A webhook carries the event's
    identity but not its position in the stream, so this response is the only
    place a push-only integration is handed the position of the gap it just
    created. Reconnecting without a cursor starts at the present moment and
    skips the gap entirely.

    It stays valid while the events behind it are retained; past that the
    stream answers `410 cursor_expired` rather than pretending.

  - `description: optional string`

    Your own label for this endpoint.

### Endpoint Health

- `EndpointHealth object { consecutive_failures, last_failure_at, last_failure_reason, 2 more }`

  How a destination has been behaving, taken from the deliveries actually
  attempted against it. Worth reading before assuming an integration is
  healthy: a destination that has been refusing events shows up here well
  before anyone notices they are missing.

  - `consecutive_failures: number`

    Failed attempts since the last success. Reset to 0 by any success.

  - `last_failure_at: string`

    When a delivery to this endpoint last failed. `null` if none ever has.

  - `last_failure_reason: string`

    A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

  - `last_failure_status: number`

    The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

  - `last_success_at: string`

    When a delivery to this endpoint last succeeded. `null` if none ever has.

### Endpoint List

- `EndpointList object { data }`

  The endpoints registered for this key's brand.

  - `data: array of Endpoint`

    - `id: string`

      The endpoint's id.

    - `created_at: string`

    - `disabled_at: string`

      When this endpoint was last disabled. `null` while it is enabled.

    - `event_kinds: array of string`

      The event kinds delivered here, or `null` for every kind (including kinds added later).

    - `health: EndpointHealth`

      How a destination has been behaving, taken from the deliveries actually
      attempted against it. Worth reading before assuming an integration is
      healthy: a destination that has been refusing events shows up here well
      before anyone notices they are missing.

      - `consecutive_failures: number`

        Failed attempts since the last success. Reset to 0 by any success.

      - `last_failure_at: string`

        When a delivery to this endpoint last failed. `null` if none ever has.

      - `last_failure_reason: string`

        A short reason for the last failure, written for a human reading it. The wording is not stable; do not branch on it.

      - `last_failure_status: number`

        The HTTP status of that last failure. `null` when the attempt never reached a response at all — a name that did not resolve, a refused connection, a timeout, or an address we will not dial.

      - `last_success_at: string`

        When a delivery to this endpoint last succeeded. `null` if none ever has.

    - `status: string`

      `enabled` or `disabled`. A disabled endpoint receives nothing, and events are not queued for it — see the update operation. Values grow additively.

    - `updated_at: string`

    - `url: string`

      Where events are POSTed. `https` only.

    - `backlog_dropped: optional number`

      How many undelivered events were dropped by THIS request, present only
      on the response to a request that disabled or deleted the endpoint.
      Disabling is a cutoff: everything still queued for this endpoint is
      dropped then and there rather than waiting to be replayed later.

    - `backlog_resume_cursor: optional string`

      Where to pick the dropped events back up: pass it as the `cursor` query
      parameter to `GET /v1/streams/events` and the first event you receive is the
      first one dropped. Present alongside `backlog_dropped` and only then.

      **Keep it if you rely on webhooks alone.** A webhook carries the event's
      identity but not its position in the stream, so this response is the only
      place a push-only integration is handed the position of the gap it just
      created. Reconnecting without a cursor starts at the present moment and
      skips the gap entirely.

      It stays valid while the events behind it are retained; past that the
      stream answers `410 cursor_expired` rather than pretending.

    - `description: optional string`

      Your own label for this endpoint.

# Replays

## Replay retained webhook events

**post** `/v1/webhook_endpoints/{endpoint}/replays`

Accept a bounded replay of events still present in account history.
Account-event history has no fixed minimum retention period in beta.
Retrying the same acceptance key recovers its response for 24 hours.

### Path Parameters

- `endpoint: string`

### Header Parameters

- `"Idempotency-Key": string`

### Body Parameters

- `after_cursor: optional string`

  Exclusive event-log cursor. Omit to begin inclusively at retained history.

- `through_cursor: optional string`

  Inclusive event-log cursor. Omit to snapshot the committed tail.

### Returns

- `ReplayAcceptance object { id, after_cursor, created_at, 3 more }`

  - `id: string`

  - `after_cursor: string`

  - `created_at: string`

  - `selected: number`

  - `state: "pending"`

    - `"pending"`

  - `through_cursor: string`

### Example

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

#### Response

```json
{
  "id": "id",
  "after_cursor": "after_cursor",
  "created_at": "2019-12-27T18:11:19.117Z",
  "selected": 0,
  "state": "pending",
  "through_cursor": "through_cursor"
}
```

## Get webhook replay status

**get** `/v1/webhook_endpoints/{endpoint}/replays/{replay}`

Read durable replay progress and its immutable terminal verdict.
During ordinary operation, a successfully completed replay remains
readable for at least 30 days after `terminal_at`, then returns the same
tenant-scoped 404 as an unknown replay. Pending and running replays are
never age-swept; failed replay evidence has no scheduled expiry.

### Path Parameters

- `endpoint: string`

- `replay: string`

### Returns

- `Replay object { id, after_cursor, created_at, 12 more }`

  - `id: string`

  - `after_cursor: string`

  - `created_at: string`

  - `delivered: number`

  - `delivering: number`

  - `exhausted: number`

  - `failure_reason: string`

  - `materialized: number`

  - `pending: number`

  - `selected: number`

  - `started_at: string`

  - `state: "pending" or "running" or "completed" or "failed"`

    - `"pending"`

    - `"running"`

    - `"completed"`

    - `"failed"`

  - `terminal_at: string`

  - `through_cursor: string`

  - `unmaterialized: number`

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/webhook_endpoints/$ENDPOINT/replays/$REPLAY \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "id": "id",
  "after_cursor": "after_cursor",
  "created_at": "2019-12-27T18:11:19.117Z",
  "delivered": 0,
  "delivering": 0,
  "exhausted": 0,
  "failure_reason": "failure_reason",
  "materialized": 0,
  "pending": 0,
  "selected": 0,
  "started_at": "2019-12-27T18:11:19.117Z",
  "state": "pending",
  "terminal_at": "2019-12-27T18:11:19.117Z",
  "through_cursor": "through_cursor",
  "unmaterialized": 0
}
```

## Domain Types

### Replay

- `Replay object { id, after_cursor, created_at, 12 more }`

  - `id: string`

  - `after_cursor: string`

  - `created_at: string`

  - `delivered: number`

  - `delivering: number`

  - `exhausted: number`

  - `failure_reason: string`

  - `materialized: number`

  - `pending: number`

  - `selected: number`

  - `started_at: string`

  - `state: "pending" or "running" or "completed" or "failed"`

    - `"pending"`

    - `"running"`

    - `"completed"`

    - `"failed"`

  - `terminal_at: string`

  - `through_cursor: string`

  - `unmaterialized: number`

### Replay Acceptance

- `ReplayAcceptance object { id, after_cursor, created_at, 3 more }`

  - `id: string`

  - `after_cursor: string`

  - `created_at: string`

  - `selected: number`

  - `state: "pending"`

    - `"pending"`

  - `through_cursor: string`

# Secrets

## Add a signing secret

**post** `/v1/webhook_endpoints/{endpoint}/secrets`

Adds a second signing secret to an endpoint — the first half of a
rotation.

Every ACTIVE secret signs every event, and all of the signatures ride
the one `webhook-signature` header (space delimited). A verifier that
accepts any one of them — which is what every Standard Webhooks library
does — keeps working throughout. So the rotation is: add a secret here,
deploy it to your receiver, confirm traffic is verifying against it,
then retire the old one. No delivery is dropped at any point.

The signing set is read fresh for each delivery, immediately before it is
sent. A delivery already on the wire when this call returns may therefore
carry the previous set — the overlap is what makes that harmless, and it
is why you deploy the new secret before retiring the old one rather than
relying on an instant.

An endpoint may hold up to five ACTIVE secrets at once; past that the
answer is HTTP 409 `too_many_active_secrets`. Every one of them signs
every event, so the header grows with the count — a rotation needs two,
and the rest of the room is for one you started and did not finish.

The new secret is shown exactly once, in this response.

### Path Parameters

- `endpoint: string`

### Returns

- `SigningSecretCreated object { id, created_at, retired_at, 2 more }`

  A newly added signing secret, with its value — shown exactly once.

  - `id: string`

    The secret's id. Safe to log: it is a handle, not key material.

  - `created_at: string`

  - `retired_at: string`

    When this secret stopped signing. `null` while it is active.

  - `secret: string`

    The signing secret in presentation form (`whsec_` + base64). **Shown exactly once, here.**

  - `status: string`

    `active` or `retired`. Values grow additively.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/webhook_endpoints/$ENDPOINT/secrets \
    -X POST \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "retired_at": "2019-12-27T18:11:19.117Z",
  "secret": "secret",
  "status": "status"
}
```

## Retire a signing secret

**delete** `/v1/webhook_endpoints/{endpoint}/secrets/{secret}`

Retires a signing secret — the second half of a rotation. It stops
signing immediately; signatures already sent are unaffected.

An endpoint's LAST active secret cannot be retired: an endpoint that
could not sign would have to stop delivering, and silently unsigned
webhooks are indistinguishable from forgeries. Add the replacement
first.

### Path Parameters

- `endpoint: string`

- `secret: string`

### Returns

- `SigningSecret object { id, created_at, retired_at, status }`

  One signing secret. Every ACTIVE secret signs every event, and all of the
  signatures ride the one `webhook-signature` header — which is what makes a
  rotation lossless.

  - `id: string`

    The secret's id. Safe to log: it is a handle, not key material.

  - `created_at: string`

  - `retired_at: string`

    When this secret stopped signing. `null` while it is active.

  - `status: string`

    `active` or `retired`. Values grow additively.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/webhook_endpoints/$ENDPOINT/secrets/$SECRET \
    -X DELETE \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "retired_at": "2019-12-27T18:11:19.117Z",
  "status": "status"
}
```

## List signing secrets

**get** `/v1/webhook_endpoints/{endpoint}/secrets`

Lists an endpoint's signing secrets — ids and status only, never the
key material. Oldest first, which is the order their signatures appear
in the header.

### Path Parameters

- `endpoint: string`

### Returns

- `SigningSecretList object { data }`

  An endpoint's signing secrets, oldest first — the order they sign in.

  - `data: array of SigningSecret`

    - `id: string`

      The secret's id. Safe to log: it is a handle, not key material.

    - `created_at: string`

    - `retired_at: string`

      When this secret stopped signing. `null` while it is active.

    - `status: string`

      `active` or `retired`. Values grow additively.

### Example

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

#### Response

```json
{
  "data": [
    {
      "id": "id",
      "created_at": "2019-12-27T18:11:19.117Z",
      "retired_at": "2019-12-27T18:11:19.117Z",
      "status": "status"
    }
  ]
}
```

## Domain Types

### Signing Secret

- `SigningSecret object { id, created_at, retired_at, status }`

  One signing secret. Every ACTIVE secret signs every event, and all of the
  signatures ride the one `webhook-signature` header — which is what makes a
  rotation lossless.

  - `id: string`

    The secret's id. Safe to log: it is a handle, not key material.

  - `created_at: string`

  - `retired_at: string`

    When this secret stopped signing. `null` while it is active.

  - `status: string`

    `active` or `retired`. Values grow additively.

### Signing Secret Created

- `SigningSecretCreated object { id, created_at, retired_at, 2 more }`

  A newly added signing secret, with its value — shown exactly once.

  - `id: string`

    The secret's id. Safe to log: it is a handle, not key material.

  - `created_at: string`

  - `retired_at: string`

    When this secret stopped signing. `null` while it is active.

  - `secret: string`

    The signing secret in presentation form (`whsec_` + base64). **Shown exactly once, here.**

  - `status: string`

    `active` or `retired`. Values grow additively.

### Signing Secret List

- `SigningSecretList object { data }`

  An endpoint's signing secrets, oldest first — the order they sign in.

  - `data: array of SigningSecret`

    - `id: string`

      The secret's id. Safe to log: it is a handle, not key material.

    - `created_at: string`

    - `retired_at: string`

      When this secret stopped signing. `null` while it is active.

    - `status: string`

      `active` or `retired`. Values grow additively.
