Skip to content
WhatIsUp.dev
Esta página está disponible solo en inglés por ahora.

Webhook endpoints

Tell WhatIsUp.dev where to deliver event notifications. One customer can have multiple endpoints (different URLs for different event subsets). One channel can be the source of events for multiple endpoints, and one endpoint can subscribe to multiple channels — they're a many-to-many.

Conceptual model

Endpoints describe a subscription: where to send, which events to send, and the signing secret used for the HMAC. The actual delivery records are in Webhook deliveries.

api_key → customer ──┬── channel ◄───┐
                     │                 │
                     └── webhook_endpoint
                              │
                              └── webhook_delivery (per event)

List endpoints

GET/v1/webhook-endpointsBearer · API key

Returns every endpoint owned by the authenticated customer.

Create an endpoint

POST/v1/webhook-endpointsBearer · API key
Schema · Request body
FieldTypeRequiredNotes
urlstring · URLrequired
eventsarray<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `qr.updated` \| `group.created` \| `group.updated` \| `group.participant_added` \| `group.participant_removed` \| `group.admin_promoted` \| `group.admin_demoted` \| `chat.updated` \| `chat.archived` \| `chat.pinned` \| `presence.updated` \| `order.placed` \| `order.cancelled` \| `cart.updated` \| `story.viewed` \| `call.offered` \| `call.terminated` \| `contact.resolved`>required
channel_idstring · uuidoptional
signing_secretstringoptional
enabledbooleanoptional
chat_type_filter`all` \| `direct_only` \| `groups_only`optional
curl -sX POST "$WHATISUP_API/v1/webhook-endpoints" \
  -H "Authorization: Bearer $WHATISUP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://api.acme.dev/whatisup/webhook","events":["message.received","message.sent"],"channel_id":"8d653c66-e4ff-43ee-97da-3de5ad5680d4"}'

channel_id is optional and singular (a UUID, not an array). Omit it for an account-wide endpoint that receives every channel's events; set it to scope the endpoint to one channel.

The 201 response echoes signing_secret — but only the value you supplied in the request. If you omit signing_secret on create, the field comes back null and the endpoint is signed with the gateway's default secret instead. Either way you can read the secret that actually signs your deliveries at any time via GET /v1/webhook-endpoints/{id}/signing-secret, which returns { signing_secret, is_default }. Normal reads (GET, PATCH) expose only the boolean has_custom_signing_secret.

Schema · Response · WebhookEndpoint
FieldTypeRequiredNotes
idstring · uuidrequired
customer_idstring · uuidrequired
channel_idstring · uuidrequired · nullable
urlstring · URLrequired
eventsarray<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `qr.updated` \| `group.created` \| `group.updated` \| `group.participant_added` \| `group.participant_removed` \| `group.admin_promoted` \| `group.admin_demoted` \| `chat.updated` \| `chat.archived` \| `chat.pinned` \| `presence.updated` \| `order.placed` \| `order.cancelled` \| `cart.updated` \| `story.viewed` \| `call.offered` \| `call.terminated` \| `contact.resolved`>required
enabledbooleanrequired
has_custom_signing_secretbooleanrequired
chat_type_filter`all` \| `direct_only` \| `groups_only`required
created_atstring · ISO 8601required
updated_atstring · ISO 8601required

The url is validated for SSRF at create time and at delivery time (DNS-rebind defense). Loopback / link-local / RFC1918 / cloud-metadata IPs are rejected with a 400. In production (NODE_ENV=production), only https:// is accepted.

Update an endpoint

PATCH/v1/webhook-endpoints/:idBearer · API key
Schema · Request body
FieldTypeRequiredNotes
urlstring · URLoptional
eventsarray<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `qr.updated` \| `group.created` \| `group.updated` \| `group.participant_added` \| `group.participant_removed` \| `group.admin_promoted` \| `group.admin_demoted` \| `chat.updated` \| `chat.archived` \| `chat.pinned` \| `presence.updated` \| `order.placed` \| `order.cancelled` \| `cart.updated` \| `story.viewed` \| `call.offered` \| `call.terminated` \| `contact.resolved`>optional
signing_secretstring \| unknownoptional
enabledbooleanoptional
chat_type_filter`all` \| `direct_only` \| `groups_only`optional

There is no rotate_secret flag. To rotate, PATCH the endpoint with a new signing_secret string (16–255 chars, your own value):

{ "signing_secret": "whsec_a-new-32-plus-char-secret-here" }

Pass "signing_secret": null instead to drop your custom secret and fall back to the gateway default. Either change takes effect on the next delivery — the old secret stops working immediately, so roll your receiver first.

Test an endpoint

Two routes fire a signed synthetic delivery. They return the same superset, so one parser reads both:

  • POST /v1/webhook-endpoints/test — probe any URL, stored or not. Body: { "url": "..." }.
  • POST /v1/webhook-endpoints/:id/test — probe a stored endpoint with its real secret.
{
  "ok": true,
  "status": "success",
  "outcome": "success",
  "http_status": 200,
  "latency_ms": 372,
  "elapsed_ms": 372,
  "signed_with": "endpoint",
  "endpoint_id": "0f2c5ad1-8b47-4e93-a5d2-6c81e04fb7a9"
}

status means different things on the two routes, kept that way for backward compatibility: on /test it is the upstream HTTP status code, on /:id/test it is the success/failure discriminator. Read ok — a boolean meaning the same thing on both. outcome is the discriminator under a name that doesn't clash.

signed_with: "gateway_default" means we hold no stored secret for that URL, so a 401 back means "your receiver expects a different secret", not "your receiver is broken".

Delete an endpoint

DELETE/v1/webhook-endpoints/:idBearer · API key

Returns 204 No Content. In-flight deliveries that have already been queued will fail against the now-deleted endpoint and exhaust their retry budget normally.