Endpoints de webhook
Diga ao WhatIsUp.dev para onde entregar as notificações de evento. Um cliente pode ter múltiplos endpoints (URLs diferentes para subconjuntos diferentes de eventos). Um canal pode ser a origem de eventos para múltiplos endpoints, e um endpoint pode assinar múltiplos canais — é um relacionamento muitos-para-muitos.
Modelo conceitual
Endpoints descrevem uma assinatura: para onde enviar, quais eventos enviar, e o secret de assinatura usado no HMAC. Os registros de entrega de fato estão em Entregas de webhook.
api_key → customer ──┬── channel ◄───┐
│ │
└── webhook_endpoint
│
└── webhook_delivery (per event)
Listar endpoints
Retorna todos os endpoints do cliente autenticado.
Criar um endpoint
| Field | Type | Required | Notes |
|---|---|---|---|
| url | string · URL | required | |
| events | array<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `message.edited` \| `message.deleted` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `channel.limits` \| `channel.health` \| `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_id | string · uuid | optional | |
| signing_secret | string | optional | |
| enabled | boolean | optional | |
| 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"}'Todo endpoint tem o seu próprio secret de assinatura. Passe signing_secret
pra escolher o valor, ou omita e o gateway gera um secret whsec_… exclusivo pra
aquele endpoint. A resposta 201 devolve o secret nos dois casos — é a única
resposta que traz ele. Depois disso você pode lê-lo de novo quando quiser via
GET /v1/webhook-endpoints/{id}/signing-secret, que retorna
{ signing_secret, is_default }; is_default é sempre false e existe só por
compatibilidade. Leituras normais (GET, PATCH) expõem apenas o booleano
has_custom_signing_secret.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | string · uuid | required | |
| customer_id | string · uuid | required | |
| channel_id | string · uuid | required · nullable | |
| url | string · URL | required | |
| events | array<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `message.edited` \| `message.deleted` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `channel.limits` \| `channel.health` \| `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 | |
| enabled | boolean | required | |
| has_custom_signing_secret | boolean | required | |
| chat_type_filter | `all` \| `direct_only` \| `groups_only` | required | |
| created_at | string · ISO 8601 | required | |
| updated_at | string · ISO 8601 | required |
A url é validada contra SSRF no momento da criação e no momento da entrega (defesa
contra DNS-rebind). IPs de loopback / link-local / RFC1918 / cloud-metadata são rejeitados com
um 400. Em produção (NODE_ENV=production), apenas https:// é aceito.
Atualizar um endpoint
| Field | Type | Required | Notes |
|---|---|---|---|
| url | string · URL | optional | |
| events | array<`message.received` \| `message.sent` \| `message.reaction` \| `message.status` \| `message.edited` \| `message.deleted` \| `channel.connected` \| `channel.disconnected` \| `channel.status` \| `channel.limits` \| `channel.health` \| `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_secret | string \| unknown | optional | |
| enabled | boolean | optional | |
| chat_type_filter | `all` \| `direct_only` \| `groups_only` | optional |
Não existe flag rotate_secret. Para rotacionar, faça PATCH no endpoint com
um novo signing_secret (string de 16–255 caracteres, valor seu):
{ "signing_secret": "whsec_um-novo-secret-de-32-ou-mais-chars" }Passe "signing_secret": null para o gateway gerar um secret novo pro endpoint
(leia com a rota signing-secret acima). Qualquer das duas mudanças vale a
partir da próxima entrega — o secret antigo para de funcionar na hora, então
atualize seu receptor primeiro.
Testar um endpoint
Duas rotas disparam uma entrega sintética assinada. As duas devolvem o mesmo superconjunto, então um parser só lê as duas:
POST /v1/webhook-endpoints/test— testa qualquer URL, guardada ou não. Corpo:{ "url": "..." }.POST /v1/webhook-endpoints/:id/test— testa um endpoint guardado com o secret real dele.
{
"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 significa coisas diferentes nas duas rotas, mantido assim por
compatibilidade: em /test é o código HTTP do upstream, em /:id/test é o
discriminador de sucesso/falha. Leia ok — booleano com o mesmo
significado nas duas. outcome é o discriminador com um nome que não colide.
signed_with: "gateway_default" significa que não temos secret guardado para
aquela URL, então o teste foi assinado com um secret aleatório de uso único: um
401 de volta é esperado e não quer dizer "seu receptor está quebrado". Salve o
endpoint e teste com /:id/test pra assinar com o secret real dele.
Excluir um endpoint
Retorna 204 No Content. Entregas em andamento que já foram enfileiradas vão falhar contra o endpoint agora excluído e esgotar o orçamento de retentativas normalmente.