Números (HTTP)
Endpoints /v1/channels — criar, listar, conectar por QR, reconectar e remover.
Um número é um número de WhatsApp que o broker opera por você. Os endpoints estão sob /v1/channels e todos exigem o header Authorization: Bearer. A equivalência SDK está no guia Números & conexão.
| método | path | o que faz |
|---|---|---|
POST | /v1/channels | cria um número OFICIAL; o não-oficial usa /connect |
POST | /v1/channels/connect | inicia o onboarding não-oficial — cria uma tentativa efêmera |
GET | /v1/channels/connect/:attemptId | poll da tentativa: QR + estado (o canal nasce ao parear) |
DELETE | /v1/channels/connect/:attemptId | cancela a tentativa (aborta a sessão + descarta) |
GET | /v1/channels | lista os números do tenant (só pareados; paginado) |
GET | /v1/channels/:id | lê um número |
PATCH | /v1/channels/:id | renomeia (só o label) |
DELETE | /v1/channels/:id | remove (soft-delete) |
POST | /v1/channels/:id/connect | reconecta um número EXISTENTE que caiu (QR novo) |
GET | /v1/channels/:id/connection | QR + estado de um número EXISTENTE (polling do reconnect) |
POST | /v1/channels/:id/disconnect | para de despachar, sem apagar |
POST | /v1/channels/:id/credentials | rotaciona credenciais (oficial) |
POST | /v1/channels/:id/register | registra o número na plataforma oficial |
GET | /v1/channels/:id/status | estado do número na plataforma oficial (ao vivo) |
GET | /v1/channels/:id/events | timeline de auditoria do número |
POST | /v1/channels/:id/profile-picture | define a foto de perfil |
GET | /v1/channels/:id/profile-picture | lê a foto de perfil (bytes) |
GET | /v1/channels/:id/overview | visão agregada: estado + limited/restricted/quarantine + entrega recente |
GET | /v1/channels/:id/journey | a jornada do número (marcos do ciclo de vida) |
POST | /v1/channels/:id/limited/release | libera um número limitado (aceite de risco) |
POST | /v1/channels/:id/quarantine/release | libera a quarentena de onboarding |
Para saber que um número está restrito, limitado ou em quarentena sem fazer polling manual de cada campo, use GET /v1/channels/:id/overview — a visão agregada resume estado, os bloqueios ativos e a entrega recente num só lugar.
Criar um número oficial
POST /v1/channels é só OFICIAL
POST /v1/channels. Um número não-oficial só passa a existir quando o provider confirma o pareamento: use POST /v1/channels/connect (cria uma tentativa efêmera) e faça polling — veja Conectar por QR (não-oficial). Enquanto não pareia, nada aparece na listagem.Resposta 201 com o número na view de um único canal (a mesma do GET /v1/channels/:id): id, kind, provider, state, label, number, engine, lastError, stateChangedAt, limited, restricted e — quando a feature está ligada no deployment — quarantine. Veja Campos do número para o significado de cada um.
Envie credentials com phoneNumberId, wabaId e accessToken. O broker verifica e assina o inbound na plataforma oficial antes de persistir; só um número já validado nasce connected. metaAppId é opcional (BYO); omitido, cai no app de plataforma padrão.
curl -X POST https://api.grwthy.com/v1/channels \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"provider": "meta",
"kind": "official",
"label": "Marketing",
"credentials": {
"phoneNumberId": "109xxxxxxxxxxxx",
"wabaId": "104xxxxxxxxxxxx",
"accessToken": "EAAG..."
}
}'Resposta 201
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": "5511",
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Credenciais são write-only
POST /v1/channels/:id/credentials (rotação) — que também re-verifica na plataforma oficial antes de gravar. A resposta é o número atualizado, nunca as credenciais.Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Conectar por QR (não-oficial)
Um número não-oficial só nasce ao parear: o broker separa uma tentativa de conexão efêmera do canal em si. Enquanto a tentativa vive, ela não aparece em GET /v1/channels — não há "Número via QR" órfão. Quando o provider confirma o pareamento, o broker promove a tentativa a um canal real e devolve o channelId. O fluxo tem três passos:
- 1. Iniciar —
POST /v1/channels/connectcria a tentativa e responde201comattemptId(prefixocxn_). - 2. Poll —
GET /v1/channels/connect/:attemptIddevolve ostatee oqra exibir; repita enquanto o estado não for terminal. - 3. Pareou — ao
connected, o poll traz ochannelIde onumberreal. Daí em diante, opere pelo canal.
1. Iniciar a tentativa
Agnóstico ao provider: você não envia baseUrl/apiKey — o broker resolve uma instância do pool. Criar uma tentativa exige um aceite de risco vigente (Compliance v1): passe riskAcceptance.termsVersion inline. Sem aceite: 422 risk_acceptance_required. Campos aceitos: label (opcional), declaredNumber (opcional, não reserva número — o número real é o que aparecer ao parear), credentials (opcional; baseUrl + apiKey? para BYO), preferredEngine (opcional) e riskAcceptance.
curl -X POST https://api.grwthy.com/v1/channels/connect \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Atendimento",
"riskAcceptance": { "termsVersion": "unofficial-v1" }
}'{ "attemptId": "cxn_9f2c...", "state": "connecting", "qr": null }Caminho normal (agnóstico): não envie credentials — o broker resolve uma instância do pool. Pool sem instância elegível → 503 no_waha_instance_available, nada persistido. Caminho avançado (BYO): traga a sua instância em credentials (baseUrl obrigatório, apiKey opcional). O BYO só é honrado se o baseUrl estiver na allowlist SSRF do deployment (WAHA_ALLOWED_BASE_URLS); fora dela, falha na guarda SSRF antes de persistir. No BYO, preferredEngine é ignorado (a engine é a da sua instância).
2. Poll da tentativa
Chame GET /v1/channels/connect/:attemptId em polling. A resposta é discriminada por state; siga enquanto o estado não for terminal.
curl https://api.grwthy.com/v1/channels/connect/cxn_9f2c... \
-H "Authorization: Bearer $WABROKER_API_KEY"| state | shape da resposta | significado |
|---|---|---|
connecting | { attemptId, state } | autenticando/carregando; ainda sem QR pendente |
scan_qr | { attemptId, state, qr } | há um QR a exibir/renovar (payload cru, veja abaixo) |
connected | { state, channelId, number } | PAREOU e o canal foi promovido — opere por channelId (ch_…) |
failed | { state, lastError? } | o connector recusou (número já em uso, provider rejeitou); lastError traz o motivo |
expired | { state } | o TTL da tentativa venceu sem parear (CONNECTION_ATTEMPT_TTL_MINUTES, default 10) |
Exemplos das duas transições que mais importam:
// enquanto aguarda o scan
{ "attemptId": "cxn_9f2c...", "state": "scan_qr", "qr": "2@AbCd..." }
// pareou — o canal real já existe
{ "state": "connected", "channelId": "ch_71a0...", "number": "5511999999999" }O qr é o payload cru, não uma imagem
qr é o conteúdo cru do QR (a string a ser rasterizada pelo seu cliente numa imagem) — não é PNG em base64 nem data: URI. Use uma lib de QR do seu lado para desenhá-lo. O broker renova o código a cada ~20s, então o próprio polling basta.3. Cancelar (opcional)
DELETE /v1/channels/connect/:attemptId aborta uma tentativa em andamento: para e apaga a sessão (best-effort) e descarta a tentativa. Responde { "cancelled": true }. Uma tentativa já promovida virou um canal com sessão viva — cancelá-la aqui a derrubaria, então recusa com 409 attempt_not_cancellable (o caminho é DELETE /v1/channels/:id).
- Tentativa de outro tenant (ou inexistente):
404 channel_not_found, nunca403— o poll e o cancel são escopados pela API key.
Reconectar um número existente
Isto é diferente do onboarding acima. Quando um número já pareado cai, POST /v1/channels/:id/connect reabre a sessão (QR novo) sem criar outro número — idempotente para um número já connected (não derruba a conexão viva). Depois, faça polling de GET /v1/channels/:id/connection, que devolve state, o qr e phoneNumber quando reconectado.
curl https://api.grwthy.com/v1/channels/ch_9f2c.../connection \
-H "Authorization: Bearer $WABROKER_API_KEY"Resposta 200
{
"id": "ch_9f2c8a1b0d3e4f50",
"qr": "<redacted>",
"state": "scan_qr"
}Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "unofficial",
"label": "Número via QR",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "waha",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Listar e ler
A listagem é paginada por cursor: use ?limit= e siga nextCursor em ?cursor=. O tenantId vem sempre da API key — um ?tenantId= na query é ignorado. Use ?tag=<id> para filtrar só os números com aquela tag aplicada.
curl "https://api.grwthy.com/v1/channels?limit=50" \
-H "Authorization: Bearer $WABROKER_API_KEY"Resposta 200
{
"items": [
{
"engine": null,
"health": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"phoneNumberId": "5511900000001",
"provider": "meta",
"rateLimitPerMinute": 60,
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z",
"tags": []
},
{
"engine": null,
"health": {
"funnel": {
"delivered": 0,
"failed": 0,
"read": 0,
"sent": 0
},
"reasons": [
{
"code": "immature",
"detail": "sem conexão",
"label": "número novo",
"severity": "warn"
}
],
"score": 85,
"tier": "at_risk"
},
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "unofficial",
"label": "Número via QR",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"phoneNumberId": null,
"provider": "waha",
"rateLimitPerMinute": 60,
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z",
"tags": []
}
],
"nextCursor": null
}Ler um número (GET /v1/channels/:id) devolve quase a mesma view de um item da listagem, com uma diferença importante: dois campos são exclusivos da listagem e não aparecem no GET de um único número — phoneNumberId e rateLimitPerMinute. Precisa deles? Leia pela listagem (GET /v1/channels). Todos os demais campos são idênticos nas duas rotas.
Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z",
"tags": []
}Campos do número
Além de id, kind, provider, state, label e number, cada número traz campos de motor, limites e de contenção. Os dois marcados só na listagem aparecem apenas em GET /v1/channels (nunca no GET /v1/channels/:id nem na criação).
| campo | forma | significado |
|---|---|---|
engine | webjs | noweb | gows | null | motor da sessão não-oficial, aditivo a provider; null no oficial, sem sessão, ou sessão sem instância |
restricted | { active, until } | SEMPRE presente. Restrição do WhatsApp (reachoutTimelock/463): bloqueia INICIAR conversa com contato NOVO; existentes seguem. Auto-expira em until (ISO); until histórico é preservado com active:false |
limited | { active, since } | SEMPRE presente. Disjuntor do broker: uma rajada de falhas de entrega barra TODO envio até liberação MANUAL. since (ISO) = quando acendeu (active = since !== null). NÃO auto-expira |
quarantine | { active, until } | presente SÓ com a feature ligada no deployment. Quarentena de onboarding: janela após a 1ª conexão em que o número novo é contido. until = fim da janela |
stateChangedAt | ISO | null | instante da última transição de estado de ciclo de vida; null só em linhas antigas |
rateLimitPerMinute | number (default 60) | teto de envios/min do número (token bucket) — SÓ NA LISTAGEM |
phoneNumberId | string | null | id do número na plataforma oficial (asset, não segredo); só no oficial, null no não-oficial/sandbox — SÓ NA LISTAGEM |
Um número restrito continua connected
state: 'connected'. Só estes campos revelam o bloqueio. Descubra-o por polling de GET /v1/channels (campos restricted/limited/quarantine) ou por GET /v1/channels/:id/overview.Um número limitado não sai do disjuntor sozinho: libere-o com POST /v1/channels/:id/limited/release (aceite de risco). A quarentena auto-libera no fim da janela, ou antecipe com POST /v1/channels/:id/quarantine/release. A restrição do WhatsApp auto-expira no prazo until — não há release.
Renomear, desconectar e remover
# Renomear (só o label; 1..80 chars)
curl -X PATCH https://api.grwthy.com/v1/channels/ch_9f2c... \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "Atendimento SP" }'
# Desconectar — para de despachar (envio passa a dar 503 channel_disconnected)
curl -X POST https://api.grwthy.com/v1/channels/ch_9f2c.../disconnect \
-H "Authorization: Bearer $WABROKER_API_KEY"
# Remover — soft-delete; some da listagem e do GET (viram 404)
curl -X DELETE https://api.grwthy.com/v1/channels/ch_9f2c... \
-H "Authorization: Bearer $WABROKER_API_KEY"Cada uma devolve o número com o state resultante — connected com o novo label (PATCH), disconnected ou deleted.
Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "unofficial",
"label": "Vendas",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "waha",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "disconnected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "official",
"label": "Suporte",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "meta",
"restricted": {
"active": false,
"until": null
},
"state": "deleted",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Só para números oficiais
POST /v1/channels/:id/register executa a cerimônia do PIN da verificação em duas etapas — corpo { "pin": "000000" }. O PIN nunca é persistido nem ecoado; a resposta é { "registered": true }. GET /v1/channels/:id/status lê ao vivo, na plataforma oficial, o estado do número (nome verificado, qualidade, aprovação do nome). Um número não-oficial responde 422 unsupported_capability aos dois.
curl -X POST https://api.grwthy.com/v1/channels/ch_9f2c.../register \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "pin": "000000" }'Resposta 200
{
"registered": true
}GET /v1/channels/:id/status lê ao vivo, na plataforma oficial, o estado do número.
Resposta 200
{
"codeVerificationStatus": "VERIFIED",
"nameStatus": "APPROVED",
"platformType": "CLOUD_API",
"qualityRating": "GREEN",
"throughput": {
"level": "STANDARD"
},
"verifiedName": "Acme Suporte"
}Foto de perfil
POST /v1/channels/:id/profile-picture define a foto do número e devolve o número atualizado. O GET correspondente devolve os bytes da imagem (não JSON).
Resposta 200
{
"engine": null,
"id": "ch_9f2c8a1b0d3e4f50",
"kind": "unofficial",
"label": "Número via QR",
"lastError": null,
"limited": {
"active": false,
"since": null
},
"number": null,
"provider": "waha",
"restricted": {
"active": false,
"until": null
},
"state": "connected",
"stateChangedAt": "2026-08-04T12:00:00.000Z"
}Auditoria
GET /v1/channels/:id/events devolve o timeline (cada transição de estado), recentes primeiro, paginado por ?before= (cursor) e ?limit=. As linhas de reatribuição de tenant são redigidas — o id da contraparte nunca vaza.
- Número de outro tenant (ou inexistente):
404 channel_not_found, nunca403. - Endereços de número são sempre E.164 sem
+.
Resposta 200
{
"items": [
{
"activeNumber": null,
"createdAt": "2026-08-04T12:00:00.000Z",
"fromState": null,
"id": "evt_0a7d3f1c9b5e2648",
"kind": "state",
"reason": "session_dropped",
"source": "supervisor",
"toState": "down",
"traceId": "tr_2f8b6d0a4c1e3597"
},
{
"activeNumber": null,
"createdAt": "2026-08-04T12:00:00.000Z",
"fromState": null,
"id": "evt_0a7d3f1c9b5e2648",
"kind": "state",
"reason": null,
"source": "supervisor",
"toState": "connected",
"traceId": "tr_2f8b6d0a4c1e3597"
},
{
"activeNumber": null,
"createdAt": "2026-08-04T12:00:00.000Z",
"fromState": null,
"id": "evt_0a7d3f1c9b5e2648",
"kind": "state",
"reason": null,
"source": "supervisor",
"toState": "connecting",
"traceId": "tr_2f8b6d0a4c1e3597"
}
],
"nextCursor": null
}Tags
Uma tag é um rótulo livre, por tenant, aplicado a números — só para organizar e filtrar; não muda comportamento nem capacidade do número. O catálogo é único por tenant (/v1/tags); o vínculo com um número específico vive sob /v1/channels/:id/tags. Equivalente no SDK: client.tags.list()/create()/delete() e client.channels.addTag()/removeTag() (C#: client.Tags.ListAsync()/CreateAsync()/DeleteAsync() e client.Channels.AddTagAsync()/RemoveTagAsync()).
| método | path | o que faz |
|---|---|---|
GET | /v1/tags | lista o catálogo do tenant |
POST | /v1/tags | cria uma tag (ou reusa, se o nome já existe — case-insensitive) |
DELETE | /v1/tags/:id | apaga a tag do catálogo (remove de todos os números) |
POST | /v1/channels/:id/tags | aplica uma tag ao número (cria e vincula numa chamada só) |
DELETE | /v1/channels/:id/tags/:tagId | desvincula a tag do número (a tag permanece no catálogo) |
Aplicar uma tag a um número (POST /v1/channels/:id/tags) é a operação mais comum: cria a tag se o nome ainda não existir no tenant, ou reusa a existente (comparação case-insensitive — o catálogo nunca tem dois "VIP"), e já vincula ao número, tudo numa chamada.
curl -X POST https://api.grwthy.com/v1/channels/ch_9f2c.../tags \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "VIP", "color": "#22c55e" }'Resposta 201
{
"color": "#22c55e",
"id": "tag_6e2a8c4f0b1d3597",
"name": "VIP"
}color é opcional e de forma livre (ex.: um hex) — o broker não valida nem limita a paleta.
POST /v1/tags cria (ou reusa) uma tag no catálogo sem vinculá-la a nenhum número ainda — útil para preparar o catálogo antes de aplicar. GET /v1/tags lista o catálogo inteiro do tenant, ordenado por nome.
Resposta 201
{
"color": "#22c55e",
"id": "tag_6e2a8c4f0b1d3597",
"name": "VIP"
}Resposta 200
[
{
"color": null,
"id": "tag_6e2a8c4f0b1d3597",
"name": "Cobrança"
},
{
"color": "#22c55e",
"id": "tag_6e2a8c4f0b1d3597",
"name": "VIP"
}
]DELETE /v1/tags/:idresponde204e apaga a tag do catálogo — ela some de todos os números que a usavam, não só de um.DELETE /v1/channels/:id/tags/:tagIdresponde204e só desvincula do número; a tag em si segue no catálogo para uso em outros números.- Tag ou número de outro tenant (ou inexistente):
404(resource_not_foundouchannel_not_found), nunca403.
Filtrar números por tag
GET /v1/channels?tag=<id> devolve só os números com aquela tag aplicada, com o mesmo escopo de tenant e a mesma paginação da listagem normal. Toda listagem e todo detalhe de número trazem tags: [{ id, name, color }] — [] quando o número não tem nenhuma.