Guias/Envio em lote
Documentação

Envio em lote

Fila persistente, múltiplas instâncias, controle assíncrono, recovery e progresso.

Recurso Pro: todas as rotas /message/batches... são classificadas comercialmente como CodeChat API Go Pro. O runtime atual não aplica entitlement de plano nem retorna 402; a autenticação executável é feita por JWT de usuário.

O Envio em lote persiste no PostgreSQL uma mensagem, os destinatários individuais, as instâncias permitidas e cada tentativa de envio. A requisição HTTP apenas cria ou altera o estado do lote; o MessageBatchWorker consome os itens de forma assíncrona, com concorrência 1 por lote. A fila no banco é a fonte da verdade.

Autenticação

Todas as rotas abaixo usam JWT de usuário:

Authorization: Bearer <jwt-do-usuario>

O token precisa ser assinado com HS256, conter userId em formato UUID e conter exp. A expiração sempre é validada, mesmo quando AUTHENTICATION_JWT_EXPIRES_IN=0. apikey, x-api-key e apiKey não autenticam esse fluxo.

Na criação, userId é persistido como owner_user_id e retornado como ownerUserId. Consultas, itens, tentativas e ações (start, pause, stop) só acessam lotes desse mesmo usuário. O mesmo userId isola eventos de WebSocket em /ws/global/events.

Arquitetura e persistência

  • message_batches: definição, estado, lease e contadores derivados.
  • message_batch_instances: IDs internos permitidos, snapshot do nome e contadores por instância.
  • message_batch_items: destinatários normalizados, ordem, estado e resultado.
  • message_batch_attempts: uma linha por tentativa, inclusive indisponibilidade da instância.
  • message_batch_outbox: eventos globais entregues após o commit.
  • Message.message_batch_id e Message.message_batch_item_id: vínculo opcional com a mensagem persistida já existente.

Contadores são recalculados dos itens na mesma transação que conclui uma tentativa. O progresso é floor(processed * 100 / total), em que processed = success + failed + skipped + unknown.

Criar um lote

POST /message/batches
Content-Type: application/json
Authorization: Bearer <jwt-do-usuario>
{
  "name": "Aviso de manutenção",
  "instances": ["codechat-01", "codechat-02"],
  "recipients": ["5531999999999", "5531888888888"],
  "message": {
    "type": "interactive",
    "payload": {
        "title": "Confirmação",
        "description": "Deseja continuar?",
        "footerText": "CodeChat",
        "buttons": [
            {
                "displayText": "Repo",
                "url": "https://github.com/code-chat-br/whatsapp-api-go"
            },
            {
                "displayText": "Código de barras",
                "copyCode": "32546413216543165489869589416558645341864321365468435469465"
            },
            {
                "displayText": "Sim",
                "id": "1"
            },
            {
                "displayText": "Não",
                "id": "0"
            },
            {
                "displayText": "Talvez",
                "id": "-1"
            }
        ]
    }
  },
  "options": {
    "replicable": true,
    "presence": "composing",
    "delay": {"minMs": 3000, "maxMs": 12000}
  },
  "autoStart": false,
  "externalAttributes": {"origin": "admin-panel"}
}

Resposta 201 Created:

{
  "id": "01900000-0000-7000-8000-000000000001",
  "ownerUserId": "11111111-1111-4111-8111-111111111111",
  "name": "Aviso de manutenção",
  "status": "DRAFT",
  "instances": [
    {"id": 1, "name": "codechat-01", "connectionStatus": "offline"},
    {"id": 2, "name": "codechat-02", "connectionStatus": "online"}
  ],
  "counts": {
    "received": 2,
    "total": 2,
    "processed": 0,
    "pending": 2,
    "sending": 0,
    "success": 0,
    "failed": 0,
    "skipped": 0,
    "unknown": 0
  },
  "progress": 0,
  "createdAt": "2026-07-13T12:00:00-03:00",
  "updatedAt": "2026-07-13T12:00:00-03:00"
}

Com autoStart: true, lote, itens, instâncias, evento de criação e transição para QUEUED são gravados na mesma transação.

Destinatários

Somente contatos individuais são aceitos. Os valores são normalizados pelo mesmo parser de endereço usado pelo projeto e a constraint (batch_id, normalized_recipient) impede duplicidade após normalização. A resposta informa counts.received, counts.total e counts.duplicatesIgnored.

Qualquer valor terminado em @g.us ou normalizado como grupo rejeita o lote inteiro com 422 e mantém os índices originais:

{
  "statusCode": 422,
  "error": "unprocessable-entity",
  "message": [
    "recipients[3] não pode ser um grupo",
    "recipients[7] não pode terminar com @g.us"
  ]
}

Mensagens compatíveis

Os tipos persistentes suportados são text, link, media, audio (incluindo os aliases ptt, whatsapp-audio e whatsapp_audio), contact, location e interactive. O payload usa o corpo interno dos endpoints individuais: por exemplo, {"text":"..."} para texto, o objeto mediaMessage para mídia, um array de contatos para contact e o objeto interativo mostrado acima para interactive.

Em interactive, o worker reutiliza o modo interativo buttons: cada botão é inferido por url, copyCode ou id, permitindo misturar os três tipos no mesmo payload, até o limite de 10 botões. title e footerText são opcionais; description e buttons são obrigatórios. mediaUploadId também é opcional: omitido, 0 ou string vazia enviam sem mídia. Como o pré-upload pertence a uma única instância, um ID positivo exige que o lote tenha exatamente uma instância; em lotes com múltiplas instâncias, mantenha o campo omitido, 0 ou vazio.

media reutiliza URL ou mediaUploadId do pré-upload existente; bytes não são armazenados no lote. Upload multipart por requisição (media-file e audio-file), reaction, form, payment-request, pix e review-order são rejeitados explicitamente porque não formam um payload persistente e repetível neste worker. mentionAll é rejeitado por ser exclusivo de grupos.

replicable é aceito e preservado no JSON de opções. O código anterior não possuía comportamento executável para essa propriedade; portanto, o Message Batch não inventa uma nova semântica nem altera o envio individual.

O payload da mensagem tem limite de 1 MiB; cada objeto de atributos externos tem limite de 64 KiB. A lista recebida respeita MESSAGE_BATCH_MAX_RECIPIENTS.

Delay e múltiplas instâncias

options.delay.minMs deve ser >= 0, maxMs >= minMs e maxMs <= MESSAGE_BATCH_MAX_DELAY_MS. Depois de cada item concluído, o worker escolhe um valor inclusivo e criptograficamente aleatório, persiste em selectedDelayMs e usa timer cancelável. Pause, stop ou encerramento do processo interrompem a espera sem time.Sleep irrecuperável.

Para cada item, o worker filtra as instâncias conectadas do lote, embaralha a lista e persiste a escolhida antes de enviar. Se ela cair antes do envio, a tentativa vira INSTANCE_UNAVAILABLE e outra instância conectada é tentada. Se nenhuma estiver disponível, o item volta para PENDING e o lote entra em WAITING_FOR_INSTANCE; a reavaliação ocorre no intervalo configurado e o lote volta automaticamente a PROCESSING.

Estados e transições

OrigemDestino permitido
DRAFTQUEUED
QUEUEDPROCESSING, WAITING_FOR_INSTANCE, INTERRUPTED
PROCESSINGPAUSE_REQUESTED, STOP_REQUESTED, WAITING_FOR_INSTANCE, COMPLETED, COMPLETED_WITH_ERRORS, INTERRUPTED, FAILED estrutural
PAUSE_REQUESTEDPAUSED, INTERRUPTED
PAUSEDQUEUED
WAITING_FOR_INSTANCEPROCESSING, PAUSE_REQUESTED, STOP_REQUESTED, INTERRUPTED
STOP_REQUESTEDSTOPPED, INTERRUPTED
INTERRUPTEDQUEUED

Transição inválida retorna 409 Conflict. start é idempotente em QUEUED e PROCESSING; pause é idempotente em PAUSE_REQUESTED e PAUSED; stop é idempotente em STOP_REQUESTED e STOPPED.

Pause não inicia um novo item, deixa o envio já confirmado terminar, cancela o delay e chega a PAUSED. Um novo start continua do próximo PENDING. Stop chega a STOPPED, é terminal e conserva os itens não processados como PENDING.

Endpoints e filtros

MétodoRotaOperação
POST/message/batchesCria o lote (201).
GET/message/batchesLista lotes.
GET/message/batches/:batchIdDetalhes sem incorporar milhares de itens.
GET/message/batches/:batchId/itemsLista itens.
GET/message/batches/:batchId/items/:itemIdItem e todas as suas tentativas.
GET/message/batches/:batchId/attemptsLista tentativas.
POST/message/batches/:batchId/startEnfileira início ou continuação e retorna imediatamente.
POST/message/batches/:batchId/pauseSolicita pausa.
POST/message/batches/:batchId/stopSolicita encerramento terminal.

Filtros de lotes: status, instanceName, createdFrom, createdTo, query, limit, nextCursor, previousCursor. Datas usam RFC 3339.

Filtros de itens: status, recipient, instanceName, limit, nextCursor, previousCursor. Estados: PENDING, SENDING, SUCCESS, FAILED, SKIPPED, INTERRUPTED, UNKNOWN.

Filtros de tentativas: status, recipient, instanceName, limit, nextCursor, previousCursor. Estados: SENDING, SUCCESS, FAILED, INSTANCE_UNAVAILABLE, INTERRUPTED, UNKNOWN.

O limite padrão é 50 e o máximo 200. Não envie nextCursor e previousCursor juntos. Os cursores são opacos para clientes; atualmente usam UUID para lotes/tentativas e posição numérica para itens. O envelope é:

{
  "nextCursor": "01900000-0000-7000-8000-000000000010",
  "previousCursor": "01900000-0000-7000-8000-000000000002",
  "totalRecords": 100,
  "records": []
}

Exemplos com curl

curl -X POST "http://localhost:8084/message/batches" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @batch.json

curl -X POST "http://localhost:8084/message/batches/$BATCH_ID/start" \
  -H "Authorization: Bearer $USER_TOKEN"

curl "http://localhost:8084/message/batches/$BATCH_ID" \
  -H "Authorization: Bearer $USER_TOKEN"

curl "http://localhost:8084/message/batches/$BATCH_ID/items?status=FAILED&limit=50" \
  -H "Authorization: Bearer $USER_TOKEN"

curl -X POST "http://localhost:8084/message/batches/$BATCH_ID/pause" \
  -H "Authorization: Bearer $USER_TOKEN"

Recovery, leases e resultado desconhecido

Cada lote ativo possui lease_owner e lease_expires_at. Claim, item SENDING e tentativa são gravados com transações e FOR UPDATE SKIP LOCKED, impedindo dois workers/réplicas de confirmarem o mesmo item. Heartbeats renovam os leases. No encerramento gracioso, timers e novos claims são cancelados e lotes pertencentes ao processo ficam INTERRUPTED.

Na inicialização e em cada ciclo, leases expirados de QUEUED, PROCESSING, PAUSE_REQUESTED, WAITING_FOR_INSTANCE ou STOP_REQUESTED são recuperados. Itens e tentativas que estavam em SENDING viram UNKNOWN; o lote vira INTERRUPTED e não reinicia automaticamente. O operador precisa chamar /start.

O worker gera e persiste clientMessageId antes de chamar o WhatsApp e o reutiliza nas tentativas de troca de instância. Isso reduz duplicidade, mas não oferece garantia de exactly once: uma mensagem pode ter sido aceita pelo WhatsApp e o processo cair antes do commit local. UNKNOWN significa exatamente que o resultado externo não pôde ser confirmado; o sistema não reenvia esse item cegamente.

Webhooks e outbox

Eventos de lote usam somente WEBHOOK_GLOBAL_URL. A mudança de item/estado e a linha de outbox são confirmadas juntas; a chamada HTTP ocorre fora da transação, com retry e backoff. message.batch.progress só é criado quando o percentual inteiro muda ou chega a 100. Consulte Webhooks para o mapa e os exemplos completos.

Variáveis de ambiente

VariávelPadrão
MESSAGE_BATCH_WORKER_ENABLEDtrue
MESSAGE_BATCH_WORKER_POLL_INTERVAL_MS1000
MESSAGE_BATCH_INSTANCE_RECHECK_INTERVAL_MS5000
MESSAGE_BATCH_MAX_DELAY_MS86400000
MESSAGE_BATCH_MAX_RECIPIENTS10000

Se o worker estiver desabilitado, criação, consulta e alterações de estado permanecem disponíveis, mas a fila não é consumida e a outbox não é entregue por este processo.