Seções

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étodopatho que faz
POST/v1/channelscria um número OFICIAL; o não-oficial usa /connect
POST/v1/channels/connectinicia o onboarding não-oficial — cria uma tentativa efêmera
GET/v1/channels/connect/:attemptIdpoll da tentativa: QR + estado (o canal nasce ao parear)
DELETE/v1/channels/connect/:attemptIdcancela a tentativa (aborta a sessão + descarta)
GET/v1/channelslista os números do tenant (só pareados; paginado)
GET/v1/channels/:idlê um número
PATCH/v1/channels/:idrenomeia (só o label)
DELETE/v1/channels/:idremove (soft-delete)
POST/v1/channels/:id/connectreconecta um número EXISTENTE que caiu (QR novo)
GET/v1/channels/:id/connectionQR + estado de um número EXISTENTE (polling do reconnect)
POST/v1/channels/:id/disconnectpara de despachar, sem apagar
POST/v1/channels/:id/credentialsrotaciona credenciais (oficial)
POST/v1/channels/:id/registerregistra o número na plataforma oficial
GET/v1/channels/:id/statusestado do número na plataforma oficial (ao vivo)
GET/v1/channels/:id/eventstimeline de auditoria do número
POST/v1/channels/:id/profile-picturedefine a foto de perfil
GET/v1/channels/:id/profile-picturelê a foto de perfil (bytes)
GET/v1/channels/:id/overviewvisão agregada: estado + limited/restricted/quarantine + entrega recente
GET/v1/channels/:id/journeya jornada do número (marcos do ciclo de vida)
POST/v1/channels/:id/limited/releaselibera um número limitado (aceite de risco)
POST/v1/channels/:id/quarantine/releaselibera 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

O não-oficial (por QR) não é mais criado por 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.

bash
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

json
{
  "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

O broker cifra as credenciais e nunca as devolve. Para trocá-las, use 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

json
{
  "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/connect cria a tentativa e responde 201 com attemptId (prefixo cxn_).
  • 2. Poll — GET /v1/channels/connect/:attemptId devolve o state e o qr a exibir; repita enquanto o estado não for terminal.
  • 3. Pareou — ao connected, o poll traz o channelId e o number real. 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.

bash
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" }
  }'
json
{ "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.

bash
curl https://api.grwthy.com/v1/channels/connect/cxn_9f2c... \
  -H "Authorization: Bearer $WABROKER_API_KEY"
stateshape da respostasignificado
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:

json
// 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

O 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, nunca 403 — 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.

bash
curl https://api.grwthy.com/v1/channels/ch_9f2c.../connection \
  -H "Authorization: Bearer $WABROKER_API_KEY"

Resposta 200

json
{
  "id": "ch_9f2c8a1b0d3e4f50",
  "qr": "<redacted>",
  "state": "scan_qr"
}

Resposta 200

json
{
  "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.

bash
curl "https://api.grwthy.com/v1/channels?limit=50" \
  -H "Authorization: Bearer $WABROKER_API_KEY"

Resposta 200

json
{
  "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

json
{
  "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).

campoformasignificado
enginewebjs | noweb | gows | nullmotor 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
stateChangedAtISO | nullinstante da última transição de estado de ciclo de vida; null só em linhas antigas
rateLimitPerMinutenumber (default 60)teto de envios/min do número (token bucket) — SÓ NA LISTAGEM
phoneNumberIdstring | nullid 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

Restrição, limitação e quarentena não disparam webhook — e um número restrito continua com 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

bash
# 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

json
{
  "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

json
{
  "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

json
{
  "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.

bash
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

json
{
  "registered": true
}

GET /v1/channels/:id/status lê ao vivo, na plataforma oficial, o estado do número.

Resposta 200

json
{
  "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

json
{
  "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, nunca 403.
  • Endereços de número são sempre E.164 sem +.

Resposta 200

json
{
  "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étodopatho que faz
GET/v1/tagslista o catálogo do tenant
POST/v1/tagscria uma tag (ou reusa, se o nome já existe — case-insensitive)
DELETE/v1/tags/:idapaga a tag do catálogo (remove de todos os números)
POST/v1/channels/:id/tagsaplica uma tag ao número (cria e vincula numa chamada só)
DELETE/v1/channels/:id/tags/:tagIddesvincula 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.

bash
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

json
{
  "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

json
{
  "color": "#22c55e",
  "id": "tag_6e2a8c4f0b1d3597",
  "name": "VIP"
}

Resposta 200

json
[
  {
    "color": null,
    "id": "tag_6e2a8c4f0b1d3597",
    "name": "Cobrança"
  },
  {
    "color": "#22c55e",
    "id": "tag_6e2a8c4f0b1d3597",
    "name": "VIP"
  }
]
  • DELETE /v1/tags/:id responde 204 e 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/:tagId responde 204 e 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_found ou channel_not_found), nunca 403.

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.