Pools de disparo (HTTP)
Endpoints /v1/pools — agrupar números e enviar pelo número LRU saudável do grupo.
Um pool é um agrupamento de números do tenant. Em vez de escolher um número para cada envio, você envia para o pool e o broker escolhe, a cada mensagem, o número LRU (o que está há mais tempo sem disparar) entre os que estão saudáveis — espalhando o volume no tempo em vez de martelar sempre o mesmo número. Os endpoints estão sob /v1/pools e exigem o mesmo header Authorization: Bearer das demais rotas.
| método | path | o que faz |
|---|---|---|
GET | /v1/pools | lista os pools do tenant |
POST | /v1/pools | cria um pool |
PATCH | /v1/pools/:id | renomeia o pool |
DELETE | /v1/pools/:id | apaga o pool |
GET | /v1/pools/:id/channels | lista os números vinculados, com estado e último disparo |
POST | /v1/pools/:id/channels | vincula um número ao pool |
DELETE | /v1/pools/:id/channels/:channelId | desvincula um número do pool |
POST | /v1/pools/:ref/messages | envia uma mensagem pelo número LRU saudável do pool |
POST | /v1/pools/:ref/messages/batch | envia uma lista de envios — cada um pode sair por um número diferente |
Em todas as rotas de envio, :ref aceita o id do pool (pl_…) ou o slug — o broker resolve os dois, sempre dentro do tenant da API key. O slug é gerado a partir do nome na criação e fica fixo mesmo que o pool seja renomeado depois; o nome cru não resolve.
Criar e gerenciar o catálogo
O corpo carrega só name (1 a 40 caracteres). Nomes são únicos por tenant, case-insensitive: criar (ou renomear para) um nome já usado por outro pool do mesmo tenant responde 409 pool_name_taken.
curl -X POST https://api.grwthy.com/v1/pools \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Marketing" }'Resposta 201
{
"channelCount": 0,
"id": "pl_4c8f2a6b0d1e3579",
"name": "Marketing",
"slug": "marketing"
}GET /v1/pools lista o catálogo, ordenado por nome, com channelCount — a contagem de números vinculados a cada pool.
Resposta 200
[
{
"channelCount": 1,
"id": "pl_4c8f2a6b0d1e3579",
"name": "Marketing",
"slug": "marketing"
}
]PATCH /v1/pools/:idaceita sónamee devolve{ id, name }— não recalculachannelCount; liste de novo se precisar dele atualizado.DELETE /v1/pools/:idresponde204. Apaga o pool e os vínculos com os números — os números em si não são removidos nem desconectados.- Pool de outro tenant (ou inexistente):
404 resource_not_found, nunca403.
Vincular números ao pool
POST /v1/pools/:id/channels vincula um número existente pelo channelId (idempotente — vincular de novo não duplica nem falha). Um número pode pertencer a mais de um pool.
curl -X POST https://api.grwthy.com/v1/pools/pl_9f2c.../channels \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "channelId": "ch_9f2c8a1b0d3e4f50" }'GET /v1/pools/:id/channels lista os números do pool, ordenados pelo mesmo critério LRU do envio (menos usado recentemente primeiro) — a mesma ordem em que o broker os escolheria para o próximo disparo.
Resposta 200
[
{
"id": "ch_9f2c8a1b0d3e4f50",
"label": "+55 11 90000-0001",
"lastDispatchAt": null,
"number": null,
"state": "connected"
}
]lastDispatchAténullenquanto o número nunca disparou pelo pool — esses vêm primeiro (nunca-usado antes de já-usado).DELETE /v1/pools/:id/channels/:channelIddesvincula (204) sem afetar o número.- Pool ou número de outro tenant (ou inexistente):
404(resource_not_foundouchannel_not_found), nunca403.
Enviar pelo pool (LRU)
POST /v1/pools/:ref/messages tem o mesmo corpo do envio por número (to, content, idempotencyKey obrigatória, opcionalmente replyToProviderId/ttlSeconds) — veja Mensagens (HTTP) para os tipos de content. A única diferença é quem escolhe o número: aqui é o broker, não você. No exemplo abaixo, marketing é o slug gerado para o pool "Marketing" criado acima — não o nome.
curl -X POST https://api.grwthy.com/v1/pools/marketing/messages \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511988887777",
"content": { "kind": "text", "text": "Promoção só hoje!" },
"idempotencyKey": "promo-4821-envio"
}'Resposta 202
{
"idempotencyKey": "promo-4821-envio",
"idempotent": false,
"messageId": "msg_7b3e1d9a2c4f6081",
"state": "queued",
"traceId": "tr_2f8b6d0a4c1e3597"
}O content aceita os mesmos kind do envio por número — inclusive interactive e reaction:
curl -X POST https://api.grwthy.com/v1/pools/marketing/messages \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511988887777",
"content": { "kind": "interactive", "interactive": {
"type": "list", "body": "Escolha um horário disponível", "buttonLabel": "Ver horários",
"sections": [
{ "title": "Manhã", "rows": [
{ "id": "h9", "title": "09:00" },
{ "id": "h10", "title": "10:00", "description": "Último horário da manhã" }
] }
]
} },
"idempotencyKey": "promo-4821-lista"
}'
curl -X POST https://api.grwthy.com/v1/pools/marketing/messages \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511988887777",
"content": { "kind": "reaction", "targetMessageId": "msg_9f2c8a1b0d3e4f50", "emoji": "👍" },
"idempotencyKey": "promo-4821-reacao"
}'A reação segue o mesmo contrato agnóstico de /v1/channels/:ref/messages — veja Mensagens (HTTP) para o detalhe (targetMessageId, alias deprecated, e invalid_recipient quando o alvo não resolve).
A escolha é atômica com a admissão: o broker olha o número com last_dispatch_at mais antigo (nunca-usado primeiro) entre os saudáveis — connected, sem estar limitado nem restrito (quarentena é aplicada em seguida, na admissão em si) — e carimba o disparo antes de devolver a resposta. Dois envios concorrentes ao mesmo pool nunca colidem no mesmo número. A partir daí, o envio herda os mesmos gates de fila, cota e compliance do envio por número — só a escolha do número muda.
Pool sem número disponível
409 no_available_channel — nada é admitido, e é seguro tentar de novo mais tarde ou por outro pool/número.Enviar em lote pelo pool
POST /v1/pools/:ref/messages/batch aceita o mesmo corpo do lote por número — { "messages": [...] }, cada item idêntico ao envio unitário (to, content, idempotencyKey obrigatória), máximo de 2000 itens num corpo de até 512KB. A resposta, porém, segue o contrato antigo e síncrono, diferente do lote por número — veja Mensagens (HTTP) para o contrato assíncrono do lote por número (202 accepted + webhook). Aqui cada item já volta resolvido na própria posição: entrou (messageId) ou falhou na admissão (error), sem passar por um estado intermediário — o lote continua não atômico nesse sentido. A diferença de roteamento: cada item escolhe seu próprio número LRU, de forma independente. Um broadcast de 200 mensagens não sai todo pelo mesmo número: ele se espalha pelos números saudáveis do pool, reduzindo o risco de um único número levar toda a rajada (bom para evitar restrição/banimento).
curl -X POST https://api.grwthy.com/v1/pools/marketing/messages/batch \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "5511900000001", "idempotencyKey": "promo-a",
"content": { "kind": "text", "text": "Promoção só hoje!" } },
{ "to": "5511900000002", "idempotencyKey": "promo-b",
"content": { "kind": "text", "text": "Promoção só hoje!" } }
]
}'Cada item traz seu messageId na posição em que entrou, ou um error se falhou na admissão — inclusive { "code": "no_available_channel" } se o pool ficou sem número saudável no meio do lote; os itens seguintes ainda tentam normalmente.
Template com params no content, e lote heterogêneo
Igual ao lote por número: quando um item tem content.kind === "template", os parâmetros de renderização (bodyParams, headerText, headerMediaUrl, buttonParams) vão dentro do content daquele item — não há mais personalização "por fora" nem herança entre itens. E como cada item carrega o seu próprio content completo, nada impede misturar kind diferentes no mesmo lote de pool, cada um saindo pelo número que o LRU escolher para ele:
curl -X POST https://api.grwthy.com/v1/pools/marketing/messages/batch \
-H "Authorization: Bearer $WABROKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "to": "5511900000001", "idempotencyKey": "pedido-a",
"content": { "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["Ana", "1234"] } },
{ "to": "5511900000002", "idempotencyKey": "pedido-b",
"content": { "kind": "template", "name": "confirmacao_pedido", "language": "pt_BR",
"bodyParams": ["Bruno", "5678"] } },
{ "to": "5511900000003", "idempotencyKey": "pedido-c",
"content": { "kind": "media", "media": "image", "url": "https://cdn.exemplo.com/nf.png" } }
]
}'Corpo estrito: campo desconhecido é rejeitado
messages) é validado estrito: uma propriedade fora do contrato responde 422 em vez de ser ignorada em silêncio.Validação de forma é atômica: 1 item inválido reprova o lote inteiro
content) é checada antes de admitir qualquer um, mesmo aqui onde a escolha do número é por LRU e por item. O primeiro item com forma inválida reprova a requisição inteira com 422 e nada é enviado. Só a admissão em runtime (fila, cota, canal disponível) continua não-atômica — essa sim, por-posição, como no restante desta seção.Equivalência no SDK
client.pools.list()/create()/rename()/delete()/channels()/addChannel()/removeChannel() cobrem o catálogo e o vínculo (C#: client.Pools.ListAsync()/CreateAsync()/RenameAsync()/DeleteAsync()/ChannelsAsync()/AddChannelAsync()/RemoveChannelAsync()); o envio é client.messages.sendToPool(poolRef, input) e client.messages.sendBatchToPool(poolRef, input) — mesma assinatura de send/sendBatch, trocando o número pelo poolRef do pool (C#: client.Messages.SendToPoolAsync(poolRef, input) e client.Messages.SendBatchToPoolAsync(poolRef, input)).