Skip to content
Linq Copy agent prompt

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 ParametersJSONExpand Collapse
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.

maxLength2000
description: optional string

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

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

phone_number_ids: optional array of string

The phone numbers whose events to deliver here. Omit it — or send an empty array — for every current and future number, which is what most integrations want. A non-empty list names exact phone-number ids from GET /v1/phone_numbers for this account and mode. An id this account cannot use — unknown, belonging to another account or to the other mode, or not an active number of this account — is refused with HTTP 422 (webhook_phone_number_not_found) pointered at the list, /phone_number_ids, never at the id, so the refusal discloses nothing about whose number it is; only an id that is malformed (empty, or over 128 bytes) is pointered at its own index, /phone_number_ids/{i}, before any lookup. Nothing is created. Account-wide events that carry no phone number go only to an unscoped endpoint.

ReturnsExpand Collapse
EndpointCreated object { id, created_at, disabled_at, 11 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
formatdate-time
disabled_at: string

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

formatdate-time
event_kinds: array of string

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

health: EndpointHealth { 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.

formatint32
last_failure_at: string

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

formatdate-time
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.

formatint32
last_success_at: string

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

formatdate-time
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
formatdate-time
url: string

Where events are POSTed. https only.

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

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

maxLength200
phone_number_ids: optional array of string

The phone numbers whose events are delivered here: the exact registered phone-number ids selected for this destination, or null for every current and future number (the default). Ids are the ones GET /v1/phone_numbers returns for this account and mode, and every one listed was an active number of this account when it was accepted.

Register a webhook endpoint

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"
        }'
{
  "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",
  "phone_number_ids": [
    "string"
  ]
}
Returns Examples
{
  "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",
  "phone_number_ids": [
    "string"
  ]
}