Documentação da API
A API da Voza usa HTTPS, JSON e os verbos HTTP de sempre. Os corpos de mensagens, chamadas e webhooks seguem o formato da API oficial do WhatsApp (Meta), então o mesmo código atende os dois tipos de número da Voza.
Endereço base:
https://www.vozaapi.com/v1
| Número por QR code | Número oficial (Meta) | |
|---|---|---|
| Como conecta | Lendo um QR code com o celular, como no WhatsApp Web | Com as credenciais da sua app da Meta (WhatsApp Cloud API) |
| Conta do WhatsApp | WhatsApp ou WhatsApp Business comum; o celular continua funcionando | WhatsApp Business Platform, na sua conta da Meta |
| Mensagens | Texto e mídias | Texto, mídias, modelos, interativas, figurinhas, localização, contatos e reações |
| Janela de 24 horas | Não há | Fora dela, só modelos aprovados pela Meta |
| Chamadas | Sim, várias ao mesmo tempo no mesmo número | Sim, quando a Meta libera as chamadas para o número |
| Cobrança das conversas | Nenhuma além do plano | A Meta cobra na sua conta da Meta |
| Risco | O WhatsApp pode bloquear números que enviam spam | Segue as políticas e a qualidade da Meta |
Início rápido
- Crie uma conta e, em Painel › Chaves de API, uma chave.
- Crie um número:
curl https://www.vozaapi.com/v1/numbers \ -H "Authorization: Bearer vz_SUA_CHAVE" \ -H "Content-Type: application/json" \ -d '{"name": "Loja Centro", "webhook_url": "https://seu-sistema.com/webhooks/whatsapp"}'
- Abra o
qr_urlda resposta e leia o QR code com o WhatsApp do celular (Aparelhos conectados › Conectar aparelho). Ou ligue um número oficial (veja Números oficiais). - Envie a primeira mensagem:
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \ -H "Authorization: Bearer vz_SUA_CHAVE" \ -H "Content-Type: application/json" \ -d '{"to": "5511987654321", "type": "text", "text": {"body": "Olá! 👋"}}'
- As respostas do contato chegam ao seu webhook, no formato da Meta. Pronto: o resto desta página é detalhe.
Autenticação
Crie uma chave em Painel › Chaves de API e envie-a em todas as requisições, no cabeçalho Authorization:
Authorization: Bearer vz_SUA_CHAVE
- Use a chave só no seu servidor. Nunca a coloque no navegador nem no app do seu cliente.
- A chave aparece uma única vez, quando é criada. Se perder, revogue-a e crie outra.
- Uma chave revogada deixa de funcionar na hora.
- O telefone web usa outro token, curto e de um só número (
wp_…): veja Telefone web.
Erros
Todos os erros têm o mesmo corpo. O código é estável e serve para o seu sistema decidir o que fazer; a mensagem é para pessoas e pode mudar.
A mensagem vem no idioma pedido no cabeçalho Accept-Language ou no parâmetro ?lang=: pt-BR, en ou es (outro idioma recebe inglês). Sem pedido, vem no idioma de quem criou a conta.
{
"error": {
"code": "invalid_request",
"message": "O campo to é obrigatório.",
"details": { "to": ["O campo to é obrigatório."] }
}
}
Quando a falha vem da Meta, num número oficial, o erro traz também meta: o erro da Meta como ela o deu, com code (131047 fora da janela de 24 horas, 190 token inválido, 1380xx nas chamadas…), subcode, type, details e fbtrace_id. Quando vem do motor dos números por QR code, meta.reason diz por quê: not_on_whatsapp, lines_busy, lines_starting, restarting, not_linked, not_connected, timeout…
{
"error": {
"code": "whatsapp_refused",
"message": "A Meta não aceitou o pedido: Message failed to send because more than 24 hours have passed.",
"meta": {
"code": 131047,
"subcode": null,
"type": "OAuthException",
"details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
"fbtrace_id": "AbCdEf123"
}
}
}
| Status | Código | Quando acontece |
|---|---|---|
| 401 | invalid_api_key | A chave está ausente, é inválida ou foi revogada. |
| 401 | invalid_webphone_token | O token do telefone web está ausente, é inválido ou venceu. |
| 403 | trial_expired | O teste grátis terminou: assine um plano no painel. |
| 403 | subscription_inactive | A assinatura terminou (cancelada ou sem pagamento): assine de novo no painel. |
| 403 | account_suspended | A conta está suspensa. |
| 403 | number_limit_reached | A conta já tem todos os números do plano. |
| 404 | not_found | O endereço ou o recurso não existe (ou é de outra conta). |
| 404 | call_not_found | A chamada não existe ou já terminou. |
| 405 | method_not_allowed | O endereço existe, mas não com este método HTTP. O cabeçalho Allow diz quais valem. |
| 409 | number_not_connected | O número por QR code não está conectado: leia o QR code antes de enviar ou ligar. |
| 409 | meta_token_invalid | A Meta recusou o token do número oficial (venceu, foi revogado ou não tem as permissões). Envie um novo em PUT /v1/numbers/{id}/meta. |
| 409 | not_a_qr_number | O pedido só vale para números por QR code. |
| 409 | not_an_official_number | O pedido só vale para números oficiais. |
| 409 | send_in_progress | Outro envio com a mesma Idempotency-Key ainda está em andamento. |
| 409 | action_in_progress | Outra ação de chamada com a mesma Idempotency-Key ainda está em andamento. |
| 409 | call_answered_elsewhere | Outro telefone web, o agente de voz ou o seu sistema já atendeu esta chamada. |
| 409 | call_not_ready | A chamada ainda está sendo preparada; tente de novo em instantes. |
| 413 | media_too_large | A mídia recebida por um número por QR code passava de 64 MB e não ficou guardada. |
| 422 | invalid_request | Os dados enviados são inválidos. O campo details lista os erros de cada campo. |
| 422 | unsupported_type | O tipo de mensagem só existe nos números oficiais (modelo, interativa…). |
| 422 | meta_refused | A Meta recusou as credenciais ao conectar um número oficial; a mensagem diz o motivo. |
| 422 | whatsapp_refused | A Meta recusou a mensagem de um número oficial (modelo inexistente, parâmetro inválido…). |
| 422 | not_on_whatsapp | O número chamado não tem WhatsApp. |
| 422 | call_failed | A Meta não aceitou a ação de chamada de um número oficial (sem permissão do contato, por exemplo). |
| 429 | rate_limited | O limite de requisições da conta (espere os segundos de Retry-After) ou o limite de envios da Meta num número oficial (envie mais devagar). |
| 429 | channel_limit_reached | A conta já está no limite de chamadas simultâneas do plano. |
| 429 | lines_busy | Todas as linhas do número estão em chamada. |
| 4xx | http_error | Outro erro do HTTP, como um corpo grande demais (413). O status diz qual. |
| 500 | server_error | Erro interno. Tente novamente; se o erro continuar, fale com a gente. |
| 502 | whatsapp_refused | O WhatsApp não aceitou a mensagem de um número por QR code (o destino não tem WhatsApp, por exemplo). |
| 502 | call_failed | O WhatsApp não aceitou a ação de chamada de um número por QR code. |
| 502 | media_unavailable | O WhatsApp não entregou o arquivo de uma mídia recebida por um número por QR code. |
| 503 | number_reconnecting | O número por QR code está se reconectando ao WhatsApp. Tente novamente em instantes. |
| 503 | engine_unavailable | O serviço que mantém os números por QR code está indisponível. Tente novamente em instantes. |
| 503 | meta_unavailable | O pedido não chegou à Meta (sem conexão) ou a Meta respondeu que está indisponível: nada foi feito. Tente novamente em instantes. |
| 503 | lines_starting | As linhas de chamada do número por QR code estão iniciando. Tente de novo em alguns segundos. |
| 504 | send_timeout | O envio demorou demais. Repita com a mesma Idempotency-Key. |
| 504 | meta_timeout | A Meta não respondeu a tempo depois de receber o pedido (um envio ou uma ação de chamada): ele pode ter sido feito ou não. Repita com a mesma Idempotency-Key. |
| 504 | call_timeout | O serviço que mantém os números por QR code demorou demais para responder a uma ação de chamada. |
Um 503 costuma passar em instantes: tente de novo, esperando um pouco mais a cada vez. Para saber se a Voza está fora, veja Status da Voza.
Limites
- Cada conta pode fazer até 600 requisições por minuto, somando todas as suas chaves, ou o limite combinado com a Voza para a conta:
GET /v1/accountmostra o da sua emlimits.requests_per_minute. - As respostas trazem
X-RateLimit-LimiteX-RateLimit-Remaining. Acima do limite, a API responde429comRetry-After. - Cada token do telefone web pode fazer até 240 requisições por minuto.
- Cada conta tem até 20 chaves de API.
- O plano define quantos números a conta conecta e quantas chamadas acontecem ao mesmo tempo. Mensagens e minutos não têm limite na Voza.
- Nos números oficiais valem também os limites de envio e a qualidade que a Meta dá a cada número.
Idempotência
Ao enviar uma mensagem, mande o cabeçalho Idempotency-Key (até 128 letras, números e _ . : -). Repetir o envio com a mesma chave devolve a resposta do primeiro em vez de enviar de novo. Use-o sempre que o seu sistema for repetir um envio que falhou ou ficou sem resposta.
Idempotency-Key: pedido-4521-aviso-entrega
A Voza guarda a primeira resposta por 24 horas, nos dois tipos de número. Um envio repetido enquanto o primeiro ainda está em andamento espera por ele até 30 segundos; depois disso, a resposta é 409 send_in_progress, e basta tentar de novo com a mesma chave.
As ações de chamada (POST /v1/numbers/{id}/calls) aceitam o mesmo cabeçalho: repetir com a mesma chave devolve a primeira resposta em vez de repetir a ação (um connect não liga duas vezes). Uma repetida enquanto a primeira está em andamento recebe 409 action_in_progress.
Num número oficial, 503 meta_unavailable quer dizer que o pedido não chegou à Meta, e 504 meta_timeout, que chegou, mas a Meta não respondeu a tempo: a mensagem pode ter sido enviada (ou a ação feita). Nos dois casos, repita com a mesma Idempotency-Key: um erro não fica guardado, então a repetição vai à Meta; a Meta não tem idempotência própria, e depois de um meta_timeout a mensagem pode chegar duas vezes ao contato.
Números por QR code
Um número por QR code é um WhatsApp ou WhatsApp Business comum conectado como aparelho, como no WhatsApp Web. O celular continua funcionando normalmente.
O objeto número
{
"id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
"name": "Loja Centro",
"external_ref": "canal-981",
"type": "qr",
"status": "connected",
"status_reason": null,
"phone": "5511987654321",
"qr_url": "https://www.vozaapi.com/qr/…",
"webhook_url": "https://seu-sistema.com/webhooks/whatsapp",
"agent_mode": "off",
"connected_at": "2026-10-22T15:04:05Z",
"created_at": "2026-10-22T15:01:10Z"
}
| Campo | Descrição |
|---|---|
external_ref | A sua referência para o número (até 100 caracteres), como o id do canal no seu sistema. Volta nos webhooks, em metadata.external_ref, para você achar o canal sem guardar o id da Voza. |
type | qr (QR code) ou meta (oficial). |
status_reason | Por que o número está no status atual (veja Status); vazio quando não há o que explicar. |
phone | Os dígitos do número, com o código do país, depois de conectado. |
qr_url | Uma página pública com o QR code, para mandar a quem está com o celular. Ela acompanha a conexão sozinha. |
agent_mode | Quando o agente de voz do número atende: off, always ou fallback (veja Agentes de voz). |
connected_at | Quando o número se conectou, em UTC (ISO 8601). |
Status
| Status | Significado |
|---|---|
starting | A conexão está começando; o QR code aparece em seguida. |
qr | Aguardando a leitura do QR code. |
connected | Conectado: o número envia e recebe mensagens e chamadas. |
failed | Recusado: pelo WhatsApp (um número bloqueado, por exemplo; use relink) ou, num número oficial, pela Meta (veja meta.error). |
Se alguém desconectar a Voza no celular (Aparelhos conectados), o número volta para starting e depois para qr, com um QR code novo. Cada mudança chega ao webhook como number.status.
status_reason diz por que o número está no status atual, e uma mudança só do motivo também chega ao webhook:
status_reason | Significado |
|---|---|
logged_out | O celular desconectou o aparelho (ou o número foi religado): é preciso ler o QR code de novo. |
restarting | O motor está reconectando o número, que continua ligado ao celular: espere, sem fazer nada. |
failed | O WhatsApp recusou o aparelho. |
meta_refused | A Meta recusou o token ou o número oficial (veja meta.error). |
Nos outros casos (qr, connected, um número novo) o motivo vem vazio.
Criar um número
{
"name": "Loja Centro",
"external_ref": "canal-981",
"webhook": { "url": "https://seu-sistema.com/webhooks/whatsapp" }
}
Responde 201 com o número em starting. external_ref e webhook são opcionais (webhook_url também é aceito, no lugar de webhook.url). Com um webhook no pedido, a resposta traz também o segredo que assina os eventos, para conferir já o primeiro:
{
"id": "01jbk3m8x2c9q4r7t6v5w0y1z3",
"status": "starting",
…
"webhook": { "url": "https://seu-sistema.com/webhooks/whatsapp", "secret": "whsec_…" }
}
O endereço do webhook pode nomear o número: {id} vira o id do número na Voza e {external_ref}, a sua referência (codificada para URL; o pedido precisa trazer external_ref). O webhook fica definido antes de o número conectar, então nenhum evento se perde, sem um PUT depois.
{ "name": "Loja Centro", "external_ref": "canal-981", "webhook": { "url": "https://seu-sistema.com/webhooks/voza/{external_ref}?numero={id}" } }
Acima do limite do plano, responde 403 com number_limit_reached.
Mostrar o QR code
{
"status": "qr",
"qr": "data:image/png;base64,iVBORw0KGgo…",
"qr_url": "https://www.vozaapi.com/qr/…"
}
qr é uma imagem PNG pronta para um <img src>. O QR code muda a cada 20 segundos: consulte de novo a cada 3 a 5 segundos até o status ser connected (então qr vem vazio). Ou mande o qr_url a quem está com o celular.
Listar, consultar e excluir
Todos os números da conta, em {"data": [...]}. Com ?external_ref=canal-981, só os que têm essa referência.
O número, com o status lido na hora.
Muda o name ou a external_ref do número ("external_ref": null a remove) e responde com o número.
Desconecta o celular e exclui o número. Responde 204.
Conectar outro celular
Desconecta o celular atual e começa de novo, com um QR code novo. Serve para trocar de celular ou para recuperar um número recusado.
Números oficiais
Um número oficial é um número da WhatsApp Cloud API, na sua conta da Meta. A Voza usa as credenciais da sua app: as mensagens e as chamadas passam pela API da Meta, e chegam ao seu webhook no mesmo formato dos números por QR code.
Antes de conectar, na Meta
- Tenha uma app da Meta com o produto WhatsApp e um número de telefone na conta do WhatsApp Business (WABA).
- Em WhatsApp › Configuração, assine os campos
messagesecallsdo webhook da app (com qualquer endereço de webhook: a Voza aponta para si só este número, com uma substituição por número, e os outros eventos da sua app continuam onde estão). - Crie um token permanente de um usuário do sistema, com as permissões
whatsapp_business_messagingewhatsapp_business_management. - Anote o ID do número de telefone e o ID da conta do WhatsApp Business (WhatsApp › Configuração da API), e a chave secreta da app (Configurações da app › Básico).
Conectar
{
"name": "Atendimento oficial",
"type": "meta",
"webhook_url": "https://seu-sistema.com/webhooks/whatsapp",
"meta": {
"phone_number_id": "109876543210987",
"waba_id": "102030405060708",
"access_token": "EAAG…",
"app_secret": "0123456789abcdef0123456789abcdef"
},
"calling": { "call_icon_visibility": "DISABLE_ALL" }
}
A Voza confere o número e o token na Meta, inscreve a sua app na conta, aponta os webhooks messages e calls do número para a Voza e ativa as chamadas. Se tudo der certo, responde 201 com o número já connected:
{
"id": "01jbk4…",
"type": "meta",
"status": "connected",
"phone": "5511912345678",
"qr_url": null,
"meta": {
"phone_number_id": "109876543210987",
"waba_id": "102030405060708",
"display_phone_number": "+55 11 91234-5678",
"verified_name": "Sua Empresa",
"quality_rating": "GREEN",
"calling": "enabled",
"error": null
}
}
- Se a Meta recusar as credenciais, a resposta é
422 meta_refusedcom o motivo, e nada fica guardado. calling(opcional) são as configurações de chamada com que o número começa (veja Configurações de chamada); o que faltar fica com o padrão da Voza: chamadas ativadas, botão de chamada visível e pedido de retorno ativado."calling": "keep"registra o número sem mexer nas configurações de chamada dele na Meta (para um número que já atende chamadas do jeito que você configurou). A Voza só as lê, para mostrarmeta.calling.- Na resposta,
meta.callingéenabled,disabled(desativadas nas configurações de chamada) ouunavailable(a Meta não libera chamadas para o número; as mensagens funcionam do mesmo jeito). - O token e a chave secreta ficam guardados encriptados e nunca aparecem nas respostas.
- A Voza confere o número na Meta no máximo uma vez por minuto ao consultá-lo, e confere todos os números oficiais a cada hora. Um token recusado (o código 190 da Meta) ou um número que a Meta não tem mais, nessa verificação, num envio de mensagem ou numa ação de chamada, leva o número a
failedcomstatus_reason: meta_refused(o motivo emmeta.error), e o webhook recebenumber.status. Quando a Meta volta a aceitar o número, ele volta aconnected.
Trocar o token
{ "access_token": "EAAG…novo", "app_secret": "(só se mudou)" }
Guarda as novas credenciais e conecta o número de novo. POST /v1/numbers/{id}/relink só verifica de novo, com as credenciais atuais. DELETE devolve os webhooks do número à sua app e o tira da Voza (o número continua na sua conta da Meta).
Nem a troca do token nem o relink mexem nas configurações de chamada: a Voza só as define quando o número é criado.
Configurações de chamada
O objeto calling das configurações do número na Meta, lido e mudado como na Meta. No PUT, só os campos enviados mudam, e a resposta é {"success": true}.
{
"calling": {
"status": "ENABLED",
"call_icon_visibility": "DEFAULT",
"callback_permission_status": "ENABLED",
"call_hours": {
"status": "ENABLED",
"timezone_id": "America/Sao_Paulo",
"weekly_operating_hours": [{ "day_of_week": "MONDAY", "open_time": "0900", "close_time": "1800" }]
}
}
}
| Campo | Descrição |
|---|---|
status | ENABLED ou DISABLED: as chamadas do número. |
call_icon_visibility | DEFAULT ou DISABLE_ALL: o botão de chamada na conversa. |
callback_permission_status | ENABLED ou DISABLED: pedir ao contato que ligou permissão para ligar de volta. |
call_hours, call_icons | O horário de atendimento e os países que veem o botão, como na Meta, que os confere. |
sip fica de fora: as chamadas passam pela Voza. Num número por QR code, a resposta é 409 not_an_official_number.
Enviar mensagens
O corpo é o mesmo da API oficial do WhatsApp. O número precisa estar conectado.
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/messages \ -H "Authorization: Bearer vz_SUA_CHAVE" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: pedido-4521" \ -d '{ "messaging_product": "whatsapp", "to": "5511987654321", "type": "text", "text": { "body": "Seu pedido saiu para entrega!" } }'
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "5511987654321", "wa_id": "5511987654321" }],
"messages": [{ "id": "3EB0C431D5A1B2C3" }]
}
Tipos de mensagem
| Tipo | Corpo | Números |
|---|---|---|
text | {"text": {"body": "…"}} (até 4096 caracteres) | todos |
image, video | {"image": {"link": "https://…", "caption": "…"}} | todos |
audio | {"audio": {"link": "https://…"}}; com "voice": true, nota de voz (use OGG/Opus) | todos |
document | {"document": {"link": "https://…", "filename": "nota-fiscal.pdf"}} | todos |
template | Um modelo aprovado: {"template": {"name": "…", "language": {"code": "pt_BR"}, "components": […]}} | oficiais |
interactive | Botões, listas, links e o pedido de permissão para ligar, como na Meta | oficiais |
sticker, location, contacts, reaction | Como na API da Meta | oficiais |
- Os links precisam ser endereços
httpspúblicos: o arquivo é baixado na hora do envio. textaceita tambémpreview_url, como na Meta, e as mídias,mime_type, o tipo do arquivo.- Para responder a uma mensagem, num número oficial, envie
"context": {"message_id": "wamid.…"}. - Para enviar a um grupo, use
"recipient_type": "group"e o id do grupo emto. Num número por QR code, o número precisa estar no grupo e o id é o do WhatsApp (120363…, com ou sem@g.us); a resposta vem semcontacts. Num número oficial, vale a API de grupos da Meta, com o id de grupo dela. - O
idda resposta volta nos status de entrega do webhook (sent,delivered,readoufailed). - Nos números oficiais, a Meta confere o corpo dos tipos só de números oficiais e diz o que estiver errado (whatsapp_refused).
A janela de 24 horas (números oficiais)
Num número oficial, a Meta só entrega mensagens livres a quem escreveu para o número nas últimas 24 horas. Fora dessa janela, comece a conversa com um modelo aprovado no WhatsApp Manager:
{
"to": "5511987654321",
"type": "template",
"template": {
"name": "pedido_enviado",
"language": { "code": "pt_BR" },
"components": [{ "type": "body", "parameters": [{ "type": "text", "text": "4521" }] }]
}
}
Uma mensagem livre fora da janela é aceita pela API, mas chega ao webhook como status failed, com o erro da Meta.
Modelos de mensagem
Os modelos da conta do WhatsApp Business de um número oficial, lidos na Meta, uma página por vez, na resposta da Meta. Cada modelo traz id, name, language, category, status e components, ou os campos pedidos em fields.
{
"data": [{
"id": "1234567890",
"name": "pedido_enviado",
"language": "pt_BR",
"category": "UTILITY",
"status": "APPROVED",
"components": [{ "type": "BODY", "text": "Seu pedido {{1}} saiu para entrega." }]
}],
"paging": {
"cursors": { "before": "MAZDZD", "after": "MjQZD" },
"next": "https://www.vozaapi.com/v1/numbers/NUMBER_ID/templates?after=MjQZD"
}
}
limit(1 a 100, 25 por padrão),afteroubefore(os cursores depaging.cursors) e os filtros da Meta:name,status(APPROVED…),categoryelanguage.paging.nextepaging.previousapontam para a Voza e só aparecem quando há outra página.- Num número por QR code, a resposta é
409 not_an_official_number: só os números oficiais têm modelos.
Botões
{
"to": "5511987654321",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Confirma a entrega amanhã às 10h?" },
"action": { "buttons": [
{ "type": "reply", "reply": { "id": "confirm", "title": "Sim" } },
{ "type": "reply", "reply": { "id": "reschedule", "title": "Remarcar" } }
] }
}
}
A escolha do contato chega ao webhook como uma mensagem do tipo interactive, com o id do botão.
Mídias
As mensagens recebidas com imagem, vídeo, áudio, documento ou figurinha trazem o id do arquivo (em image.id, audio.id…). Baixe-o com:
- A resposta são os bytes do arquivo, com o
Content-Typedele. - Números por QR code: as mídias ficam disponíveis por 24 horas; arquivos acima de 64 MB não ficam guardados (
media_too_large), e um arquivo que o WhatsApp não entregou respondemedia_unavailable. - Números oficiais: a Voza busca o arquivo na Meta, que o guarda por 30 dias.
Webhooks
Cada número tem o seu webhook. A Voza envia para ele as mensagens recebidas, os status de entrega das mensagens enviadas, os eventos de chamada e as mudanças de status do número.
curl -X PUT https://www.vozaapi.com/v1/numbers/NUMBER_ID/webhook \ -H "Authorization: Bearer vz_SUA_CHAVE" \ -H "Content-Type: application/json" \ -d '{"url": "https://seu-sistema.com/webhooks/whatsapp"}'
{ "url": "https://seu-sistema.com/webhooks/whatsapp", "secret": "whsec_…" }
O endereço precisa ser https e público. Envie "url": null para remover o webhook e, junto com a url, "rotate_secret": true para trocar o segredo. GET no mesmo endereço mostra a configuração atual.
Mensagens e status de entrega
Vêm no mesmo envelope da API oficial, então o código que lê os webhooks da Meta lê os da Voza:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "42",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID", "external_ref": "canal-981" },
"contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }],
"messages": [{
"from": "5521999990000",
"id": "ABCD1234",
"timestamp": "1761150000",
"type": "text",
"text": { "body": "Olá, tudo bem?" }
}]
}
}]
}]
}
Um status de entrega vem no mesmo envelope, em statuses:
"statuses": [{ "id": "3EB0C431D5A1B2C3", "status": "delivered", "timestamp": "1761150030", "recipient_id": "5521999990000" }]
phone_number_idé oiddo número na Voza, nos dois tipos de número, eexternal_ref, a sua referência para ele, quando o número tem uma.- Os status de entrega chegam em
statuses(sent,delivered,read,failed), com oidda mensagem enviada. Nos números oficiais trazem também opricinge aconversationda Meta, e oserrorsde uma falha. - Os
errorsque a Meta manda numa mudança (um problema de pagamento da conta, por exemplo) chegam emvalue.errors, como na Meta, mesmo quando a mudança só tem erros. Vale paramessagesecalls. - Tipos de mensagem recebida:
text,image,video,audio(comvoice: truenuma nota de voz),document,sticker,location,contactseunsupported; nos números oficiais, tambéminteractive,buttonereaction.
Status do número
{
"object": "voza",
"event": "number.status",
"number": { "id": "NUMBER_ID", "name": "Loja Centro", "external_ref": "canal-981", "status": "starting", "status_reason": "logged_out", "phone": null },
"timestamp": 1761150000
}
O evento test, enviado pelo painel, tem a mesma forma. status_reason diz por que o número mudou (veja Status).
Cabeçalhos e assinatura
| Cabeçalho | Conteúdo |
|---|---|
X-Voza-Signature | sha256= e o HMAC-SHA256 do corpo, com o segredo do webhook. O mesmo valor vai em X-Hub-Signature-256, como na Meta. |
X-Voza-Delivery | O id da entrega, igual em todas as tentativas: use-o para ignorar repetições. |
X-Voza-Event | messages, calls, number.status ou test |
User-Agent | Voza-Webhooks/1.0 |
Verifique a assinatura com o corpo cru, antes de ler o JSON:
// PHP $body = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $body, getenv('VOZA_WEBHOOK_SECRET')); if (! hash_equals($expected, $_SERVER['HTTP_X_VOZA_SIGNATURE'] ?? '')) { http_response_code(401); exit; } // Node.js (body é o Buffer cru da requisição) const expected = 'sha256=' + crypto.createHmac('sha256', process.env.VOZA_WEBHOOK_SECRET).update(body).digest('hex'); const given = req.headers['x-voza-signature'] || ''; const valid = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); # Python (body são os bytes crus da requisição) expected = 'sha256=' + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, request.headers.get('X-Voza-Signature', ''))
Respostas e novas tentativas
- Responda com qualquer status
2xxem até 10 segundos. Processe o evento depois, numa fila, se for demorado. - Sem um 2xx, a Voza tenta de novo depois de 1, 5, 15 e 30 minutos, 1, 2 e 4 horas, e a cada 8 horas até completar 24 horas.
- Uma conta pode ter um prazo maior, combinado com a Voza, de até 7 dias:
GET /v1/accountmostra o da sua emwebhook_retry_hours. - Os eventos de chamada têm uma fila só deles, à frente das mensagens. Sem um 2xx, a Voza tenta de novo depois de 5, 15 e 30 segundos. Os de uma chamada tocando (o connect e o status RINGING) desistem em 2 minutos, porque uma chamada não toca por mais que isso; o fim de uma chamada (terminate e os outros status) continua sendo tentado, depois disso, como um evento de mensagem.
- A Voza não segue redirecionamentos: um
3xxconta como falha. Configure o endereço final. - Um número sem webhook não guarda os eventos: como na Meta, eles se perdem. Configure o webhook antes de conectar o número.
- A ordem de chegada não é garantida: use o timestamp de cada mensagem.
- O conteúdo do evento só fica na Voza, encriptado, até a entrega terminar. O histórico no painel guarda 7 dias de entregas, sem o conteúdo.
Telefone web
O jeito mais simples de atender e fazer ligações: um SDK em JavaScript, sem dependências, que roda no navegador dos seus atendentes. A chave de API fica no seu servidor; o navegador recebe só um token curto, de um número. Funciona com os dois tipos de número.
1. No seu servidor, peça um token para o atendente:
curl https://www.vozaapi.com/v1/numbers/NUMBER_ID/webphone-tokens \ -H "Authorization: Bearer vz_SUA_CHAVE" \ -H "Content-Type: application/json" \ -d '{"label": "Ana", "minutes": 480}'
{ "token": "wp_…", "expires_at": "2026-10-23T01:00:00Z", "sdk_url": "https://www.vozaapi.com/sdk/webphone.js" }
minutes vai de 5 a 720 (8 horas por padrão). Use um token por aba do navegador.
2. Na página do atendente, o telefone pronto:
<script src="https://www.vozaapi.com/sdk/webphone.js"></script> <voza-phone token="wp_…" lang="pt-BR"></voza-phone>
- O elemento fala português, inglês e espanhol: o idioma do atributo
langou, sem ele, o da página ou o do navegador. Os outros idiomas recebem inglês. - O elemento segue o tema claro ou escuro da página; as variáveis CSS
--voza-phone-background,--voza-phone-colore--voza-phone-radiusmudam as cores. - O SDK fala com a Voza no endereço de onde foi carregado. Se servir o
webphone.jsde outro lugar (do seu domínio ou num bundle), diga onde está a Voza:base-url="https://www.vozaapi.com"no elemento oubaseUrlemnew VozaWebphone().
Ou a sua própria interface, com a API do SDK:
const phone = new VozaWebphone({ token: 'wp_…' });
phone.on('incoming', (call) => {
// call.contact, call.contactName; call.accept() ou call.reject()
});
phone.on('call', (call) => console.log(call.direction, call.state));
phone.on('error', (error) => console.warn(error.code, error.message));
phone.on('disconnected', () => { /* o token venceu: peça outro */ });
await phone.connect();
const call = await phone.call('5511987654321');
call.mute(true);
call.hangup();
phone.disconnect(); // para de seguir as chamadas e desliga as que estão em curso
| Evento | Quando acontece |
|---|---|
ready | O telefone está pronto. Recebe a sessão, com o número (veja API do telefone web). |
state | O telefone mudou de estado: connecting, ready ou closed. |
incoming | Uma chamada recebida começou a tocar. |
call | Qualquer mudança numa chamada: um novo estado ou o mudo. |
ended | Uma chamada terminou; call.reason diz por quê. |
monitor | Uma supervisão aberta com phone.monitor() mudou de estado (veja Supervisionar uma chamada da IA). |
error | Um erro, com error.code: microphone_denied, microphone_unavailable, insecure_context, unsupported_browser, connection_failed, network_error ou um código da API, como not_on_whatsapp. |
disconnected | O token venceu ou foi recusado: peça outro e crie um novo telefone. |
- Uma chamada tem
id,direction(incomingououtgoing),contact,contactName,state,reason,answeredBy(quem atendeu uma chamada recebida:webphone,agentouapi),mutededuration(segundos desde que foi atendida). - Estados de uma chamada recebida:
ringing,connecting,active,ended; se o agente de voz atendeu,ringing,agent(com a IA: o elemento mostra “Atendida pela IA”) eended. De uma chamada feita:connecting,dialing,ringing,active,ended.call.reasondiz por que terminou:completed,rejected,missed,no_answer,hangup,answered_elsewhere,answered_by_agent(a chamada com a IA terminou),failedou o código de um erro. - A página precisa de https (ou localhost) e da permissão do microfone.
- Quando vários atendentes recebem a mesma chamada, o primeiro que atende fica com ela; nos outros ela some.
- Uma chamada que a IA atendeu continua na tela até terminar, mas este telefone não a atende nem desliga:
call.hangup()não faz nada, e encerrá-la é a açãoterminatedo seu servidor.
Supervisionar uma chamada da IA
Um supervisor ouve, pelo navegador, uma chamada que o agente de voz atendeu, e pode assumi-la e devolvê-la à IA. Entrar só pede um token curto; assumir, devolver e sussurrar precisam da chave de API, então o SDK chama funções da sua página, e elas pedem ao seu servidor.
1. No seu servidor, peça um token de supervisão com POST /v1/numbers/{id}/calls/{call_id}/monitor e passe {token, ice_servers} ao navegador. Cada token entra uma vez, em até 5 minutos.
2. No navegador, com a API do SDK:
// a sua função que chama o seu servidor const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const monitor = await phone.monitor(callId, { // ou VozaWebphone.monitor(), sem telefone web token: 'mon_…', iceServers, // os ice_servers do token takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }), release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }), whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }), }); monitor.on('state', (m) => console.log(m.state)); // listening, takeover, ended await monitor.takeover(); // pede o microfone e fala com quem ligou await monitor.whisper('Ofereça a sexta às 9h30.'); await monitor.release(); // a IA volta a falar monitor.leave();
- No seu servidor, cada função vira uma chamada à Voza com a chave de API:
takeoveremPOST …/calls/{call_id}/takeover{"monitor": monitorId},releaseemPOST …/calls/{call_id}/releaseewhisperemPOST …/calls/{call_id}/whisper{"text"}(veja Supervisão). - As funções recebem
{callId, monitorId, monitor}(etext, no sussurro) e podem devolver aResponsedofetch: uma resposta de erro rejeita a promessa com oerror.codeque o seu servidor repassar (por exemplonot_in_control). Sem a função, o método rejeita comtakeover_unavailable,release_unavailableouwhisper_unavailable. - Ouvir não pede o microfone: ele só é pedido ao assumir (ou ao entrar com
mode: "takeover").monitor.mute()silencia o supervisor enquanto ele está com a chamada. - Estados:
connecting,listening,takeover,ended.monitor.reasondiz por que terminou:left(leave(), que antes devolve a chamada à IA se o supervisor estiver com ela),completed(a chamada terminou) ou o código de um erro, comoinvalid_monitor_token,token_usedoucall_not_with_agent. Eventos:state,erroreended. monitor.leave()avisa a Voza na hora (DELETE /v1/monitor/sessioncom o mesmo token, que serve para sair mesmo depois de vencer), e o SDK faz o mesmo quando a página fecha; assim a IA recupera a chamada sem esperar a conexão cair.- Um token entra uma vez: para voltar a ouvir depois de sair, peça outro. Uma chamada no estado
agenttambém temcall.monitor({token, …}).
3. Ou no telefone pronto: com as funções em supervisor, uma chamada atendida pela IA mostra “Atendida pela IA” e o botão Ouvir; a supervisão tem Assumir, Devolver à IA, Mudo, Sair e um campo de instrução para a IA.
const element = document.querySelector('voza-phone'); element.supervisor = { token: (call) => post('/supervise/token', { callId: call.id }), // o seu servidor devolve {token, ice_servers} takeover: ({ callId, monitorId }) => post('/supervise/takeover', { callId, monitorId }), release: ({ callId, monitorId }) => post('/supervise/release', { callId, monitorId }), whisper: ({ callId, text }) => post('/supervise/whisper', { callId, text }), }; // ou, para uma chamada que você escolheu: element.monitor(callId, { token: 'mon_…', iceServers });
- Sem a função
whisper, o campo de instrução não aparece; semtakeoverourelease, os botões delas também não. - Sem o atributo
token, o elemento só supervisiona: mostra a supervisão aberta commonitor().
Chamadas pela API
Para quem prefere o seu próprio cliente WebRTC: as mesmas ações e eventos da API de chamadas da Meta. A Voza não grava as chamadas.
| Ação | Corpo |
|---|---|
connect | Liga para to com a oferta SDP do navegador em session (sdp_type: offer). Responde {"calls": [{"id": "…"}]}; a resposta SDP chega no evento connect. |
pre_accept | Para uma chamada recebida: a resposta SDP do navegador (sdp_type: answer), para o áudio já se conectar. |
accept | Atende a chamada no WhatsApp. Nos números oficiais, envie de novo a mesma resposta SDP em session. |
reject, terminate | Recusa ou encerra a chamada call_id. |
{
"action": "pre_accept",
"call_id": "wacid.ABGG…",
"session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" }
}
- O SDP não tem trickle ICE: envie o SDP completo, depois de reunir os candidatos.
- Números por QR code: o áudio vai até os servidores da Voza, que ficam atrás de NAT e só se alcançam por TURN. Use os servidores de
ice_serversemGET /v1/webphone/session(veja API do telefone web) e peça-os de novo antes de cada chamada: as credenciais vencem. - Números oficiais: para ligar para um contato, a Meta exige a permissão dele. Peça-a com uma mensagem
interactivedo tipocall_permission_request; a resposta chega ao webhook. - Cada plano tem um número de chamadas simultâneas. Uma chamada recebida acima do limite é recusada na hora: o webhook recebe o fim dela (
terminate, comstatus: REJECTEDevoza_reason: channel_limit), sem a oferta SDP, e ela fica no registro comend_reason: channel_limit. Os telefones web não a veem tocar. - Uma chamada que o agente de voz recusou (veja Quem atende a chamada) chega ao fim com
voza_reason: agent_unavailablee o motivo emvoza_detail. - Como na Meta, as ações aceitam
biz_opaque_callback_data(até 512 caracteres).
Permissão para ligar
Se o número pode ligar para o contato agora. Num número oficial, é a resposta da consulta call_permissions da Meta; num número por QR code, sempre {"permission": {"status": "granted"}}, porque não há permissão a pedir.
{
"messaging_product": "whatsapp",
"permission": { "status": "temporary", "expiration_time": 1761236400 },
"actions": [{ "action_name": "start_call", "can_perform_action": true, "limits": [ … ] }]
}
status é granted, temporary (vence em expiration_time) ou no_permission. wa_id é o número do contato, só com algarismos.
Eventos
Os eventos chegam no webhook do número com field: calls, no envelope da Meta: calls[] com event: connect (a oferta SDP de uma chamada recebida, ou a resposta de uma chamada feita) e event: terminate (com duration quando foi atendida), e statuses[] de uma chamada feita (RINGING, ACCEPTED, REJECTED). direction é USER_INITIATED numa chamada recebida e BUSINESS_INITIATED numa feita.
"value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "5511987654321", "phone_number_id": "NUMBER_ID" }, "contacts": [{ "wa_id": "5521999990000", "profile": { "name": "Bruno" } }], "calls": [{ "id": "wacid.ABGG…", "from": "5521999990000", "to": "5511987654321", "event": "connect", "direction": "USER_INITIATED", "timestamp": "1761150000", "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" } }] }
Quando a chamada termina:
"calls": [{ "id": "wacid.ABGG…", "event": "terminate", "direction": "USER_INITIATED", "status": "COMPLETED", "duration": 126, … }]
Registro de chamadas
As chamadas do número, das mais novas para as mais antigas: ?limit= de 1 a 200, 50 por padrão.
{
"data": [{
"id": 981,
"call_id": "wacid.ABGG…",
"direction": "incoming",
"contact": "5521999990000",
"contact_name": "Bruno",
"status": "completed",
"end_reason": null,
"end_detail": null,
"started_at": "2026-10-22T15:20:00Z",
"answered_at": "2026-10-22T15:20:04Z",
"ended_at": "2026-10-22T15:22:10Z",
"duration": 126,
"answered_by": "agent",
"agent": { "agent_ref": "agent_42", "session_id": "…", "seconds": 124 }
}]
}
| Campo | Descrição |
|---|---|
call_id | O id da chamada no WhatsApp, o mesmo dos eventos e das ações. |
status | dialing, ringing, active, completed, missed, rejected, no_answer ou failed. |
end_reason | channel_limit: recusada por passar do limite de chamadas simultâneas. agent_unavailable: recusada porque o agente de voz não pôde atender. lost: ficou mais de 6 horas sem um fim e foi encerrada como failed. Vazio nas outras. |
end_detail | Com agent_unavailable, por que o agente recusou a chamada (veja Quem atende a chamada). Vazio nas outras. |
answered_at | Quando foi atendida; vazio numa chamada não atendida. Os horários são em UTC. |
duration | Os segundos de conversa; vazio numa chamada não atendida. |
answered_by | Quem atendeu: webphone, agent (o agente de voz) ou api (o seu sistema); numa chamada feita, quem ligou. Vazio numa chamada que ninguém atendeu. |
agent | Numa chamada do agente de voz: agent_ref, a sessão no motor e os segundos da IA (sem o tempo em que um supervisor estava com a chamada). |
API do telefone web
O que o SDK usa por baixo, para quem faz o próprio telefone no navegador. Estes endereços aceitam o token do telefone web, não a chave de API, e respondem a páginas de qualquer domínio (CORS). O token vai em Authorization: Bearer wp_… ou, no fluxo de eventos, em ?token=, porque o EventSource não envia cabeçalhos.
{
"session_id": "01jbk5…",
"label": "Ana",
"number": { "id": "01jbk3…", "name": "Loja Centro", "phone": "5511987654321", "status": "connected" },
"ice_servers": [{ "urls": ["turn:turn.cloudflare.com:3478?transport=udp", …], "username": "…", "credential": "…" }],
"expires_at": "2026-10-23T01:00:00Z"
}
ice_servers vai direto para new RTCPeerConnection({ iceServers }). Peça a sessão de novo antes de cada chamada: as credenciais do TURN vencem.
As chamadas do número, por Server-Sent Events. O evento calls traz o mesmo value do webhook de chamadas (contacts, calls e statuses, com o SDP). O evento claimed diz que alguém atendeu uma chamada, com call_id, session_id e by (webphone, agent ou api); se o session_id não for o seu, ela foi atendida em outro lugar.
const events = new EventSource('https://www.vozaapi.com/v1/webphone/events?token=wp_…'); events.addEventListener('calls', (event) => { const { calls, statuses } = JSON.parse(event.data); }); events.addEventListener('claimed', (event) => { const { call_id, session_id, by } = JSON.parse(event.data); });
- Cada evento tem um
id. Ao reconectar, oEventSourceenviaLast-Event-IDe o fluxo continua do evento seguinte (semEventSource, use?after=). Uma conexão nova começa 20 segundos antes, para mostrar uma chamada que já está tocando. - O fluxo fecha a cada 300 segundos, e o
EventSourcereconecta sozinho; quando o token vence, fecha de vez. A cada 15 segundos chega um comentário (: ping), para a conexão não cair.
As ações de chamada no número do token, com o mesmo corpo de POST /v1/numbers/{id}/calls. Quando vários telefones recebem a mesma chamada, o primeiro pre_accept ou accept fica com ela, e os outros recebem 409 call_answered_elsewhere. O mesmo vale contra o agente de voz e contra o seu sistema com a chave de API (veja Agentes de voz).
Agentes de voz
Um agente de voz atende as chamadas de um número com IA: ouve, pensa e fala. Funciona nos números por QR code e nos oficiais, com a mesma API. A lógica é do seu produto: a Voza não guarda prompt nem configuração, só qual agente do seu produto atende o número e onde perguntar. No começo de cada chamada, o motor de agentes da Voza pede a configuração ao seu produto, e as ferramentas (agenda, base de conhecimento, mensagens) também são chamadas ao seu produto. Uma pessoa sempre pode ouvir, orientar a IA e assumir a chamada.
A Voza não guarda o conteúdo: a transcrição e o resumo vão para o seu produto nos eventos, e só ficam na Voza até a entrega. O motor de agentes guarda só o áudio das frases fixas do agente (a saudação, as frases de espera e as mensagens de handoff), para tocá-las sem esperar a voz sintética; uma frase que não toca há 30 dias sai, e apagar a conta apaga as dela. As chaves dos provedores de IA são as da sua conta.
O agente de um número
{
"mode": "always",
"fallback_after_seconds": 15,
"agent_ref": "agent_42",
"callback_url": "https://seu-sistema.com/voza/agents",
"events_url": "https://seu-sistema.com/voza/events"
}
| Campo | Descrição |
|---|---|
mode | always: o agente atende toda chamada recebida. fallback: os telefones web tocam primeiro e, se ninguém atender em fallback_after_seconds (de 5 a 120, 15 por padrão), o agente atende. off: o agente só atende as chamadas que o seu produto entregar a ele. |
agent_ref | O agente no seu produto (até 255 caracteres). A Voza o repete em cada pedido e evento. |
callback_url | Onde o motor pede a configuração (agent.config) e chama as ferramentas (tool.call). https e público. |
events_url | Opcional: para onde vão os eventos do agente. Sem ela, vão para o webhook do número. |
Na primeira vez, a resposta traz agent.secret: ele assina os pedidos do motor ao seu produto e os eventos enviados a events_url. Guarde-o: GET no mesmo endereço mostra a configuração sem ele. POST /v1/numbers/{id}/agent/secret cria um novo (o anterior para de valer na hora) e DELETE desliga o agente e esquece o segredo.
{ "agent": { "mode": "always", "fallback_after_seconds": 15, "agent_ref": "agent_42", "callback_url": "https://…/voza/agents", "events_url": "https://…/voza/events", "secret": "agsec_…" } }
Quem atende a chamada
Uma chamada recebida fica com quem atender primeiro: um telefone web, o agente ou o seu sistema com a chave de API (pre_accept ou accept em POST /v1/numbers/{id}/calls). Os outros recebem 409 call_answered_elsewhere, e os telefones web recebem o evento claimed com by: webphone, agent ou api. O registro de chamadas mostra quem atendeu em answered_by.
Com o agente em always, logo que a chamada chega, o agente fica com ela e o motor responde à oferta SDP; os telefones web a veem tocar e, em seguida, o claimed. O webhook do número recebe os eventos calls como sempre. Se o motor não puder atender (o seu produto não mandou a configuração e não havia uma guardada, falta uma chave de IA…), a chamada é recusada e fica no registro com end_reason: agent_unavailable. Em fallback, ela continua tocando nos telefones web.
O seu produto também pode recusar uma chamada: se ele responder ao agent.config com 409 ou 410 e {"error": {"code": "agent_unavailable"}}, o motor não usa a configuração guardada e não atende. Em always, a chamada é recusada (end_reason: agent_unavailable); em fallback, ela continua tocando nos telefones web; entregue pela API, a resposta é 409 agent_unavailable.
Uma conta bloqueada (teste encerrado, assinatura encerrada ou suspensa) não tem agente, como não tem API nem telefone web: a chamada chega ao webhook do número e fica tocando até quem ligou desistir.
Quando o agente não pode ficar com uma chamada que toca, o seu produto recebe o evento call.agent.declined, com o motivo em reason, o código da recusa em detail, o modo do número em mode e, em outcome, o que aconteceu com a chamada: rejected (recusada, em always) ou ringing (continua tocando nos telefones web, em fallback). Uma chamada recusada fica no registro com end_reason: agent_unavailable e o motivo em end_detail. Entregue pela API, a chamada não gera o evento: o seu produto recebe o erro.
reason | Quando acontece |
|---|---|
product_refused | O seu produto recusou a chamada (respondeu ao agent.config com 409 ou 410 e agent_unavailable). |
config_unavailable | O seu produto não enviou uma configuração válida a tempo, e o motor não tinha uma guardada. |
missing_provider_key | Falta a chave de um provedor de IA que o agente usa. |
invalid_offer | O motor não conseguiu responder à oferta SDP. |
at_capacity, draining | O motor de agentes está no limite de chamadas, ou reiniciando (a Voza pede de novo uma vez, dentro do mesmo prazo). |
account_limit | A conta já tem todas as sessões de agente simultâneas que pode ter (limits.agent_sessions em GET /v1/account). |
runtime_unavailable, runtime_error | O motor de agentes não respondeu a tempo, ou falhou. |
whatsapp_refused | A Meta ou o motor de QR recusou a resposta do agente (pre_accept). |
error | Outra falha da Voza. |
Cada conta tem um limite de sessões de agente ao mesmo tempo, somando as chamadas que os agentes atendem e as chamadas de teste, e um limite menor só para as chamadas de teste: limits.agent_sessions e limits.agent_tests em GET /v1/account (por padrão, 10 e 2; a Voza muda os de uma conta parceira). Acima dele, uma chamada em always é recusada (account_limit), uma em fallback continua tocando nos telefones web, e a API responde 429 agent_limit_reached.
Entrega ao agente uma chamada recebida que ainda está tocando: o seu produto decide chamada a chamada. {"agent_ref": "…"} é opcional (outro agente do seu produto). Responde {"success": true, "call": {…}}, ou 409 call_answered_elsewhere se alguém já atendeu e 409 call_not_ringing se ela já terminou. call_id é o id da chamada no WhatsApp; o id dela na Voza também vale.
O que o motor pede ao seu produto
Pedidos POST à callback_url, com X-Voza-Event (agent.config ou tool.call), X-Voza-Timestamp (segundos Unix) e X-Voza-Signature: sha256=…, o HMAC-SHA256 de {timestamp}.{corpo} (o timestamp, um ponto e o corpo como chegou) com o segredo do agente. Confira a assinatura e recuse um X-Voza-Timestamp a mais de 5 minutos do seu relógio: assim um pedido capturado não pode ser repetido depois.
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
$valid = hash_equals($expected, $signature) && abs(time() - (int) $timestamp) <= 300;
No começo de cada chamada, a configuração (responda em até 2 segundos):
{ "type": "agent.config", "agent_ref": "agent_42", "call": { "id": "01jbk5…", "direction": "incoming", "test": false }, "number": { "id": "NUMBER_ID", "phone": "5511987654321", "type": "qr" }, "contact": { "wa_id": "5521999990000", "name": "Bruno" } }
{
"version": "42.3",
"language": "pt-BR",
"greeting": { "text": "Olá, aqui é a assistente virtual da Clínica Sorriso. Como posso ajudar?", "interruptible": false },
"instructions": "Marque, remarque e desmarque consultas. Confirme a data em voz alta.",
"guardrails": ["Nunca dê conselho médico."],
"voice": { "provider": "elevenlabs", "voice_id": "…", "speed": 1.0 },
"stt": { "provider": "deepgram", "language": "multi" },
"llm": { "provider": "anthropic", "model": "claude-haiku-5-5" },
"turn": { "interruption": "medium", "silence_ms": 600 },
"tools": [{
"name": "find_slots",
"description": "Horários livres na agenda",
"parameters": { "type": "object", "properties": { "day": { "type": "string" } }, "required": ["day"] },
"filler": "Um instante, vou ver a agenda."
}],
"handoff": {
"on_request": true,
"message": "Vou passar para a equipe, um momento.",
"timeout_seconds": 45,
"timeout_message": "Ninguém da equipe pode atender agora. Posso anotar um recado?",
"timeout_action": "resume"
},
"error_message": "Desculpe, estou com um problema técnico. Vou passar para a equipe.",
"max_duration_seconds": 900
}
O motor guarda a última configuração de cada agente pela version: se o seu produto não responder a tempo, usa a guardada. Cada ferramenta é chamada com {"type": "tool.call", "call_id", "agent_ref", "tool", "arguments"} e tem 8 segundos para responder {"result": …}, com say (uma frase a dizer), end_call: true ou handoff: true opcionais. A base de conhecimento é uma ferramenta como outra qualquer.
stt.language: o idioma de quem liga (pt-BR…) oumulti, quando ele pode falar mais de um, até na mesma frase.multisó vale com os modelos da Deepgram que transcrevem vários idiomas (nova-3, o padrão, ounova-2, que só junta inglês e espanhol); com outrostt.model, a configuração é recusada.languagecontinua sendo o idioma em que o agente fala.handoff: depois de passar a chamada para a equipe, o agente esperatimeout_seconds(de 10 a 600, 45 por padrão) que um supervisor a assuma. Se ninguém assumir, o eventocall.agent.statetrazhandoff_timeout, o agente diztimeout_messagee, comtimeout_action: resume(o padrão), volta a atender e oferece anotar um recado (sem mensagem, a IA avisa com as próprias palavras); comend, despede-se com a mensagem (obrigatória) e encerra a chamada (reason: handoff_timeoutemcall.agent.ended). Quem liga nunca fica em silêncio atémax_duration_seconds.max_duration_secondsconta só o tempo da IA: enquanto um supervisor está com a chamada, o relógio para, e um supervisor que devolve a chamada depois desse tempo a encerra (max_duration). Toda chamada termina em 4 horas, com quem estiver nela (max_call_duration).error_message(opcional): o que o agente diz quando um provedor falha no meio da chamada (veja Quando um provedor falha).
Quando um provedor falha
Se a transcrição, a IA ou a voz falharem no meio de uma chamada (o provedor recusou a chave da conta, continua falhando, ou a IA não responde a um turno em 20 segundos), quem liga não fica em silêncio: o seu produto recebe call.agent.error, o agente diz error_message (ou, sem ela, a mensagem do handoff; se a própria voz falhou, só uma frase já guardada, ou então um toque) e passa a chamada para a equipe, se o agente tiver handoff.on_request, ou a encerra (reason: provider_error). A IA não volta a atender nessa chamada: se a equipe a devolver, ou ninguém a assumir, ela termina.
{ "service": "llm", "provider": "anthropic", "status": 401, "category": "authentication", "action": "handoff" }
service é stt, llm ou tts; status, o status HTTP do provedor, quando há um; category, o tipo de falha (authentication, rate_limit, quota, server, timeout…); action, handoff ou end.
Eventos do agente
Vão para a events_url do agente, assinados com o segredo do agente, ou para o webhook do número, assinados com o segredo do webhook. Têm object: voza e o nome do evento em event e em X-Voza-Event. Saem numa fila só deles, e os de uma chamada saem em ordem, um depois do outro: um evento que falhou segura os seguintes da mesma chamada até a nova tentativa. As novas tentativas são depois de 5, 15 e 30 segundos, por até 2 minutos; call.agent.ended (com a conversa inteira), call.agent.error e call.agent.declined continuam depois, pelo prazo dos eventos de mensagem.
{
"object": "voza",
"event": "call.transcript",
"event_id": "5f2c…",
"number": { "id": "NUMBER_ID", "name": "Loja Centro", "external_ref": null, "status": "connected", "status_reason": null, "phone": "5511987654321" },
"call": { "id": "01jbk5…", "call_id": "wacid.ABGG…", "direction": "incoming", "contact": "5521999990000", "contact_name": "Bruno", "agent_ref": "agent_42", "agent_session_id": "…" },
"test": false,
"data": { "speaker": "caller", "text": "Quero remarcar a consulta de sexta.", "start": 4.2, "end": 6.9 },
"timestamp": 1761150012,
"timestamp_ms": 1761150012345
}
| Evento | Quando | Em data |
|---|---|---|
call.agent.started | O agente atendeu. | call, number, contact, agent_ref e a versão da configuração (config_version). |
call.agent.state | A cada mudança. | state: listening, thinking, speaking, handoff, handoff_timeout (ninguém assumiu a chamada a tempo) ou supervised. |
call.transcript | A cada fala completa. | speaker (caller, agent ou supervisor), text, start e end em segundos desde o atendimento. |
call.agent.tool_call | A cada ferramenta. | name, duration_ms e result (ok, timeout, error); sem os argumentos. |
call.metrics | A cada turno. | As latências em milissegundos: stt_ms, llm_ms (até a primeira palavra), tts_ms e end_to_end_ms. |
call.supervision | Um supervisor entra, muda de modo, sussurra ou sai. | action, monitor_id e mode. |
call.agent.error | Um provedor falhou no meio da chamada. | service, provider, status, category e action (veja Quando um provedor falha). |
call.agent.declined | O agente não pôde ficar com uma chamada que tocava. | reason, detail, mode e outcome (veja Quem atende a chamada). |
call.agent.ended | O fim. | reason, duration_seconds, os segundos da IA (ai_seconds) e os de um supervisor com a chamada (supervised_seconds), a transcrição completa (transcript), as ferramentas usadas (tools) e o uso (usage). |
timestamp é o instante do evento em segundos, e timestamp_ms, o mesmo instante em milissegundos: use-o para ordenar eventos do mesmo segundo.
event_id é o mesmo em cada tentativa: use-o para não processar um evento duas vezes. As chamadas atendidas pelo agente e os segundos da IA entram no uso (agent_calls e agent_seconds em GET /v1/account/usage).
O reason de call.agent.ended: remote_ended (quem ligou desligou), agent ou tool (o agente encerrou), handoff_timeout, max_duration, max_call_duration, idle, media_closed, connect_timeout, accept_failed, provider_error (um provedor falhou), agent_removed, number_removed e account_blocked (o agente ou o número foi removido, ou a conta bloqueada: a chamada termina na hora), runtime_lost (o motor de agentes caiu no meio da chamada: a Voza encerra a chamada e manda este evento, sem a transcrição) ou shutdown.
Supervisão
Quem pode supervisionar é decidido pelo seu produto; a Voza só confere a chave de API. Encerrar a chamada continua sendo a ação terminate.
Um token curto (mon_…, 5 minutos, uma vez) para o navegador do supervisor, com os ice_servers. O navegador envia a oferta SDP dele para POST /v1/monitor/session com Authorization: Bearer mon_… e recebe a resposta do motor e o monitor_id. mode é listen (ouve; o padrão) ou takeover (já entra falando). Esse endereço aceita páginas de qualquer domínio (CORS).
{ "session": { "sdp_type": "offer", "sdp": "v=0\r\no=- …" }, "mode": "listen" }
{ "monitor_id": "mon_1", "call_id": "wacid.ABGG…", "session": { "sdp_type": "answer", "sdp": "v=0\r\no=- …" } }
| Endereço | Corpo | O que faz |
|---|---|---|
POST …/calls/{call_id}/whisper | {"text": "Ofereça a sexta às 9h30."} | O texto entra no contexto da IA; quem ligou não ouve. |
POST …/calls/{call_id}/takeover | {"monitor": "mon_1"} | A IA para de falar e o microfone daquele supervisor vai para a chamada. |
POST …/calls/{call_id}/release | {} | Devolve a chamada à IA. |
DELETE …/calls/{call_id}/monitor/{monitor_id} | Tira um supervisor da chamada; se ele estava com ela, a IA volta a falar. |
No navegador, o SDK do telefone web faz a parte do supervisor: veja Supervisionar uma chamada da IA.
Chamada de teste
Conversa com um agente pelo navegador, sem WhatsApp: envie {"agent_ref", "callback_url"} e receba {id, token, ice_servers, expires_at}. O navegador envia a oferta SDP para POST /v1/agents/test-sessions/connect com Authorization: Bearer agt_… (uma vez) e recebe a resposta do motor. Com number_id, o motor assina com o segredo do agente desse número, e os eventos saem com "test": true para onde vão os do número; sem ele, a resposta traz um secret só desta chamada, e os eventos não são enviados. DELETE /v1/agents/test-sessions/{id} encerra.
Uma chamada de teste dura no máximo 10 minutos e conta nas sessões de agente da conta: acima de limits.agent_tests chamadas de teste ao mesmo tempo, connect responde 429 test_session_limit_reached (e 429 agent_limit_reached acima de limits.agent_sessions). O token pode ser usado de novo depois de uma recusa assim.
Voz e chaves de IA
Guarda, encriptadas, as chaves dos provedores de IA da conta: anthropic, deepgram, elevenlabs e cartesia. Só os provedores enviados mudam, e null remove uma chave. Só uma chave de API criada por um proprietário da conta pode mudá-las (senão, 403 owner_only). Elas nunca são devolvidas: a resposta, como GET, só diz quais existem.
Antes de guardar, a Voza confere cada chave nova no próprio provedor, num endereço que não gasta nada (a lista de modelos, de projetos ou de vozes). Se ele recusar uma chave (401), nenhuma chave do pedido fica guardada, e a resposta é 422 invalid_provider_key, com os provedores em meta.providers. Um provedor fora do ar, lento, ou que aceita a chave sem a permissão daquela lista, não impede a chave.
{ "anthropic": "sk-ant-…", "deepgram": "…" }
{ "anthropic": true, "deepgram": true, "elevenlabs": false, "cartesia": false }
{"provider", "voice_id", "text", "speed"} → o áudio (audio/mpeg, ou audio/wav com "format": "wav"), com a chave do provedor guardada na conta. Serve para ouvir a saudação antes de publicá-la. O texto não fica guardado; uma frase fixa que uma chamada do agente já guardou (a mesma voz e o mesmo texto) volta sem esperar (X-Voza-Cache: hit).
As vozes da conta num provedor (elevenlabs ou cartesia), lidas com a chave guardada na conta, então as vozes clonadas vêm também. Cada voz traz id (o voice_id da configuração e de /v1/tts), name, languages, gender e accent quando o provedor diz, e preview_url. A lista fica guardada por uma hora, por conta e provedor.
{ "provider": "elevenlabs", "data": [{ "id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel", "languages": ["en", "pt-BR"], "gender": "female", "accent": "american", "preview_url": "https://…/preview.mp3" }] }
Erros dos agentes
| Status | Código | Quando acontece |
|---|---|---|
| 401 | invalid_monitor_token, invalid_test_token | O token de supervisão ou da chamada de teste está ausente, é inválido ou venceu. |
| 403 | owner_only | Só uma chave de API criada por um proprietário guarda as chaves de IA. |
| 404 | monitor_not_found | Não há um supervisor com esse id na chamada. |
| 409 | agent_not_configured | O número não tem um agente. |
| 409 | agent_unavailable | O seu produto recusou a chamada (respondeu ao agent.config com 409 ou 410 e agent_unavailable). |
| 409 | call_not_ringing | A chamada não está mais tocando. |
| 409 | call_not_with_agent | A chamada não está com o agente: outra pessoa atendeu, ela terminou ou ainda não foi atendida. |
| 409 | token_used | O token de supervisão ou da chamada de teste já foi usado. |
| 409 | too_many_monitors, not_in_control | A chamada já tem todos os supervisores, ou outro supervisor a assumiu. |
| 422 | missing_provider_key | Falta a chave de um provedor que o agente usa: guarde-a em PUT /v1/account/ai-keys. |
| 422 | invalid_offer | O motor não conseguiu responder à oferta SDP. |
| 422 | invalid_provider_key | Um provedor recusou a chave enviada em PUT /v1/account/ai-keys. |
| 429 | agent_limit_reached, test_session_limit_reached | A conta já tem todas as sessões de agente, ou chamadas de teste, simultâneas que pode ter. |
| 502 | agent_config_unavailable | O seu produto não enviou a configuração a tempo, e o motor não tinha uma guardada. |
| 502 | tts_failed | O provedor de voz não gerou o áudio. |
| 502 | voices_failed | O provedor de voz não devolveu a lista de vozes (uma chave recusada, por exemplo). |
| 503 | agents_unavailable, agents_busy | O motor de agentes não respondeu ou está no limite de chamadas. meta.reason traz o motivo que o motor deu. |
Conta, plano e uso
{
"id": 42,
"name": "Sua Empresa",
"status": "active",
"trial_ends_at": null,
"partner": false,
"plan": "pro",
"subscription": { "status": "active", "currency": "brl", "renews_at": "2026-11-23T14:30:00Z", "ends_at": null },
"limits": { "numbers": 5, "channels": 10, "requests_per_minute": 600, "agent_sessions": 10, "agent_tests": 2 },
"webhook_retry_hours": 24
}
| Campo | Descrição |
|---|---|
status | trial (teste grátis), active (ativa), suspended (suspensa) ou canceled (assinatura encerrada). |
plan | start, pro, scale, ou vazio sem assinatura. |
subscription | A assinatura no Stripe: status (active, past_due quando um pagamento falhou, canceled…), a moeda e as datas. Vazio sem assinatura. |
limits | Quantos números a conta conecta, quantas chamadas simultâneas pode ter, quantas requisições por minuto faz na API e quantas sessões de agente de voz tem ao mesmo tempo (agent_sessions, das quais até agent_tests chamadas de teste). |
webhook_retry_hours | Por quantas horas um evento de mensagem que falhou é tentado de novo: 24, ou o prazo combinado com a Voza, até 168 (7 dias). |
partner | Uma conta parceira, de outro produto, com limites combinados com a Voza: as chamadas dela não são recusadas pelo limite de chamadas simultâneas, porque o produto limita as dos seus clientes. |
A assinatura se faz no painel (Plano e uso), com cartão, pelo Stripe. Quando ela termina, a API responde subscription_inactive até a conta assinar de novo.
{
"month": "2026-10",
"totals": { "messages_sent": 1840, "messages_received": 2311, "calls": 96, "call_seconds": 20412, "agent_calls": 40, "agent_seconds": 7310 },
"numbers": [
{ "number_id": "01jbk3…", "name": "Loja Centro", "messages_sent": 1200, "messages_received": 1500, "calls": 60, "call_seconds": 14000, "agent_calls": 40, "agent_seconds": 7310 }
]
}
O uso do mês em UTC, no total e por número (number_id vazio para um número já excluído). Sem month, o mês atual. agent_calls e agent_seconds são as chamadas que um agente de voz atendeu e os segundos da IA nelas (sem o tempo em que um supervisor estava com a chamada).
OpenAPI e Postman
A API inteira está descrita em OpenAPI 3.1: https://www.vozaapi.com/docs/openapi.json?lang=pt-BR. Importe-a no Postman ou no Insomnia, ou gere um cliente na sua linguagem.
Os textos da especificação vêm no idioma de ?lang=: pt-BR, en ou es.
Status da Voza
O status da Voza ao vivo (site e API, banco de dados, webhooks e e-mails, novas tentativas, os números por QR code e os agentes de voz) está em https://www.vozaapi.com/status. Para um monitor de disponibilidade, o mesmo em JSON: https://www.vozaapi.com/status.json, que responde 503 quando algum serviço está fora ou atrasado.
{
"status": "operational",
"checked_at": "2026-10-27T15:00:00Z",
"components": [{ "key": "engine", "name": "Números por QR code", "status": "operational", "message": null }]
}
status é operational, degraded ou down. Os números oficiais dependem também da API da Meta, que esta página não acompanha.
Novidades
- 29/10/2026: os eventos dos agentes saem numa fila só deles, em ordem por chamada, e nunca atrasam os eventos de chamada;
call.agent.declinedeend_detaildizem por que o agente recusou uma chamada, e oterminatedela trazvoza_reason: agent_unavailableevoza_detail;call.agent.erroreerror_messagequando um provedor falha no meio da chamada (provider_error);runtime_lostquando o motor de agentes cai;max_duration_secondsconta só o tempo da IA, ecall.agent.endedtrazai_secondsesupervised_seconds; limites de sessões de agente e de chamadas de teste por conta (limits.agent_sessionselimits.agent_tests,429 agent_limit_reached); remover o agente ou o número, ou bloquear a conta, encerra as chamadas do agente em curso; as chaves de IA são conferidas no provedor; o fim de uma chamada é tentado depois dos 2 minutos; um token recusado num envio, numa ação de chamada ou na verificação de hora em hora leva o número oficial ameta_refused. - 29/10/2026:
POST /v1/ttsnão guarda o texto (osaydas ferramentas também não); o agente não atende as chamadas de uma conta bloqueada; o agente aceita a chamada até 4 s depois dopre_acceptmesmo sem a mídia conectada, e a saudação espera a mídia. - 28/10/2026:
"calling": "keep"ao conectar um número oficial;{id}e{external_ref}no webhook de um número novo;meta_timeout, separado demeta_unavailable, eIdempotency-Keynas ações de chamada;timestamp_msnos eventos do agente;X-Voza-Timestampna assinatura dos pedidos do motor; o produto pode recusar uma chamada (agent_unavailable); o tempo limite dohandoff;stt.language: multi;GET /v1/voices. - 28/10/2026: o telefone web supervisiona as chamadas atendidas pela IA:
monitor(callId, {token})ouve, e assumir, devolver e sussurrar passam pelo seu servidor; o estadoagent(“Atendida pela IA”),answeredBye o motivoanswered_by_agentnas chamadas do SDK; o botão Ouvir e a supervisão no<voza-phone>. - 28/10/2026: agentes de voz:
PUT /v1/numbers/{id}/agent(o agente de um número, emalwaysoufallback), entregar uma chamada ao agente, supervisão (ouvir, sussurrar, assumir e devolver), chamadas de teste,POST /v1/tts,PUT /v1/account/ai-keys, os eventoscall.agent.*,call.transcript,call.metricsecall.supervision,answered_byno registro de chamadas eagent_callseagent_secondsno uso. Uma chamada atendida com a chave de API também fica com quem atendeu primeiro. - 28/10/2026:
GET /v1/numbers/{id}/templates(os modelos de um número oficial);external_refnos números, nos webhooks (metadata.external_ref) e emPATCH /v1/numbers/{id}; o segredo do webhook na resposta dePOST /v1/numbers; idempotência guardada pela Voza também nos números por QR code;limits.requests_per_minuteewebhook_retry_hoursemGET /v1/account. - 28/10/2026: o erro da Meta em
error.meta(e o motivo do motor emerror.meta.reason); oserrorsda Meta nos webhooks; eventos de chamada numa fila própria, com novas tentativas em segundos; a chamada recusada pelo limite chega ao webhook;GETePUT /v1/numbers/{id}/calling;GET /v1/numbers/{id}/call-permissions/{wa_id};status_reasonnos números. - 23/10/2026: a API em português, inglês e espanhol: as mensagens de erro seguem
Accept-Languageou?lang=(sem eles, o idioma de quem criou a conta), a especificação OpenAPI vem no idioma pedido e o<voza-phone>ganhou o atributolang. - 23/10/2026: números oficiais da Meta pela mesma API (modelos, mensagens interativas, chamadas);
GET /v1/account/usage; plano e assinatura emGET /v1/account; especificação OpenAPI. - 22/10/2026: primeira versão: números por QR code, mensagens, mídias, webhooks, chamadas e telefone web.
Mudanças que possam quebrar a sua integração são avisadas com antecedência e entram numa nova versão do endereço (/v2); a /v1 só ganha campos e eventos novos.