{"openapi":"3.1.0","info":{"title":"Luna: API de mensageria WhatsApp","version":"1.0.0","description":"A Luna entrega WhatsApp para empresas de software que precisam conversar com os\nclientes delas. Ela é construída sobre a **WhatsApp Cloud API oficial da Meta**, e a\nHelsen opera como Tech Provider aprovado. Nenhuma biblioteca não-oficial participa\ndo caminho, em nenhuma circunstância.\n\n[Read this documentation in English](/docs/en/)\n\n## O que esta API faz por você\n\nConectar o WhatsApp de um cliente final sem que ele saia do seu produto, receber tudo\nque chega nesse número, e enviar mensagem de volta. A inteligência da conversa é sua;\no que a Luna entrega é a infraestrutura de mensageria embaixo dela.\n\n## Autenticação\n\nToda rota exige uma chave de API no cabeçalho `Authorization`, no formato\n`Bearer hlsn_...`. A chave carrega **escopos**: cada rota exige o seu, e uma chave sem\no escopo devido recebe `403`. Crie e revogue chaves pelo console, em Integração → Chaves.\n\nA chave é mostrada **uma única vez**, no momento em que é criada. Não há rota que a\ndevolva depois: guardamos apenas um resumo criptográfico dela.\n\n## O envio é assíncrono, e isso é contrato\n\nEnviar uma mensagem devolve `202`, não `200`. A Luna aceita, enfileira e entrega à Meta\nrespeitando o ritmo que ela permite. Isso existe porque o limite que mais aperta é de\n**uma mensagem a cada seis segundos para o mesmo destinatário**. Um agente que responde\nem rajada dispara esse limite sozinho, e a resposta certa a isso é fila, nunca um erro\ndevolvido a você.\n\n## Erros\n\nTodo erro devolve um corpo JSON com `error` e `message`. `404` é usado também para\nrecurso que existe mas não é seu, porque a API não confirma a existência de recurso de outro\ncliente, nem pelo código de status.\n\n## Template aprovado não é template liberado\n\nA Meta aplica um ritmo próprio de liberação sobre os primeiros envios de um template\nrecém-aprovado. Ela chama isso de template pacing, e o define como um processo que dá\ntempo aos usuários do WhatsApp de reagirem à mensagem: se as primeiras entregas forem\nmal recebidas, o template é pausado antes de alcançar muita gente.\n\n**A Meta não publica prazo nem mecânica**, e a Luna não inventa nenhum. Não existe\ncampo nesta API dizendo quando o ritmo termina, e não existe estado dizendo que ele\nestá em curso, porque essa informação não é emitida por quem a controla. Procurar um\naqui é procurar o que não pode existir.\n\nA consequência prática é uma só: **escalone o primeiro disparo** em vez de programá-lo\npara uma data. Comece por um volume pequeno e cresça conforme as entregas confirmarem,\nacompanhando o estado do template — ele vai para `PAUSED` se a reação for negativa.\n\n## Limites\n\nExiste limite de requisições por chave de API. Ele é técnico, idêntico para todos os\nclientes, e documentado. Nenhum limite desta plataforma varia por preço.","contact":{"name":"Suporte Luna","email":"ia.helsenservice@gmail.com"}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Chave de API do tenant"}},"schemas":{}},"paths":{"/healthz":{"get":{"summary":"Verificar se a API está no ar","tags":["Serviço"],"description":"Responde 200 quando o processo está de pé. Não exige autenticação e não consulta banco nem fila, porque é sonda de processo e não de dependência. Um 200 aqui não promete que o envio de mensagem está funcionando.","responses":{"200":{"description":"Default Response"}}}},"/v1/api-keys":{"get":{"summary":"Listar chaves","tags":["Chaves de API"],"description":"As chaves deste cliente, com escopos, data de criação, último uso e revogação.\n\nO segredo nunca volta aqui, nem em rota nenhuma. A Luna guarda apenas um resumo criptográfico dele. Se você perdeu a chave, revogue e crie outra.\n\nUse kind=integration para ver só as chaves de integração e esconder as sessões do console.","parameters":[{"schema":{"type":"string","enum":["integration","session","elevation"]},"in":"query","name":"kind","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O nome que você deu à chave, para reconhecê-la na lista."},"kind":{"type":"string","enum":["integration","session","elevation"],"description":"integration para chave sua, session para uma sessão do console. elevation é valor histórico, de credenciais emitidas até 10/08/2026, e nenhuma chave nova nasce com ele."},"scopes":{"type":"array","items":{"type":"string"},"description":"O que esta chave pode fazer. Uma rota fora deste conjunto responde 403."},"createdAt":{"type":"string"},"expiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Nulo em chave de integração: ela vale até ser revogada."},"revokedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastUsedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O último uso registrado, para identificar chave esquecida."}},"required":["id","label","kind","scopes","createdAt","expiresAt","revokedAt","lastUsedAt"],"additionalProperties":false,"example":{"id":"7d1f4c02-9a3b-4e51-8c7d-2f0b6a91e334","label":"integração de produção","kind":"integration","scopes":["messages:write","messages:read","numbers:read"],"createdAt":"2026-08-07T12:10:44.201Z","expiresAt":null,"revokedAt":null,"lastUsedAt":"2026-08-10T09:02:18.664Z"}}}}}}}},"post":{"summary":"Criar uma chave","tags":["Chaves de API"],"description":"Cunha uma chave nova e devolve o segredo uma única vez. Guarde-o no momento em que recebe, porque não há rota que o devolva depois.\n\nDê a ela só os escopos de que ela precisa. Uma chave que só envia mensagem não deveria poder conectar número nem cunhar outras chaves.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"scopes":{"minItems":1,"type":"array","items":{"type":"string","enum":["keys:read","keys:write","numbers:read","numbers:write","messages:read","messages:write","templates:read","templates:write","onboarding:write","webhooks:read","webhooks:write","session:end","session:elevate"]}},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["scopes"]}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O nome que você deu à chave, para reconhecê-la na lista."},"kind":{"type":"string","enum":["integration","session","elevation"],"description":"integration para chave sua, session para uma sessão do console. elevation é valor histórico, de credenciais emitidas até 10/08/2026, e nenhuma chave nova nasce com ele."},"scopes":{"type":"array","items":{"type":"string"},"description":"O que esta chave pode fazer. Uma rota fora deste conjunto responde 403."},"createdAt":{"type":"string"},"expiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Nulo em chave de integração: ela vale até ser revogada."},"revokedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastUsedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O último uso registrado, para identificar chave esquecida."},"secret":{"type":"string"}},"required":["id","label","kind","scopes","createdAt","expiresAt","revokedAt","lastUsedAt","secret"],"additionalProperties":false}}}}}}},"/v1/api-keys/{id}":{"get":{"summary":"Consultar uma chave","tags":["Chaves de API"],"description":"Os metadados de uma chave. O segredo não volta, pelo mesmo motivo descrito na listagem.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O nome que você deu à chave, para reconhecê-la na lista."},"kind":{"type":"string","enum":["integration","session","elevation"],"description":"integration para chave sua, session para uma sessão do console. elevation é valor histórico, de credenciais emitidas até 10/08/2026, e nenhuma chave nova nasce com ele."},"scopes":{"type":"array","items":{"type":"string"},"description":"O que esta chave pode fazer. Uma rota fora deste conjunto responde 403."},"createdAt":{"type":"string"},"expiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Nulo em chave de integração: ela vale até ser revogada."},"revokedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastUsedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O último uso registrado, para identificar chave esquecida."}},"required":["id","label","kind","scopes","createdAt","expiresAt","revokedAt","lastUsedAt"],"additionalProperties":false,"example":{"id":"7d1f4c02-9a3b-4e51-8c7d-2f0b6a91e334","label":"integração de produção","kind":"integration","scopes":["messages:write","messages:read","numbers:read"],"createdAt":"2026-08-07T12:10:44.201Z","expiresAt":null,"revokedAt":null,"lastUsedAt":"2026-08-10T09:02:18.664Z"}}}}}}}},"/v1/api-keys/{id}/revoke":{"post":{"summary":"Revogar uma chave","tags":["Chaves de API"],"description":"Invalida a chave imediatamente. A operação é idempotente, então revogar de novo devolve o mesmo resultado, sem erro.\n\nRevogação não apaga a linha. A data fica registrada, para que uma auditoria consiga responder quando o acesso terminou.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O nome que você deu à chave, para reconhecê-la na lista."},"kind":{"type":"string","enum":["integration","session","elevation"],"description":"integration para chave sua, session para uma sessão do console. elevation é valor histórico, de credenciais emitidas até 10/08/2026, e nenhuma chave nova nasce com ele."},"scopes":{"type":"array","items":{"type":"string"},"description":"O que esta chave pode fazer. Uma rota fora deste conjunto responde 403."},"createdAt":{"type":"string"},"expiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Nulo em chave de integração: ela vale até ser revogada."},"revokedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"lastUsedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O último uso registrado, para identificar chave esquecida."}},"required":["id","label","kind","scopes","createdAt","expiresAt","revokedAt","lastUsedAt"],"additionalProperties":false,"example":{"id":"7d1f4c02-9a3b-4e51-8c7d-2f0b6a91e334","label":"integração de produção","kind":"integration","scopes":["messages:write","messages:read","numbers:read"],"createdAt":"2026-08-07T12:10:44.201Z","expiresAt":null,"revokedAt":null,"lastUsedAt":"2026-08-10T09:02:18.664Z"}}}}}}}},"/v1/subscription":{"get":{"summary":"Consultar a assinatura de um período","tags":["Assinatura"],"description":"O que é cobrado pela Helsen no período: a capacidade de números ativos, e nada além disso.\n\nA única grandeza faturável é a quantidade de números que este cliente manteve ativos no período. Nenhum evento da plataforma altera este valor: mensagem, conversa, template, mídia e sessão não entram nesta conta, por caminho nenhum.\n\nO uso da plataforma WhatsApp é cobrado pela Meta, diretamente do negócio final, no método de pagamento que ele cadastra na conta dele. A Helsen não intermedeia esse pagamento, não o adianta e não o repassa.\n\nAtivo significa capacidade configurada, e não apto a enviar agora. Um número aguardando o pagamento do cliente na Meta continua sendo capacidade configurada e continua contando. Um número ativo por três dias conta igual a um ativo o mês inteiro: não há fração de período.\n\nO período é meio-aberto: o início entra, o fim não. Ele é obrigatório e não tem valor padrão, para que duas chamadas iguais devolvam sempre a mesma resposta.\n\nA lista de identificadores traz os números que contaram, para que a contagem seja conferível.","parameters":[{"schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","example":"2026-03-01T00:00:00.000Z"},"in":"query","name":"inicio","required":true},{"schema":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$","example":"2026-04-01T00:00:00.000Z"},"in":"query","name":"fim","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"periodo":{"type":"object","properties":{"inicio":{"type":"string","example":"2026-03-01T00:00:00.000Z"},"fim":{"type":"string","example":"2026-04-01T00:00:00.000Z"}},"required":["inicio","fim"],"additionalProperties":false},"numerosAtivos":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"example":3},"phoneNumberIds":{"type":"array","items":{"type":"string"},"example":["15550001111","15550002222"]}},"required":["periodo","numerosAtivos","phoneNumberIds"],"additionalProperties":false}}}}}}},"/v1/messages":{"post":{"summary":"Enviar mensagem","tags":["Mensagens"],"description":"Aceita a mensagem, devolve 202 com o identificador, e entrega à Meta de forma assíncrona.\n\nPor que 202 e não 200: o limite que mais aperta na plataforma da Meta é de uma mensagem a cada seis segundos para o mesmo destinatário. Um agente que responde em rajada dispara isso sozinho. Enfileirar é o produto, e devolver 429 seria transferir o problema para você com outro nome.\n\nfrom é o identificador do número na Meta, o mesmo metaPhoneNumberId que a listagem de números devolve. to é o telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais.\n\nMande o cabeçalho Idempotency-Key se você retenta. Com ele, repetir o pedido não produz segundo envio.\n\nA janela de atendimento é aplicada aqui: fora dela, mensagem livre é recusada com 409 antes de qualquer ida à Meta, porque uma chamada recusada por lá é gasta à toa e conta como erro contra o seu número. A resposta diz quando a janela fechou e o que fazer. Template e reação passam mesmo com a janela fechada, e enviar um template é justamente o que a reabre. Consulte GET /v1/conversations para saber o estado da janela antes de tentar.\n\nAo enviar um template com cabeçalho de mídia, a mídia vai no componente de cabeçalho do envio, e cada mensagem fornece a sua. O handle usado na criação do template é apenas o exemplo que a Meta revisa: ele não vira o conteúdo entregue, e não há nada a reaproveitar dele aqui. Para escolher entre link e identificador de mídia, vale a mesma recomendação publicada em POST /v1/numbers/{id}/media.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"default":"text","description":"Sempre text. Pode ser omitido: um corpo sem type é interpretado como texto, para não quebrar quem integrou antes dos demais tipos existirem.","type":"string","enum":["text"]},"text":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":4096,"description":"O texto da mensagem. Limite de 4096 caracteres, o mesmo da Cloud API."},"preview_url":{"description":"Se a Meta deve buscar a primeira URL do texto para montar uma prévia. Desligado por padrão: ligado, ele dispara uma requisição de saída a partir de conteúdo que o negócio final escreveu, com latência e comportamento que não são da plataforma.","type":"boolean"}},"required":["body"]}},"required":["from","to","text"],"example":{"from":"1266743959851219","to":"5531981036436","text":{"body":"Olá! Recebemos seu pedido e já estamos preparando."}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre image.","enum":["image"]},"image":{"type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"caption":{"description":"A legenda que acompanha o arquivo. Limite de 1024 caracteres, o mesmo da Cloud API.","type":"string","maxLength":1024}}}},"required":["from","to","type","image"],"example":{"from":"1266743959851219","to":"5531981036436","type":"image","image":{"link":"https://exemplo.com.br/pedido.jpg","caption":"Seu pedido saiu para entrega."}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre video.","enum":["video"]},"video":{"type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"caption":{"description":"A legenda que acompanha o arquivo. Limite de 1024 caracteres, o mesmo da Cloud API.","type":"string","maxLength":1024}}}},"required":["from","to","type","video"],"example":{"from":"1266743959851219","to":"5531981036436","type":"video","video":{"id":"1425835918205946"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre audio.","enum":["audio"]},"audio":{"type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}}},"required":["from","to","type","audio"],"example":{"from":"1266743959851219","to":"5531981036436","type":"audio","audio":{"id":"1425835918205947"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre document.","enum":["document"]},"document":{"type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"caption":{"description":"A legenda que acompanha o arquivo. Limite de 1024 caracteres, o mesmo da Cloud API.","type":"string","maxLength":1024},"filename":{"description":"O nome com que o arquivo aparece para quem recebe. Sem ele, quem recebe vê o nome que a Meta derivar da origem.","type":"string","minLength":1,"maxLength":240}}}},"required":["from","to","type","document"],"example":{"from":"1266743959851219","to":"5531981036436","type":"document","document":{"link":"https://exemplo.com.br/nota.pdf","filename":"nota-fiscal.pdf"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre sticker.","enum":["sticker"]},"sticker":{"type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}}},"required":["from","to","type","sticker"],"example":{"from":"1266743959851219","to":"5531981036436","type":"sticker","sticker":{"id":"1425835918205948"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre location.","enum":["location"]},"location":{"type":"object","properties":{"latitude":{"type":"number","minimum":-90,"maximum":90,"description":"A latitude em graus decimais, entre -90 e 90."},"longitude":{"type":"number","minimum":-180,"maximum":180,"description":"A longitude em graus decimais, entre -180 e 180."},"name":{"description":"O nome do lugar, exibido acima do endereço.","type":"string","maxLength":1000},"address":{"description":"O endereço do lugar, exibido abaixo do nome.","type":"string","maxLength":1000}},"required":["latitude","longitude"]}},"required":["from","to","type","location"],"example":{"from":"1266743959851219","to":"5531981036436","type":"location","location":{"latitude":-19.9227,"longitude":-43.9451,"name":"Praça da Liberdade","address":"Belo Horizonte, MG"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre contacts.","enum":["contacts"]},"contacts":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"object","properties":{"formatted_name":{"type":"string","minLength":1,"maxLength":512,"description":"O nome como ele aparece no cartão."},"first_name":{"description":"O primeiro nome.","type":"string","maxLength":256},"last_name":{"description":"O sobrenome.","type":"string","maxLength":256},"middle_name":{"description":"O nome do meio.","type":"string","maxLength":256},"prefix":{"description":"O prefixo, como Dr. ou Sra.","type":"string","maxLength":64},"suffix":{"description":"O sufixo, como Jr. ou Neto.","type":"string","maxLength":64}},"required":["formatted_name"]},"birthday":{"description":"A data de nascimento em AAAA-MM-DD.","type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"org":{"description":"Os dados profissionais do contato.","type":"object","properties":{"company":{"description":"A empresa.","type":"string","maxLength":256},"department":{"description":"O departamento.","type":"string","maxLength":256},"title":{"description":"O cargo.","type":"string","maxLength":256}}},"phones":{"description":"Os telefones do contato.","maxItems":20,"type":"array","items":{"type":"object","properties":{"phone":{"description":"O telefone, como ele deve aparecer.","type":"string","maxLength":64},"type":{"description":"O rótulo do telefone, como CELL ou WORK.","type":"string","maxLength":64},"wa_id":{"description":"O identificador do contato no WhatsApp, se conhecido.","type":"string","maxLength":64}}}},"emails":{"description":"Os e-mails do contato.","maxItems":20,"type":"array","items":{"type":"object","properties":{"email":{"description":"O endereço de e-mail.","type":"string","maxLength":320},"type":{"description":"O rótulo do e-mail, como HOME ou WORK.","type":"string","maxLength":64}}}},"addresses":{"description":"Os endereços do contato.","maxItems":20,"type":"array","items":{"type":"object","properties":{"street":{"description":"O logradouro e o número.","type":"string","maxLength":512},"city":{"description":"A cidade.","type":"string","maxLength":256},"state":{"description":"O estado.","type":"string","maxLength":256},"zip":{"description":"O CEP.","type":"string","maxLength":64},"country":{"description":"O país.","type":"string","maxLength":256},"country_code":{"description":"O código do país em duas letras.","type":"string","maxLength":8},"type":{"description":"O rótulo do endereço, como HOME ou WORK.","type":"string","maxLength":64}}}},"urls":{"description":"As páginas do contato.","maxItems":20,"type":"array","items":{"type":"object","properties":{"url":{"description":"O endereço na web.","type":"string","maxLength":2048},"type":{"description":"O rótulo da URL, como HOME ou WORK.","type":"string","maxLength":64}}}}},"required":["name"]},"description":"Os cartões de contato a enviar. Ao menos um, no máximo vinte."}},"required":["from","to","type","contacts"],"example":{"from":"1266743959851219","to":"5531981036436","type":"contacts","contacts":[{"name":{"formatted_name":"Ana Souza","first_name":"Ana","last_name":"Souza"},"phones":[{"phone":"+55 31 98103-6436","type":"CELL"}]}]}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre reaction.","enum":["reaction"]},"reaction":{"type":"object","properties":{"message_id":{"type":"string","minLength":1,"maxLength":256,"description":"O identificador da mensagem que recebe a reação, o wamid que a Meta atribuiu."},"emoji":{"type":"string","maxLength":16,"description":"O emoji da reação. Mande vazio para remover uma reação que já tinha sido posta."}},"required":["message_id","emoji"]}},"required":["from","to","type","reaction"],"example":{"from":"1266743959851219","to":"5531981036436","type":"reaction","reaction":{"message_id":"wamid.HBgNNTUzMTk4MTAzNjQzNhUCABIYFjNBMEE=","emoji":"👍"}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre interactive.","enum":["interactive"]},"interactive":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","description":"Sempre button.","enum":["button"]},"header":{"description":"O cabeçalho, exibido acima do corpo.","type":"object","properties":{"type":{"type":"string","enum":["text","image","video","document"],"description":"O que o cabeçalho carrega."},"text":{"description":"O texto do cabeçalho, quando type é text.","type":"string","maxLength":60},"image":{"description":"A imagem do cabeçalho, quando type é image.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"video":{"description":"O vídeo do cabeçalho, quando type é video.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"document":{"description":"O documento do cabeçalho, quando type é document.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"filename":{"type":"string","maxLength":240}}}},"required":["type"]},"body":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":1024,"description":"O texto do corpo."}},"required":["text"],"description":"O corpo da mensagem interativa."},"footer":{"description":"O rodapé, exibido abaixo dos botões.","type":"object","properties":{"text":{"type":"string","maxLength":60,"description":"O texto do rodapé."}},"required":["text"]},"action":{"type":"object","properties":{"buttons":{"minItems":1,"maxItems":3,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Sempre reply.","enum":["reply"]},"reply":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":256,"description":"O identificador que volta no webhook quando o botão é tocado."},"title":{"type":"string","minLength":1,"maxLength":20,"description":"O rótulo do botão. Limite de 20 caracteres."}},"required":["id","title"]}},"required":["type","reply"]},"description":"Os botões de resposta. De um a três, o teto da Cloud API."}},"required":["buttons"]}},"required":["type","body","action"]},{"type":"object","properties":{"type":{"type":"string","description":"Sempre list.","enum":["list"]},"header":{"description":"O cabeçalho, exibido acima do corpo.","type":"object","properties":{"type":{"type":"string","enum":["text","image","video","document"],"description":"O que o cabeçalho carrega."},"text":{"description":"O texto do cabeçalho, quando type é text.","type":"string","maxLength":60},"image":{"description":"A imagem do cabeçalho, quando type é image.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"video":{"description":"O vídeo do cabeçalho, quando type é video.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"document":{"description":"O documento do cabeçalho, quando type é document.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"filename":{"type":"string","maxLength":240}}}},"required":["type"]},"body":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":1024,"description":"O texto do corpo."}},"required":["text"],"description":"O corpo da mensagem interativa."},"footer":{"description":"O rodapé, exibido abaixo dos botões.","type":"object","properties":{"text":{"type":"string","maxLength":60,"description":"O texto do rodapé."}},"required":["text"]},"action":{"type":"object","properties":{"button":{"type":"string","minLength":1,"maxLength":20,"description":"O rótulo do botão que abre a lista. Limite de 20 caracteres."},"sections":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"title":{"description":"O título da seção.","type":"string","maxLength":24},"rows":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"maxLength":200,"description":"O identificador que volta no webhook quando a linha é escolhida."},"title":{"type":"string","minLength":1,"maxLength":24,"description":"O título da linha."},"description":{"description":"A descrição da linha.","type":"string","maxLength":72}},"required":["id","title"]},"description":"As linhas da seção. De uma a dez."}},"required":["rows"]},"description":"As seções da lista. De uma a dez, o teto da Cloud API."}},"required":["button","sections"]}},"required":["type","body","action"]},{"type":"object","properties":{"type":{"type":"string","description":"Sempre cta_url.","enum":["cta_url"]},"header":{"description":"O cabeçalho, exibido acima do corpo.","type":"object","properties":{"type":{"type":"string","enum":["text","image","video","document"],"description":"O que o cabeçalho carrega."},"text":{"description":"O texto do cabeçalho, quando type é text.","type":"string","maxLength":60},"image":{"description":"A imagem do cabeçalho, quando type é image.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"video":{"description":"O vídeo do cabeçalho, quando type é video.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"}}},"document":{"description":"O documento do cabeçalho, quando type é document.","type":"object","properties":{"id":{"description":"O identificador de um arquivo já enviado à Meta. Use este OU link, nunca os dois. A Meta retém o arquivo enviado por 30 dias.","type":"string","minLength":1,"maxLength":128},"link":{"description":"A URL pública do arquivo, que a Meta baixa no momento do envio. Use esta OU id, nunca os dois. Ela precisa responder sem autenticação.","type":"string","maxLength":2048,"format":"uri"},"filename":{"type":"string","maxLength":240}}}},"required":["type"]},"body":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":1024,"description":"O texto do corpo."}},"required":["text"],"description":"O corpo da mensagem interativa."},"footer":{"description":"O rodapé, exibido abaixo dos botões.","type":"object","properties":{"text":{"type":"string","maxLength":60,"description":"O texto do rodapé."}},"required":["text"]},"action":{"type":"object","properties":{"name":{"default":"cta_url","description":"Sempre cta_url.","type":"string","enum":["cta_url"]},"parameters":{"type":"object","properties":{"display_text":{"type":"string","minLength":1,"maxLength":20,"description":"O rótulo do botão. Limite de 20 caracteres."},"url":{"type":"string","maxLength":2048,"format":"uri","description":"O endereço que o botão abre."}},"required":["display_text","url"]}},"required":["parameters"]}},"required":["type","body","action"]}],"description":"A mensagem interativa: botões de resposta, lista de opções, ou botão com URL."}},"required":["from","to","type","interactive"],"example":{"from":"1266743959851219","to":"5531981036436","type":"interactive","interactive":{"type":"button","body":{"text":"Podemos confirmar a entrega para amanhã?"},"action":{"buttons":[{"type":"reply","reply":{"id":"confirma","title":"Confirmar"}},{"type":"reply","reply":{"id":"remarca","title":"Remarcar"}}]}}}},{"type":"object","properties":{"from":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"O identificador do número na Meta, o mesmo metaPhoneNumberId que GET /v1/numbers devolve."},"to":{"type":"string","pattern":"^[1-9]\\d{7,14}$","description":"O telefone do destinatário em formato internacional, só dígitos, sem o sinal de mais."},"type":{"type":"string","description":"Sempre template.","enum":["template"]},"template":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":512,"description":"O nome do template já aprovado pela Meta."},"language":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":16,"description":"O código do idioma do template, como pt_BR. Ele precisa bater com o aprovado."}},"required":["code"]},"components":{"description":"Os componentes que preenchem as variáveis do template.","maxItems":32,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["header","body","button"],"description":"Qual parte do template este componente preenche."},"sub_type":{"description":"O subtipo, exigido pela Cloud API quando type é button.","type":"string","maxLength":64},"index":{"description":"A posição do botão, exigida pela Cloud API quando type é button.","type":"string","maxLength":8},"parameters":{"description":"Os valores das variáveis desta parte, na forma que a Cloud API define.","maxItems":64,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["type"]}}},"required":["name","language"]}},"required":["from","to","type","template"],"example":{"from":"1266743959851219","to":"5531981036436","type":"template","template":{"name":"confirmacao_de_pedido","language":{"code":"pt_BR"},"components":[{"type":"body","parameters":[{"type":"text","parameter_name":"nome","text":"Ana"}]}]}}}],"description":"O pedido de envio de mensagem. O campo type diz qual é o tipo; um corpo sem type é interpretado como texto."}}},"description":"O pedido de envio de mensagem. O campo type diz qual é o tipo; um corpo sem type é interpretado como texto."},"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"O identificador da mensagem na Luna. Ele existe antes de a Meta responder."},"status":{"type":"string","description":"Sempre queued: a mensagem foi aceita e está na fila de saída.","enum":["queued"]}},"required":["id","status"],"additionalProperties":false,"example":{"id":"ae60f288-7565-4315-a440-3663e9c231e1","status":"queued"}}}}}}},"get":{"summary":"Listar mensagens","tags":["Mensagens"],"description":"O registro do que entrou e saiu, do mais recente para o mais antigo.\n\nFiltre por número com phoneNumberId, que é o identificador interno da Luna e não o da Meta. Filtre por interlocutor com conversationId, que é o id devolvido por GET /v1/conversations. Para percorrer páginas, mande de volta o nextCursor que veio na resposta anterior. Quando ele vem nulo, acabou.\n\nO cursor é opaco: ele carrega posição, não contagem. Não tente construir um à mão.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false},{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"query","name":"phoneNumberId","required":false},{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"query","name":"conversationId","required":false},{"schema":{"type":"string","maxLength":256},"in":"query","name":"cursor","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"direction":{"type":"string"},"wamid":{"anyOf":[{"type":"string"},{"type":"null"}]},"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"bodyText":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"anyOf":[{"type":"string"},{"type":"null"}]},"waId":{"anyOf":[{"type":"string"},{"type":"null"}]},"displayNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","direction","wamid","type","bodyText","status","waId","displayNumber","createdAt"],"additionalProperties":false}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Mande este valor de volta em cursor para pegar a próxima página. Nulo significa fim."}},"required":["data","nextCursor"],"additionalProperties":false,"example":{"data":[{"id":"ae60f288-7565-4315-a440-3663e9c231e1","direction":"outbound","wamid":"wamid.HBgMNTUzMTgwMzYwMjEwFQIAERgSQjczNEE1OEVDMDgyMTJCMjA2AA==","type":"text","bodyText":"Olá! Recebemos seu pedido e já estamos preparando.","status":"read","waId":"5531981036436","displayNumber":"+55 31 98103-6436","createdAt":"2026-08-07T12:24:02.085Z"},{"id":"b6d84033-a8e8-4db9-aab6-565ce203cc51","direction":"inbound","wamid":"wamid.HBgMNTUzMTgwMzYwMjEwFQIAEhgUM0FCMEFBNDUzNzg4NzY5MUQ5MTUA","type":"text","bodyText":"oi","status":null,"waId":"5531981036436","displayNumber":"+55 31 98103-6436","createdAt":"2026-08-07T11:35:10.000Z"}],"nextCursor":null}}}}}}}},"/v1/conversations":{"get":{"summary":"Listar conversas","tags":["Mensagens"],"description":"Uma linha por interlocutor por número, da mais ativa para a mais parada.\n\nA ordem é por atividade real e não por data de abertura: uma conversa aberta há meses e ativa hoje vem primeiro. lastMessage traz a mensagem mais recente daquela conversa, e ela é conteúdo, não resumo estatístico. Conversa que ainda não teve mensagem devolve lastMessage nulo e continua na lista.\n\nCada linha diz se a janela de atendimento está aberta. windowOpen verdadeiro significa que você pode mandar mensagem livre para aquele interlocutor agora; falso significa que o caminho é enviar um template, que reabre a conversa. windowExpiresAt diz quando ela fecha, e windowSource diz o que a abriu: usuario, template, anuncio ou desconhecido. Quando não há janela conhecida os três vêm nulos ou falsos, e nunca ausentes.\n\nNão sai contagem nenhuma daqui. Nem total, nem contador por interlocutor, nem agregado por período. O nextCursor carrega posição e não quantidade, e você não deve tentar construir um à mão.\n\nFiltre por número com phoneNumberId, que é o identificador interno da Luna e não o da Meta. Para percorrer páginas, mande de volta o nextCursor que veio na resposta anterior. Quando ele vem nulo, acabou.\n\nUma limitação que vale conhecer antes de integrar: o agrupamento é pelo waId, que é o identificador que a plataforma gravou para aquele interlocutor. Quando o identificador registrado pela Meta difere do que foi digitado no envio, o mesmo interlocutor humano aparece em duas conversas, com dois identificadores. A Luna devolve o que a plataforma gravou e não adivinha equivalência, porque adivinhar erraria nos dois sentidos.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false},{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"query","name":"phoneNumberId","required":false},{"schema":{"type":"string","maxLength":256},"in":"query","name":"cursor","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"waId":{"type":"string"},"phoneNumberId":{"type":"string"},"displayNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"},"lastMessage":{"anyOf":[{"type":"object","properties":{"direction":{"type":"string"},"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"bodyText":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["direction","type","bodyText","createdAt"],"additionalProperties":false},{"type":"null"}]},"windowOpen":{"type":"boolean","description":"Verdadeiro quando você pode mandar mensagem livre para este interlocutor agora. Falso significa que o caminho é enviar um template, que reabre a conversa."},"windowExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Instante em que a janela de atendimento fecha, em ISO-8601. Nulo quando não há janela conhecida, e nesse caso windowOpen é falso."},"windowSource":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O que abriu a janela: usuario quando o interlocutor escreveu, template quando um template entregue reabriu a conversa, anuncio quando ele veio de um anúncio ou botão de página, e desconhecido quando a origem não foi identificada. Nulo quando não há janela conhecida."}},"required":["id","waId","phoneNumberId","displayNumber","createdAt","lastMessage","windowOpen","windowExpiresAt","windowSource"],"additionalProperties":false}},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Mande este valor de volta em cursor para pegar a próxima página. Nulo significa fim."}},"required":["data","nextCursor"],"additionalProperties":false,"example":{"data":[{"id":"0f2c8e6a-1d3b-4a5c-9e7f-2b4d6a8c0e12","waId":"5531981036436","phoneNumberId":"7a1c9d3e-5f2b-4c8a-9d6e-1b3f5a7c9e11","displayNumber":"+55 31 98103-6436","createdAt":"2026-08-07T11:35:10.000Z","lastMessage":{"direction":"outbound","type":"text","bodyText":"Olá! Recebemos seu pedido e já estamos preparando.","createdAt":"2026-08-07T12:24:02.085Z"},"windowOpen":true,"windowExpiresAt":"2026-08-08T11:35:10.000Z","windowSource":"usuario"}],"nextCursor":null}}}}}}}},"/v1/messages/{id}/media":{"get":{"summary":"Obter a mídia de uma mensagem","tags":["Mensagens"],"description":"Devolve uma URL assinada, emitida pela Luna, para baixar a mídia que já foi persistida.\n\nA URL expira em poucos minutos e é uma credencial de portador: quem a tiver baixa o arquivo, sem apresentar chave nenhuma. Não a guarde, não a coloque em log e não a mande por canal que você não controla. Quando ela expirar, chame esta rota de novo.\n\nA URL nunca aponta para a Meta. Ela aponta para o armazenamento da Luna, no domínio da API de objetos do provedor, porque URL assinada não funciona com domínio customizado. Não espere um domínio da marca aqui.\n\nSe a mídia ainda não foi obtida, a resposta é 409 com o estado atual e sem URL: pendente e falhou voltam sozinhos, porque a varredura retenta dentro da janela de sete dias da Meta; expirado é definitivo, porque passada essa janela o identificador não resolve mais e não há segunda chave.\n\nMensagem que não é sua responde 404, igual a mensagem que não existe.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador da mensagem na Luna, como GET /v1/messages o devolve."}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL assinada para baixar o arquivo. É credencial de portador: quem a tiver baixa, sem apresentar chave. Não guarde, não registre em log. Ela aponta para o armazenamento da Luna e nunca para a Meta — e sai no domínio da API de objetos do provedor, porque URL assinada não funciona com domínio customizado."},"expiraEm":{"type":"string","description":"Instante em que a URL deixa de valer, em ISO-8601 UTC. Depois dele, chame a rota de novo."}},"required":["url","expiraEm"],"additionalProperties":false,"example":{"url":"https://c1b129d42065073c6f8b203801c44b2d.r2.cloudflarestorage.com/lunabucket/midia/ae60f288-7565-4315-a440-3663e9c231e1?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-Signature=EXEMPLO","expiraEm":"2026-08-24T12:29:02.085Z"}}}}}}}},"/v1/messages/{id}/read":{"post":{"summary":"Marcar uma mensagem como lida","tags":["Mensagens"],"description":"Marca na Meta uma mensagem que você recebeu como lida, e responde 200 quando ela aceitou.\n\nEsta rota é síncrona, ao contrário do envio. A razão é que confirmação de leitura perde valor com o tempo: a que chega meia hora depois já foi lida, do outro lado, como ausência de leitura. Enfileirá-la entregaria o sinal quando ele não significa mais nada.\n\nSó vale para mensagem recebida. Mensagem que você enviou, mensagem que não é sua e mensagem que não existe respondem 404 igual.\n\nEla é uma chamada à plataforma como qualquer outra, e é contada no mesmo ritmo do seu número que o envio usa.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador da mensagem na Luna, como GET /v1/messages o devolve."}],"responses":{"200":{"description":"A Meta aceitou a marcação.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false,"description":"A Meta aceitou a marcação."}}}}}}},"/v1/messages/{id}/typing":{"post":{"summary":"Mostrar o indicador de digitando","tags":["Mensagens"],"description":"Mostra ao destinatário que você está escrevendo, e responde 200 quando a Meta aceitou.\n\nEsta rota é síncrona, e é a que mais depende disso: um indicador de digitando enfileirado seria entregue depois de a presença ter acabado. Não é entrega atrasada: é uma afirmação falsa sobre o presente.\n\nSaiba de uma coisa antes de integrar: a plataforma da Meta não tem indicador isolado. O indicador é campo do mesmo corpo que marca a mensagem como lida, e exige o identificador de uma mensagem recebida para saber em qual conversa exibir. Então chamar esta rota TAMBÉM marca aquela mensagem como lida. Não há como fazer uma coisa sem a outra.\n\nO indicador some sozinho quando você responde, com teto de vinte e cinco segundos do lado da Meta. Não existe rota para desligá-lo, e não é preciso.\n\nEla é contada no mesmo ritmo do seu número que o envio usa.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador da mensagem na Luna, como GET /v1/messages o devolve."}],"responses":{"200":{"description":"A Meta aceitou o indicador.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false,"description":"A Meta aceitou o indicador."}}}}}}},"/v1/numbers/{id}/media":{"post":{"summary":"Subir um arquivo e obter o identificador de mídia","tags":["Mensagens"],"description":"Recebe um arquivo em multipart/form-data no campo file, sobe para a Meta e devolve o identificador dele.\n\nO id na URL é o identificador do número na Luna, o mesmo que GET /v1/numbers devolve, e não o identificador dele na Meta.\n\nUse o identificador no lugar de link ao enviar mídia: com ele a Meta não precisa baixar nada da sua infraestrutura, o envio fica mais rápido e você pode reaproveitar o mesmo arquivo em várias mensagens.\n\nO identificador vale trinta dias. Depois disso, suba o arquivo de novo.\n\nA resposta não traz URL, e isso é decisão e não omissão: a URL de download da Meta expira em cinco minutos, e uma URL guardada é um valor que já nasce errado, e o retry que a usasse falharia sempre, muito depois de a causa ter passado.\n\nArquivo acima do teto é recusado com 413 antes de qualquer ida à Meta, para não gastar chamada sua. O teto é o mesmo para todos os clientes: ele existe por capacidade de infraestrutura, e não varia.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador do número na Luna, como GET /v1/numbers o devolve."}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"mediaId":{"type":"string","description":"O identificador do arquivo na Meta. Use-o no lugar de link ao enviar mídia: a Meta não precisa baixar nada da sua infraestrutura, e você reaproveita o mesmo arquivo em várias mensagens. Ele vale 30 dias; depois disso, suba o arquivo de novo."}},"required":["mediaId"],"additionalProperties":false,"example":{"mediaId":"1425835918205946"}}}}}}}},"/v1/messages/{id}":{"get":{"summary":"Consultar o estado de uma mensagem","tags":["Mensagens"],"description":"Devolve o estado atual da mensagem, os instantes de cada etapa e o que a Meta disse quando recusou alguma coisa.\n\nO campo erros traz a lista do que deu errado, em ordem de acontecimento. Cada item tem a explicação em português e a ação recomendada, e traz junto o campo original: o corpo de erro da Meta sem reescrita e sem filtro, que é o que o suporte dela reconhece. O fbtrace_id dentro dele é o que faz um chamado avançar quando a nossa explicação não bastar.\n\nAtenção ao repassar a resposta: o campo original pode conter o telefone do consumidor final e trechos da conversa. Trate-o com o mesmo cuidado do restante do conteúdo das mensagens antes de mandá-lo a terceiros.\n\nA lista reúne o que a Meta recusou na hora do envio e o que ela informou depois, por evento de status. Você não precisa saber de qual das duas veio cada item: em ambos os casos ela recusou algo, e a ação recomendada é a que resolve.\n\nA resposta não traz contagem de tentativas, custo nem qualquer número de consumo. Para o histórico completo das suas mensagens, use GET /v1/messages.\n\nMensagem que não é sua responde 404, igual a mensagem que não existe.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador da mensagem na Luna, como GET /v1/messages o devolve."}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$","description":"O identificador da mensagem na Luna."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O estado atual da entrega, como a Meta o reportou por último. Nulo quando nenhum evento de status chegou ainda."},"criadaEm":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando a Luna aceitou a mensagem, em ISO-8601 UTC."},"enviadaEm":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando a Meta confirmou o envio. Nulo enquanto isso não aconteceu."},"entregueEm":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando a mensagem chegou ao aparelho do destinatário. Nulo enquanto isso não aconteceu."},"lidaEm":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando o destinatário leu a mensagem. Nulo enquanto isso não aconteceu, e também quando ele desliga a confirmação de leitura."},"erros":{"type":"array","items":{"type":"object","properties":{"codigo":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"O código de erro da Meta, íntegro. Nulo quando não houve resposta da Meta — um tempo esgotado, por exemplo, não tem código."},"categoria":{"type":"string","enum":["parametro_invalido","reauth_required","recurso_inacessivel","orcamento_management","throughput_excedido","estado_de_produto","janela_de_atendimento","midia_indisponivel","nao_entregavel","template_invalido","ritmo_por_destinatario","terminal","transporte","contrato","desconhecido"],"description":"A natureza do erro, no vocabulário da Helsen. O valor desconhecido é literal e deliberado: a Meta acrescenta códigos sem aviso, e um código ainda não mapeado chega assim em vez de ser traduzido para uma categoria que não é a dele."},"mensagem":{"type":"string","description":"O que aconteceu, em português. É texto da Helsen, não a tradução da mensagem da Meta."},"acaoRecomendada":{"type":"string","description":"O que fazer a respeito, em português e em uma frase acionável."},"original":{"anyOf":[{},{"type":"null"}],"description":"O corpo de erro que a Meta devolveu, sem reescrita e sem filtro. É o que o suporte da Meta reconhece, e o fbtrace_id dentro dele é o que faz um chamado avançar. Atenção: ele pode conter o telefone do consumidor final e trechos da conversa — trate-o com o mesmo cuidado do restante do conteúdo das mensagens antes de repassá-lo a terceiros. É nulo quando não houve resposta da Meta."}},"required":["codigo","categoria","mensagem","acaoRecomendada","original"],"additionalProperties":false,"example":{"codigo":131026,"categoria":"nao_entregavel","mensagem":"O destinatário não pode receber esta mensagem: o número não tem WhatsApp, ou não existe.","acaoRecomendada":"Confira o número do destinatário e reenvie para o número correto. Repetir o envio para o mesmo número não muda o resultado.","original":{"error":{"message":"Message undeliverable","type":"OAuthException","code":131026,"fbtrace_id":"AbCdEfGhIjK"}}}},"description":"O que deu errado, em ordem de acontecimento, e vazio quando nada deu. Reúne o que a Meta recusou na hora do envio e o que ela informou depois por evento de status: você não precisa saber de qual das duas veio cada item."}},"required":["id","status","criadaEm","enviadaEm","entregueEm","lidaEm","erros"],"additionalProperties":false,"example":{"id":"ae60f288-7565-4315-a440-3663e9c231e1","status":"failed","criadaEm":"2026-08-24T12:24:02.085Z","enviadaEm":null,"entregueEm":null,"lidaEm":null,"erros":[{"codigo":131026,"categoria":"nao_entregavel","mensagem":"O destinatário não pode receber esta mensagem: o número não tem WhatsApp, ou não existe.","acaoRecomendada":"Confira o número do destinatário e reenvie para o número correto. Repetir o envio para o mesmo número não muda o resultado.","original":{"error":{"message":"Message undeliverable","type":"OAuthException","code":131026,"fbtrace_id":"AbCdEfGhIjK"}}}]}}}}}}}},"/v1/messages/{id}/replay":{"post":{"summary":"Reenviar o webhook de uma mensagem","tags":["Mensagens"],"description":"Pede que o evento desta mensagem seja entregue de novo ao seu endpoint. A resposta é 202: o pedido foi registrado, e a entrega acontece de forma assíncrona.\n\nCada pedido produz uma entrega nova, com deliveryId próprio. Chamar duas vezes produz duas entregas, e não um erro: o segundo pedido é um pedido, e não um clique repetido. Use o deliveryId para distinguir a reentrega que você pediu de uma reentrega automática por falha.\n\nO reenvio não fura a fila nem pula defesa nenhuma. Se o seu endpoint estiver fora do ar, a entrega será adiada e retentada pela política de sempre, sem que nenhuma requisição chegue a sair. Não adianta pedir em massa para acelerar.\n\nSe você ainda não cadastrou um endpoint de webhook, a resposta é 409: não há para onde entregar, e um 202 aqui seria uma confirmação que não se cumpre.\n\nMensagem que não é sua responde 404, igual a mensagem que não existe.\n\nA resposta não traz contagem de nada, de propósito: nenhum número desta API alimenta cobrança.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"in":"path","name":"id","required":true,"description":"O identificador da mensagem na Luna, como GET /v1/messages o devolve."}],"responses":{"202":{"description":"O pedido de reenvio foi registrado. A entrega acontece de forma assíncrona, pelo mesmo caminho de sempre.","content":{"application/json":{"schema":{"type":"object","properties":{"deliveryId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador desta entrega. Cada pedido gera um novo: o reenvio é entrega nova, e não reescrita de histórico."},"requestedAt":{"type":"string","description":"Quando o pedido foi registrado, em UTC."}},"required":["deliveryId","requestedAt"],"additionalProperties":false,"description":"O pedido de reenvio foi registrado. A entrega acontece de forma assíncrona, pelo mesmo caminho de sempre.","example":{"deliveryId":"9f2a1b0c-3d4e-4f5a-8b9c-0d1e2f3a4b5c","requestedAt":"2026-08-26T18:30:00.000Z"}}}}}}}},"/v1/onboarding/sessions":{"post":{"summary":"Abrir uma sessão de conexão","tags":["Onboarding"],"description":"Cria a sessão que o fluxo de conexão da Meta preenche, e devolve o sessionId e o csrfState.\n\nO csrfState é o valor de amarração. Ele volta na conclusão e é comparado em tempo constante. Sem ele, quem descobrisse um sessionId poderia concluir a conexão de outra pessoa.\n\nA sessão tem prazo. Enquanto ela não é concluída, nenhuma conta existe do lado da Luna.","responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"state":{"type":"string"},"configId":{"type":"string"},"csrfState":{"type":"string"},"expiresAt":{"type":"string"}},"required":["sessionId","state","configId","csrfState","expiresAt"],"additionalProperties":false}}}}}}},"/v1/onboarding/sessions/{id}":{"get":{"summary":"Consultar uma sessão de conexão","tags":["Onboarding"],"description":"O estado atual da sessão e a orientação correspondente, em português, pronta para exibir.\n\nUse para acompanhar o progresso depois da conclusão, ou para descobrir por que uma conexão parou. Estados terminais não avançam mais, e a orientação diz o que fazer em cada um.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"state":{"type":"string"},"orientacao":{"type":"string"},"terminal":{"type":"boolean"},"proximoPasso":{"anyOf":[{"type":"string"},{"type":"null"}]},"attemptCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"nextAttemptAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"retryNotBefore":{"anyOf":[{"type":"string"},{"type":"null"}]},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["sessionId","state","orientacao","terminal","proximoPasso","attemptCount","nextAttemptAt","retryNotBefore","expiresAt","createdAt","updatedAt"],"additionalProperties":false}}}}}}},"/v1/onboarding/sessions/{id}/abandon":{"post":{"summary":"Registrar abandono ou erro do fluxo","tags":["Onboarding"],"description":"Telemetria do que aconteceu na janela da Meta quando o fluxo não completou: em que tela o cliente parou, ou que erro a Meta reportou.\n\nNão muda o estado da sessão e não é obrigatório. Existe para que a causa de um abandono seja descobrível depois, em vez de virar silêncio.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"csrfState":{"type":"string","minLength":1},"currentStep":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}]},"errorCode":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"errorMessage":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"required":["csrfState"]}}}},"parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"state":{"type":"string"},"orientacao":{"type":"string"},"abandonedAtStep":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["sessionId","state","orientacao","abandonedAtStep"],"additionalProperties":false}}}}}}},"/v1/onboarding/sessions/{id}/complete":{"post":{"summary":"Concluir a conexão","tags":["Onboarding"],"description":"Troca o código do fluxo embutido por acesso permanente à conta do cliente, e leva a sessão até o fim: token selado, aplicativo inscrito na conta, número registrado.\n\nO código vale trinta segundos. Chame esta rota no mesmo instante em que ele chega, sem fila, sem trabalho intermediário e sem partida a frio no caminho.\n\nwabaId e phoneNumberId são opcionais de propósito. Quando o navegador não os entrega, o que acontece na prática, a Luna descobre os dois na Graph API a partir do próprio token. Mande o que você tiver.\n\nResponde 200 mesmo quando um passo falha. O corpo carrega o estado real da sessão e o motivo, porque conexão que para no meio não é erro de requisição: é estado de negócio.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"csrfState":{"type":"string","minLength":1},"wabaId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"phoneNumberId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"businessId":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}]}},"required":["code","csrfState"]}}}},"parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"state":{"type":"string"},"orientacao":{"type":"string"},"terminal":{"type":"boolean"},"proximoPasso":{"anyOf":[{"type":"string"},{"type":"null"}]},"motivo":{"anyOf":[{"type":"string"},{"type":"null"}]},"podeRetentar":{"type":"boolean"},"wabaId":{"anyOf":[{"type":"string"},{"type":"null"}]},"phoneNumberId":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["sessionId","state","orientacao","terminal","proximoPasso","motivo","podeRetentar","wabaId","phoneNumberId"],"additionalProperties":false,"example":{"sessionId":"77012a00-f049-4523-a658-3a3a4b105de6","state":"APP_SUBSCRIBED","orientacao":"O número está registrado na Meta e pronto para trocar mensagem.","terminal":true,"proximoPasso":null,"motivo":null,"podeRetentar":false,"wabaId":"1048633161461678","phoneNumberId":"1266743959851219"}}}}}}}},"/v1/numbers":{"get":{"summary":"Listar números conectados","tags":["Números"],"description":"Todos os números de WhatsApp conectados a este cliente, com estado, qualidade, velocidade de envio e limite diário de conversas.\n\nDois campos parecidos e diferentes: throughputLevel é VELOCIDADE, quantas mensagens por segundo a Meta processa; limiteDiarioDeConversas é VOLUME, quantas conversas novas o número pode iniciar por dia. Um não substitui o outro, e nenhum dos dois conta o que já foi usado.\n\nthroughputLevel descreve uma progressão que a Meta sobe sozinha conforme a qualidade do número. Nem todo número segue essa progressão: um número conectado em coexistência tem teto FIXO, que não sobe nunca. Quem diz qual é o caso é tetoDeVazaoPorSegundo, e ele sai na leitura de um número e na saúde dele, não nesta lista.\n\npodeRetentar diz se o estado admite nova tentativa de conexão. A decisão é da plataforma e não sua, porque cada tentativa de registro consome uma cota que a Meta conta por número numa janela de 72 horas. Esgotar essa cota do lado da Meta trava o número até a janela correr.\n\norientacao traz, em português, o que está acontecendo com a conexão e o que fazer a respeito. Ela é escrita para você repassar ao seu cliente sem reescrever.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaPhoneNumberId":{"type":"string"},"displayNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"state":{"type":"string"},"retryNotBefore":{"anyOf":[{"type":"string"},{"type":"null"}]},"podeRetentar":{"type":"boolean"},"qualityRating":{"anyOf":[{"type":"string"},{"type":"null"}]},"throughputLevel":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A VELOCIDADE de envio que a Meta concede a este número: quantas mensagens por segundo ela aceita processar. Não é o volume que o número pode iniciar por dia, que é limiteDiarioDeConversas."},"limiteDiarioDeConversas":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O VOLUME que este número pode INICIAR por dia, no vocabulário da Meta: TIER_250, TIER_1K, TIER_10K, TIER_100K ou TIER_UNLIMITED. Não é a velocidade de envio, que é throughputLevel. Conversas que o cliente final inicia, e respostas dentro da janela de atendimento, não consomem este limite. Nulo enquanto a plataforma ainda não verificou este número."},"codeVerificationStatus":{"anyOf":[{"type":"string"},{"type":"null"}]},"orientacao":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O que está acontecendo com a conexão deste número e o que fazer a respeito, em português pronto para você repassar ao seu cliente sem reescrever. Existe para todos os estados, inclusive os que não são falha: um número operando traz a confirmação de que está tudo certo. Um número que a Meta recusou por falta de método de pagamento traz aqui a razão e onde ela se resolve. Nulo quando não há sessão de conexão registrada para este número."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaPhoneNumberId","displayNumber","state","retryNotBefore","podeRetentar","qualityRating","throughputLevel","limiteDiarioDeConversas","codeVerificationStatus","orientacao","createdAt","updatedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}}}},"/v1/numbers/{id}":{"get":{"summary":"Consultar um número","tags":["Números"],"description":"Um número, com tudo que a listagem traz mais a saúde da conexão com a Meta.\n\ntetoDeVazaoPorSegundo é o campo que distingue teto que sobe de teto que não sobe. throughputLevel descreve a progressão que a Meta concede conforme a qualidade do número, e ela sobe com o tempo. Um número conectado em coexistência, que continua funcionando no aplicativo de celular do negócio, tem teto fixo e não entra nessa progressão. Com valor, este campo diz qual é o teto fixo em mensagens por segundo; nulo, a progressão vale e quem a descreve é throughputLevel. O valor vem do que a plataforma observou no tráfego deste número, e não do rótulo que a Meta devolveu.\n\nsincronizacaoDoHistorico conta a importação das conversas anteriores do aparelho, num número conectado em coexistência, e são QUATRO situações e não três. Nulo significa que não há importação para este número. em_curso significa que ela está acontecendo, e sincronizacaoPrazo diz até quando: passado esse instante sem concluir, a plataforma da Meta desconecta o negócio final. concluida significa que terminou, e as conversas antigas já aparecem em GET /v1/messages. recusada significa que o dono do negócio recusou compartilhar no aparelho dele, e aí não vai concluir e pedir de novo não muda isso.\n\nNão leia o percentual sozinho. sincronizacaoProgresso é um rótulo que a plataforma da Meta atribui, e cem por cento NÃO significa sucesso: a recusa chega com o progresso que já havia sido registrado. Quem diz o desfecho é sincronizacaoDoHistorico, e só ele. E ausência não é progresso zero: nulo é não haver importação, e zero é uma importação que começou e ainda não andou.\n\nQuando a Meta recusa o envio por falta de método de pagamento do negócio final, o número fica registrado e sem enviar. Esse não é um erro da plataforma e não há nada a fazer do lado da Helsen: o campo orientacao diz o que falta e onde resolver, em texto pronto para você repassar ao seu cliente.\n\nsubscriptionConfirmedAt é o campo que mais importa aqui. Ele registra quando a inscrição do aplicativo na conta foi confirmada por leitura de volta. Sem essa inscrição nenhum evento chega, e a falha é silenciosa: o número parece perfeito e não recebe nada.\n\ncredentialStatus diz se a autorização do cliente continua válida. Quando ela morre, o número precisa ser reconectado.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaPhoneNumberId":{"type":"string"},"displayNumber":{"anyOf":[{"type":"string"},{"type":"null"}]},"state":{"type":"string"},"retryNotBefore":{"anyOf":[{"type":"string"},{"type":"null"}]},"podeRetentar":{"type":"boolean"},"qualityRating":{"anyOf":[{"type":"string"},{"type":"null"}]},"throughputLevel":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A VELOCIDADE de envio que a Meta concede a este número: quantas mensagens por segundo ela aceita processar. Não é o volume que o número pode iniciar por dia, que é limiteDiarioDeConversas."},"limiteDiarioDeConversas":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O VOLUME que este número pode INICIAR por dia, no vocabulário da Meta: TIER_250, TIER_1K, TIER_10K, TIER_100K ou TIER_UNLIMITED. Não é a velocidade de envio, que é throughputLevel. Conversas que o cliente final inicia, e respostas dentro da janela de atendimento, não consomem este limite. Nulo enquanto a plataforma ainda não verificou este número."},"codeVerificationStatus":{"anyOf":[{"type":"string"},{"type":"null"}]},"orientacao":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O que está acontecendo com a conexão deste número e o que fazer a respeito, em português pronto para você repassar ao seu cliente sem reescrever. Existe para todos os estados, inclusive os que não são falha: um número operando traz a confirmação de que está tudo certo. Um número que a Meta recusou por falta de método de pagamento traz aqui a razão e onde ela se resolve. Nulo quando não há sessão de conexão registrada para este número."},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"tetoDeVazaoPorSegundo":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"O teto de envio deste número em mensagens por segundo, quando ele é FIXO e não sobe. Números conectados em coexistência, que continuam funcionando no aplicativo de celular do negócio, têm esse teto fixo pela plataforma da Meta, e ele não acompanha a progressão de throughputLevel. Nulo significa o contrário: este número segue a progressão da Meta, e quem a descreve é throughputLevel. Não é contagem de nada e não muda com o que o número enviou: é a capacidade que a plataforma da Meta concede.","example":null},"sincronizacaoDoHistorico":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Em que pé está a importação das conversas anteriores do aparelho do negócio, num número conectado em coexistência. São quatro situações e cada uma pede uma reação diferente. Nulo significa que não há importação para este número, e não que ela parou. em_curso significa que ela está acontecendo, e sincronizacaoPrazo diz até quando. concluida significa que terminou, e as conversas antigas já aparecem em GET /v1/messages. recusada significa que o dono do negócio recusou compartilhar no aparelho dele: não vai concluir, e pedir de novo não muda isso. Atenção ao ler sincronizacaoProgresso junto: cem por cento não significa sucesso, porque a recusa chega com o progresso que já havia sido registrado.","example":"em_curso"},"sincronizacaoPrazo":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Até quando a importação precisa terminar. Passado esse instante sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. Sai sempre como instante em UTC, e nunca como texto do tipo faltam tantas horas: um texto desses fica errado na tela de quem a deixou aberta, e quem integra pela API precisa de um valor comparável com o próprio relógio. Nulo quando não há importação em curso conhecida.","example":"2026-09-19T09:00:00.000Z"},"sincronizacaoProgresso":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"O rótulo de progresso que a plataforma da Meta atribui à importação, de 0 a 100. É um rótulo e não uma contagem: ele chega a 100 igualmente para um aparelho com poucas conversas antigas e para um com muitas, e nada nele cresce com o tamanho do que foi importado. Não use este campo para deduzir quantidade, porque ele não carrega nenhuma. Nulo quando a plataforma da Meta ainda não informou progresso para este número.","example":40},"subscriptionConfirmedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando a inscrição do aplicativo na conta foi confirmada por leitura de volta. Nulo significa que os eventos deste número ainda não chegam."},"credentialStatus":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"O estado da autorização concedida pelo cliente. Nunca a credencial em si."},"podeReconectar":{"type":"boolean","description":"Se a autorização deste número admite reconexão. Quando for true, POST /v1/onboarding/reconnect abre a sessão que a substitui."}},"required":["id","metaPhoneNumberId","displayNumber","state","retryNotBefore","podeRetentar","qualityRating","throughputLevel","limiteDiarioDeConversas","codeVerificationStatus","orientacao","createdAt","updatedAt","tetoDeVazaoPorSegundo","sincronizacaoDoHistorico","sincronizacaoPrazo","sincronizacaoProgresso","subscriptionConfirmedAt","credentialStatus","podeReconectar"],"additionalProperties":false,"example":{"id":"ed404df1-b420-4ed6-8743-ba1609892e0a","metaPhoneNumberId":"1266743959851219","displayNumber":"+55 31 98103-6436","state":"REGISTERED","retryNotBefore":null,"podeRetentar":false,"qualityRating":"GREEN","throughputLevel":"STANDARD","limiteDiarioDeConversas":"TIER_10K","codeVerificationStatus":"VERIFIED","orientacao":"Número conectado e operando. Mensagens já entram e saem por ele.","createdAt":"2026-08-07T11:28:14.597Z","updatedAt":"2026-08-07T11:28:28.703Z","tetoDeVazaoPorSegundo":null,"sincronizacaoDoHistorico":null,"sincronizacaoPrazo":null,"sincronizacaoProgresso":null,"subscriptionConfirmedAt":"2026-08-07T11:27:47.141Z","credentialStatus":"active","podeReconectar":false}}}}}}}},"/v1/numbers/{id}/pin":{"post":{"summary":"Informar o PIN de verificação em duas etapas de um número","tags":["Onboarding"],"description":"Informa o PIN de verificação em duas etapas de um número que já está registrado na Cloud API de outro provedor, para que a Luna consiga registrá-lo sem que o negócio final precise desligar a verificação.\n\nO id na URL é o identificador do número na Luna, o mesmo que GET /v1/numbers devolve, e não o identificador dele na Meta.\n\nUse quando state for PIN_REQUIRED. A Meta exige o PIN que já está configurado no número, e não um novo: não existe endpoint para ler nem para desligar o PIN de um número que já tem verificação em duas etapas, então apenas o negócio final o conhece. Se o número nunca teve verificação em duas etapas, o valor que você enviar passa a ser o PIN dele.\n\nO PIN é guardado cifrado e nunca volta por rota nenhuma. Ele não aparece na resposta, no registro de acesso nem em log.\n\nDepois da submissão o número volta ao ponto de onde a conexão prossegue, e a tentativa seguinte usa o PIN informado.\n\nPIN errado é recusado pela Meta e consome uma das dez tentativas de registro que ela concede a cada número numa janela de 72 horas. Esgotar essas tentativas trava o número até a janela correr, e por isso a plataforma guarda as últimas: quando o orçamento chega perto do fim, esta rota responde 409 dizendo quantas restam e quando a janela vira, em vez de gastar as que sobraram. Confirme o PIN com o negócio final antes de tentar de novo.\n\nNúmero que não está aguardando o PIN responde 409 com o estado corrente. Sobrescrever o PIN de um número que já conectou o deixaria sem caminho de volta.\n\nNúmero que não é seu responde 404, igual a número que não existe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pin":{"type":"string","pattern":"^\\d{6}$","description":"O PIN de verificação em duas etapas deste número, com exatamente seis dígitos. Quando o número já tem verificação em duas etapas configurada em outro provedor, é o PIN existente dele, e apenas o negócio final o conhece: não existe endpoint para ler nem para desligar o PIN de um número. Quando o número não tem verificação em duas etapas, este valor passa a ser o PIN dele."}},"required":["pin"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador deste número na Luna."},"state":{"type":"string","description":"O estado do número depois da submissão. Ele volta ao ponto de onde o registro opera, e a próxima tentativa de conexão usa o PIN que você informou."}},"required":["id","state"],"additionalProperties":false,"example":{"id":"ed404df1-b420-4ed6-8743-ba1609892e0a","state":"PENDING"}}}}}}}},"/v1/onboarding/reconnect":{"post":{"summary":"Reconectar um número cuja autorização morreu","tags":["Onboarding"],"description":"Abre uma sessão de conexão para um número que já existe, quando a autorização concedida pelo cliente expirou ou foi revogada.\n\nUse quando credentialStatus de GET /v1/numbers/{id} for reauth_required. O mesmo endpoint diz, em podeReconectar, se este caminho está disponível: a decisão é da plataforma, e não sua.\n\nA sessão devolvida é usada exatamente como a de uma conexão nova: leve o csrfState ao fluxo da Meta e conclua em POST /v1/onboarding/sessions/{id}/complete. A diferença está do nosso lado: a sessão já nasce amarrada à conta existente, então concluí-la substitui a autorização daquela conta em vez de criar uma segunda.\n\nNúmero cuja autorização ainda está válida responde 409, e a resposta diz o estado corrente. Autorização revogada em definitivo também responde 409: esse estado não volta, e o caminho é uma conexão nova.\n\nNúmero que não é seu responde 404, igual a número que não existe.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"numberId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador do número na Luna, o mesmo que GET /v1/numbers devolve como id."}},"required":["numberId"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"state":{"type":"string"},"configId":{"type":"string"},"csrfState":{"type":"string"},"expiresAt":{"type":"string"}},"required":["sessionId","state","configId","csrfState","expiresAt"],"additionalProperties":false,"example":{"sessionId":"77012a00-f049-4523-a658-3a3a4b105de6","state":"STARTED","configId":"1234567890","csrfState":"Xk3s...","expiresAt":"2026-08-26T18:30:00.000Z"}}}}}}}},"/v1/business-accounts":{"get":{"summary":"Listar contas de negócio","tags":["Números"],"description":"As contas de WhatsApp Business que este cliente conectou, da mais recente para a mais antiga.\n\nnumbers traz os números de cada conta na forma legível, para que uma tela de escolha possa mostrar o número em vez do identificador. Uma conta pode ter vários, e lista vazia é resposta válida — conta conectada cujo número ainda não completou o registro.\n\nwabaId é o identificador da conta na Meta, e é o valor que a criação de template recebe. Essa é a razão de esta rota existir: sem ela, o único lugar em que o identificador aparecia era a resposta de conclusão do onboarding, que é um evento passado.\n\nA rota devolve o identificador da conta e nada sobre a credencial dela. O estado da autorização do cliente sai em credentialStatus, na leitura de um número.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"wabaId":{"type":"string"},"numbers":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string"}},"required":["id","wabaId","numbers","createdAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false,"example":{"data":[{"id":"ed404df1-b420-4ed6-8743-ba1609892e0a","wabaId":"102290129340398","numbers":["+55 31 98103-6436"],"createdAt":"2026-08-07T11:27:41.208Z"}]}}}}}}}},"/v1/operators/signup":{"post":{"summary":"Criar uma conta","tags":["Console"],"description":"Cria a conta e o primeiro operador dela, numa única chamada e sem credencial.\n\nA resposta não traz sessão. Depois do 201, autentique em POST /v1/operators/login com o mesmo e-mail e a mesma senha.\n\nA senha tem no mínimo 12 caracteres, sem exigência de composição. Comprimento protege mais que a mistura obrigatória de símbolos, que só produz senha previsível.\n\nA conta nova entra, lê a documentação, cria e revoga chave de API e integra contra esta API. O que ela ainda não pode é abrir sessão de conexão de número: isso espera a confirmação do e-mail, e nada além dela.\n\nA recusa não diz se o e-mail já pertence a alguém. Dizer isso entregaria de graça a lista de quem tem conta.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":254},"password":{"type":"string","minLength":12,"maxLength":512},"tenantName":{"type":"string","minLength":1,"maxLength":120}},"required":["email","password","tenantName"]}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"nextStep":{"type":"string","enum":["login"]}},"required":["nextStep"],"additionalProperties":false}}}}}}},"/v1/operators/confirm":{"get":{"summary":"Confirmar o e-mail de uma conta","tags":["Console"],"description":"Destino do link enviado por e-mail quando a conta é criada. Ela responde com um redirecionamento para o console, e não com JSON, porque quem a abre é uma pessoa num navegador.\n\nO link vale por sete dias e é aceito uma única vez. Abrir de novo não desfaz a confirmação, e a conta continua confirmada.\n\nToda recusa leva ao mesmo endereço, com erro=1. A resposta não diz se o token não existe, se expirou ou se já foi usado, porque distinguir os três entregaria informação sobre uma conta alheia.\n\nConfirmar o e-mail é o que libera a conexão de número: a conta confirmada já pode abrir sessão de conexão.","parameters":[{"schema":{"type":"string","maxLength":512},"in":"query","name":"token","required":false}],"responses":{"200":{"description":"Default Response"}}}},"/v1/operators/login":{"post":{"summary":"Autenticar um operador","tags":["Console"],"description":"Troca email e senha por uma credencial de sessão de oito horas, usada pelo console web.\n\nA recusa é sempre a mesma, byte a byte. A API não distingue senha errada de conta inexistente, porque distinguir entregaria de graça a lista de quem tem conta.\n\nA sessão não pode criar nem revogar chaves de API. Para isso existe a reautenticação.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","minLength":1,"maxLength":254},"password":{"type":"string","minLength":1,"maxLength":512}},"required":["email","password"]}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tenantId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"operatorId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"},"token":{"type":"string"},"expiresAt":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}},"required":["tenantId","operatorId","token","expiresAt","scopes"],"additionalProperties":false}}}}}}},"/v1/operators/step-up":{"post":{"summary":"Reautenticar para uma ação sensível","tags":["Console"],"description":"Confirma a senha do operador da sessão apresentada e abre uma janela de duas horas em que essa mesma sessão pode criar e revogar chaves de API.\n\nNenhuma credencial elevada é emitida. Não há segredo novo nesta resposta, nem linha nova no banco: o que muda é um instante gravado na própria sessão, e elevatedUntil é esse instante.\n\nPassadas as duas horas a senha volta a ser pedida, e reabrir a janela exige confirmá-la de novo. Encerrar a sessão encerra a janela junto, sem passo próprio.\n\nNão recebe email: a identidade vem da própria sessão. Assim a senha de um operador nunca eleva a sessão de outro.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","minLength":1,"maxLength":512}},"required":["password"]}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"elevatedUntil":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}}},"required":["elevatedUntil","scopes"],"additionalProperties":false}}}}}}},"/v1/operators/logout":{"post":{"summary":"Encerrar a sessão","tags":["Console"],"description":"Revoga a credencial apresentada, e apenas ela. O identificador vem da própria autenticação e nunca do corpo, então esta rota não consegue revogar chave de terceiro.","responses":{"200":{"description":"Default Response"}}}},"/v1/numbers/{id}/health":{"get":{"summary":"Consultar a saúde de um número","tags":["Números"],"description":"A avaliação de qualidade, o nível de vazão e o limite diário de conversas que a Meta atribui a este número agora, e as mudanças que a plataforma observou ao longo do tempo.\n\nlimiteDiarioDeConversas é VOLUME, quantas conversas novas o número pode iniciar por dia; throughputLevel é VELOCIDADE, quantas mensagens por segundo a Meta processa. Cada transição carrega o valor vigente naquele ponto, e é assim que se descobre quando o limite subiu ou desceu. Nenhum dos dois conta o que já foi usado.\n\nE há teto que sobe e teto que não sobe. throughputLevel descreve uma progressão que a Meta sobe sozinha conforme a qualidade do número. Um número conectado em coexistência, que continua funcionando no aplicativo de celular do negócio, tem teto FIXO e não entra nessa progressão. Quem separa os dois casos é tetoDeVazaoPorSegundo, logo ao lado: com valor, ele diz o teto fixo em mensagens por segundo; nulo, a progressão vale para este número. O valor vem do que a plataforma observou no tráfego, e não do rótulo que a Meta devolveu.\n\nsincronizacaoDoHistorico conta a importação das conversas anteriores do aparelho, num número conectado em coexistência, e são QUATRO situações e não três. Nulo significa que não há importação para este número. em_curso significa que ela está acontecendo, e sincronizacaoPrazo diz até quando: passado esse instante sem concluir, a plataforma da Meta desconecta o negócio final. concluida significa que terminou, e as conversas antigas já aparecem em GET /v1/messages. recusada significa que o dono do negócio recusou compartilhar no aparelho dele, e aí não vai concluir e pedir de novo não muda isso.\n\nNão leia o percentual sozinho. sincronizacaoProgresso é um rótulo que a plataforma da Meta atribui, e cem por cento NÃO significa sucesso: a recusa chega com o progresso que já havia sido registrado. Quem diz o desfecho é sincronizacaoDoHistorico, e só ele. E ausência não é progresso zero: nulo é não haver importação, e zero é uma importação que começou e ainda não andou.\n\nA lista de transições traz uma entrada por MUDANÇA, e não uma por verificação. Um número cuja avaliação nunca mudou desde a conexão devolve a lista vazia, e isso significa estabilidade: não trate lista vazia como ausência de dado.\n\nQuem separa os dois casos é checkedAt. Com instante, a plataforma perguntou à Meta e não encontrou mudança. Nulo, a verificação ainda não aconteceu para este número.\n\nAs transições vêm da mais recente para a mais antiga.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"qualityRating":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"GREEN"},"throughputLevel":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"HIGH"},"tetoDeVazaoPorSegundo":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"O teto de envio deste número em mensagens por segundo, quando ele é FIXO e não sobe. Números conectados em coexistência, que continuam funcionando no aplicativo de celular do negócio, têm esse teto fixo pela plataforma da Meta, e ele não acompanha a progressão de throughputLevel. Nulo significa o contrário: este número segue a progressão da Meta, e quem a descreve é throughputLevel. Não é contagem de nada e não muda com o que o número enviou: é a capacidade que a plataforma da Meta concede.","example":null},"sincronizacaoDoHistorico":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Em que pé está a importação das conversas anteriores do aparelho do negócio, num número conectado em coexistência. São quatro situações e cada uma pede uma reação diferente. Nulo significa que não há importação para este número, e não que ela parou. em_curso significa que ela está acontecendo, e sincronizacaoPrazo diz até quando. concluida significa que terminou, e as conversas antigas já aparecem em GET /v1/messages. recusada significa que o dono do negócio recusou compartilhar no aparelho dele: não vai concluir, e pedir de novo não muda isso. Atenção ao ler sincronizacaoProgresso junto: cem por cento não significa sucesso, porque a recusa chega com o progresso que já havia sido registrado.","example":"em_curso"},"sincronizacaoPrazo":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Até quando a importação precisa terminar. Passado esse instante sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. Sai sempre como instante em UTC, e nunca como texto do tipo faltam tantas horas: um texto desses fica errado na tela de quem a deixou aberta, e quem integra pela API precisa de um valor comparável com o próprio relógio. Nulo quando não há importação em curso conhecida.","example":"2026-09-19T09:00:00.000Z"},"sincronizacaoProgresso":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"O rótulo de progresso que a plataforma da Meta atribui à importação, de 0 a 100. É um rótulo e não uma contagem: ele chega a 100 igualmente para um aparelho com poucas conversas antigas e para um com muitas, e nada nele cresce com o tamanho do que foi importado. Não use este campo para deduzir quantidade, porque ele não carrega nenhuma. Nulo quando a plataforma da Meta ainda não informou progresso para este número.","example":40},"limiteDiarioDeConversas":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"TIER_10K"},"codeVerificationStatus":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"VERIFIED"},"checkedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"2026-08-26T12:00:00.000Z"},"transitions":{"type":"array","items":{"type":"object","properties":{"qualityRating":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"GREEN"},"throughputLevel":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"HIGH"},"limiteDiarioDeConversas":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"TIER_1K"},"codeVerificationStatus":{"anyOf":[{"type":"string"},{"type":"null"}],"example":"VERIFIED"},"observedAt":{"type":"string","example":"2026-08-24T10:00:00.000Z"}},"required":["qualityRating","throughputLevel","limiteDiarioDeConversas","codeVerificationStatus","observedAt"],"additionalProperties":false}}},"required":["id","qualityRating","throughputLevel","tetoDeVazaoPorSegundo","sincronizacaoDoHistorico","sincronizacaoPrazo","sincronizacaoProgresso","limiteDiarioDeConversas","codeVerificationStatus","checkedAt","transitions"],"additionalProperties":false}}}}}}},"/v1/templates":{"post":{"summary":"Criar um template","tags":["Templates"],"description":"Cria o template na conta do cliente e o submete à Meta para aprovação.\n\nTemplate é o que permite iniciar conversa fora da janela de 24 horas. Dentro da janela, texto livre basta.\n\nO corpo aceita variável na forma {{nome_da_variavel}} — minúsculas e sublinhado. Toda variável exige um valor de amostra em examples, porque a Meta recusa parâmetro sem exemplo, e a recusa custa uma unidade do orçamento de gestão da conta.\n\nA categoria que volta na resposta é a que a Meta atribuiu, e pode não ser a que você pediu, porque ela reclassifica na criação. O que a Luna guarda é o que a Meta respondeu, nunca o que foi pedido. E ela é a categoria atribuída NA CRIAÇÃO: a Luna não a mantém depois disso. Para a categoria corrente de um template antigo, consulte o painel da Meta.\n\nCada idioma é um template SEPARADO para a Meta. Dez templates em nove idiomas são noventa templates, e o teto é de 250 por conta de WhatsApp Business. O teto é da Meta, é o mesmo para todos os clientes: ele existe por capacidade da plataforma dela, e não varia.\n\nAprovado não significa liberado para disparo em massa. A Meta aplica um ritmo próprio de liberação nos primeiros envios.\n\nCabeçalho de mídia: use headerMedia com format IMAGE, VIDEO ou DOCUMENT e o handle devolvido por POST /v1/template-assets. Esse handle não é o identificador de mídia que POST /v1/numbers/{id}/media devolve: são dois endpoints da Meta com propósitos diferentes, e um identificador de mídia no lugar do handle faz a criação ser recusada. Para documento a Meta aceita apenas PDF. Nem GIF nem localização são aceitos, e isso é decisão e não esquecimento.\n\nO arquivo do headerMedia é o EXEMPLO que a Meta revisa, e não o conteúdo entregue. Cada mensagem que você enviar com este template fornece a própria mídia, no componente de cabeçalho do envio. Quem criar o template achando que a imagem do exemplo é a imagem enviada vai mandar mensagem sem cabeçalho.\n\nOs dois cabeçalhos são exclusivos: header é o de texto, headerMedia é o de mídia, e a Meta aceita um só por template.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"wabaId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"name":{"type":"string","pattern":"^[a-z0-9_]{1,512}$"},"bodyText":{"type":"string","minLength":1,"maxLength":1024},"examples":{"example":{"primeiro_nome":"Maria","pedido":"88213"},"description":"Um valor de amostra por variável do corpo. Obrigatório quando o corpo tem `{{variavel}}`, porque a Meta recusa parâmetro sem exemplo.","type":"object","propertyNames":{"type":"string","pattern":"^[a-z0-9_]+$"},"additionalProperties":{"type":"string","minLength":1,"maxLength":512}},"header":{"type":"string","minLength":1,"maxLength":60,"description":"Cabeçalho de texto, exibido acima do corpo. Para cabeçalho de imagem, vídeo ou documento, use headerMedia."},"headerMedia":{"type":"object","properties":{"format":{"type":"string","enum":["IMAGE","VIDEO","DOCUMENT"],"description":"IMAGE, VIDEO ou DOCUMENT. Para documento a Meta aceita apenas PDF."},"handle":{"type":"string","minLength":1,"maxLength":2048,"description":"O handle devolvido por POST /v1/template-assets."}},"required":["format","handle"],"additionalProperties":false,"description":"Cabeçalho de imagem, vídeo ou documento. O handle vem de POST /v1/template-assets, e não do identificador de mídia de mensagem. O arquivo é o exemplo que a Meta revisa, e não o conteúdo entregue: cada mensagem enviada fornece a própria mídia."},"footer":{"type":"string","minLength":1,"maxLength":60,"description":"Rodapé, exibido abaixo do corpo. Não aceita variável."},"buttons":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["QUICK_REPLY"]},"text":{"type":"string","minLength":1,"maxLength":25}},"required":["type","text"]},{"type":"object","properties":{"type":{"type":"string","enum":["URL"]},"text":{"type":"string","minLength":1,"maxLength":25},"url":{"type":"string","minLength":1,"maxLength":2000},"example":{"maxItems":1,"type":"array","items":{"type":"string","minLength":1,"maxLength":2000}}},"required":["type","text","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["PHONE_NUMBER"]},"text":{"type":"string","minLength":1,"maxLength":25},"phone_number":{"type":"string","minLength":1,"maxLength":20}},"required":["type","text","phone_number"]},{"type":"object","properties":{"type":{"type":"string","enum":["COPY_CODE"]},"example":{"type":"string","minLength":1,"maxLength":20}},"required":["type","example"]}]},"description":"Até 10 botões no total, somando todos os tipos. Se houver respostas rápidas junto de outros botões, os dois conjuntos precisam ficar agrupados — respostas rápidas de um lado, o resto do outro. Acima de três botões, apenas dois aparecem na mensagem entregue: a Meta aceita o template e entrega dois."},"category":{"type":"string","enum":["MARKETING","UTILITY"],"example":"UTILITY","description":"A categoria do template. Ausente vale UTILITY, que é o comportamento de quem já integra e não manda o campo. A Meta pode reclassificar na criação: o template continua aprovado, com outra categoria, e a resposta traz a que ela atribuiu. Template de autenticação tem corpo próprio e ainda não é criado por esta rota."},"language":{"type":"string","pattern":"^[a-z]{2,3}(_[A-Z]{2})?$","example":"pt_BR","description":"O código de idioma do template, na grafia da Meta: sublinhado e não hífen. pt_BR, pt_PT, en_US, es_MX, fr. A lista completa está em Supported Languages, na documentação da Meta, e o campo aceita qualquer código que ela publicar, porque um conjunto fechado aqui impediria você de usar o próximo. Ausente vale pt_BR. Atenção ao que o idioma custa: cada idioma é um template separado para a Meta, e o teto é de 250 templates por conta de WhatsApp Business."}},"required":["wabaId","name","bodyText"]}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaTemplateId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string"},"category":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaTemplateId","name","language","category","status","createdAt","updatedAt"],"additionalProperties":false}}}}}},"get":{"summary":"Listar templates","tags":["Templates"],"description":"Os templates deste cliente, do mais recente para o mais antigo.\n\nO status que volta aqui é o último que a Luna observou, e não uma leitura ao vivo na Meta. Para o estado atual de um template específico, use a consulta por identificador.\n\nUse ?status= para filtrar por estado, por exemplo ?status=PAUSED. A comparação é literal e sensível a maiúsculas, e o campo aceita qualquer texto: a Meta acrescenta estado sem aviso, e uma lista fechada impediria você de filtrar pelo estado novo no dia em que ele aparecesse.\n\nA categoria devolvida é a que a Meta atribuiu NA CRIAÇÃO, e pode não ser a que foi pedida, porque ela reclassifica. A Luna não a mantém depois disso: se a Meta reclassificar um template já aprovado, o valor que você lê aqui continua sendo o da criação. Para a categoria corrente, consulte o painel da Meta.\n\nAprovado não significa liberado para disparo em massa. A Meta aplica um ritmo próprio de liberação nos primeiros envios.","parameters":[{"schema":{"example":"PAUSED","type":"string","minLength":1,"maxLength":64},"in":"query","name":"status","required":false,"description":"Devolve apenas os templates cujo último estado observado é exatamente este valor. A comparação é literal e sensível a maiúsculas. Valores comuns: APPROVED, PENDING, REJECTED, PAUSED, DISABLED — mas o campo é texto livre, porque a Meta acrescenta estado sem aviso e um conjunto fechado impediria você de filtrar pelo estado novo."}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaTemplateId":{"type":"string"},"wabaId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string"},"category":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaTemplateId","wabaId","name","language","category","status","createdAt","updatedAt"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false,"example":{"data":[{"id":"ae60f288-7565-4315-a440-3663e9c231e1","metaTemplateId":"1259544702043801","wabaId":"102290129340398","name":"numero_conectado","language":"pt_BR","category":"UTILITY","status":"APPROVED","createdAt":"2026-08-10T12:24:02.085Z","updatedAt":"2026-08-10T12:31:47.004Z"}]}}}}}}}},"/v1/templates/{id}":{"get":{"summary":"Consultar o status de um template","tags":["Templates"],"description":"O estado atual do template na Meta.\n\nO estado é texto livre e não um conjunto fechado, de propósito: a Meta acrescenta valor novo sem aviso, e um conjunto fechado transformaria isso em erro da Luna.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaTemplateId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string"},"category":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaTemplateId","name","language","category","status","createdAt","updatedAt"],"additionalProperties":false}}}}}},"patch":{"summary":"Editar um template","tags":["Templates"],"description":"Altera os componentes de um template existente.\n\nA atualização é PARCIAL: o que você não informar permanece como está. É por isso que o verbo é PATCH e não PUT — PUT prometeria substituir o template inteiro, e não é isso que acontece.\n\nNome, idioma e conta de negócio NÃO podem ser alterados: a Meta não aceita mudá-los depois de o template criado. Para um nome diferente, crie outro template.\n\nSe você informar qualquer componente, informe também bodyText: a Meta recusa template sem corpo.\n\nA edição volta a submeter o template à aprovação, e o estado muda. Consulte o estado depois, ou espere o webhook.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"bodyText":{"type":"string","minLength":1,"maxLength":1024},"examples":{"type":"object","propertyNames":{"type":"string","pattern":"^[a-z0-9_]+$"},"additionalProperties":{"type":"string","minLength":1,"maxLength":512}},"header":{"type":"string","minLength":1,"maxLength":60,"description":"Cabeçalho de texto, exibido acima do corpo. Para cabeçalho de imagem, vídeo ou documento, use headerMedia."},"footer":{"type":"string","minLength":1,"maxLength":60,"description":"Rodapé, exibido abaixo do corpo. Não aceita variável."},"buttons":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["QUICK_REPLY"]},"text":{"type":"string","minLength":1,"maxLength":25}},"required":["type","text"]},{"type":"object","properties":{"type":{"type":"string","enum":["URL"]},"text":{"type":"string","minLength":1,"maxLength":25},"url":{"type":"string","minLength":1,"maxLength":2000},"example":{"maxItems":1,"type":"array","items":{"type":"string","minLength":1,"maxLength":2000}}},"required":["type","text","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["PHONE_NUMBER"]},"text":{"type":"string","minLength":1,"maxLength":25},"phone_number":{"type":"string","minLength":1,"maxLength":20}},"required":["type","text","phone_number"]},{"type":"object","properties":{"type":{"type":"string","enum":["COPY_CODE"]},"example":{"type":"string","minLength":1,"maxLength":20}},"required":["type","example"]}]},"description":"Até 10 botões no total, somando todos os tipos. Se houver respostas rápidas junto de outros botões, os dois conjuntos precisam ficar agrupados — respostas rápidas de um lado, o resto do outro. Acima de três botões, apenas dois aparecem na mensagem entregue: a Meta aceita o template e entrega dois."}}}}}},"parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaTemplateId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string"},"category":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaTemplateId","name","language","category","status","createdAt","updatedAt"],"additionalProperties":false}}}}}},"delete":{"summary":"Remover um template","tags":["Templates"],"description":"Remove o template na Meta e da sua listagem.\n\nATENÇÃO: a Meta remove template pelo NOME, e isso apaga TODAS as versões de idioma daquele nome — não apenas a que você está removendo. Se você mantém o mesmo template em três idiomas, esta chamada apaga os três de uma vez do lado da Meta. A Luna remove da sua listagem apenas o que você pediu: as outras versões de idioma continuam aparecendo aqui e já não existem lá. A consequência é da Meta, é destrutiva e não muda.\n\nA remoção é definitiva. Um template removido não volta, e criar outro com o mesmo nome recomeça a aprovação do zero.\n\nRemover duas vezes é seguro: a segunda chamada devolve 404, o mesmo 404 de um identificador que nunca existiu.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response"}}}},"/v1/templates/authentication":{"post":{"summary":"Criar um template de autenticação","tags":["Templates"],"description":"Cria um template de autenticação com botão de copiar código, e o submete à Meta para aprovação.\n\nEste é um caminho de criação separado, e não uma categoria a mais em POST /v1/templates. A razão é que o conteúdo não é seu: o corpo é fixo pela Meta e não é customizável, com o texto \"{{1}} is your verification code.\" traduzido para o idioma do template, e a propriedade de texto não é aceita. Por isso esta rota não recebe corpo, exemplos, cabeçalho, rodapé nem botões. O que você escolhe é o nome, o idioma, se o aviso de segurança aparece, em quantos minutos o código expira e, se quiser, o rótulo do botão.\n\nO aviso de segurança e o aviso de expiração também são textos da Meta, gerados no idioma do template. O rótulo do botão, quando omitido, vem no idioma do template já traduzido, e omitir é o recomendado.\n\nA categoria que volta na resposta é a que a Meta atribuiu, como em toda criação, e a Luna guarda o que ela respondeu.\n\nCada idioma é um template SEPARADO para a Meta, e o teto de 250 por conta de WhatsApp Business vale igual aqui. O teto é da Meta, é o mesmo para todos os clientes: ele existe por capacidade da plataforma dela, e não varia.\n\nSobre o ENVIO, três coisas que valem saber antes de integrar. Primeira: o código de uso único vai DUAS VEZES no corpo da mensagem enviada, uma no componente de corpo e outra no componente de botão. Isso é o formato, e não redundância a ser otimizada. Segunda: a senha aceita no máximo 15 caracteres. Terceira, e esta é uma lacuna que preferimos declarar a preencher com chute: a forma exata do componente de botão no envio ainda não é publicada aqui, porque a documentação da Meta tem duas páginas em desacordo sobre ela e ninguém desta casa executou um envio para decidir. Ela será publicada quando houver essa execução.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"wabaId":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"name":{"type":"string","pattern":"^[a-z0-9_]{1,512}$"},"language":{"type":"string","pattern":"^[a-z]{2,3}(_[A-Z]{2})?$","example":"pt_BR","description":"O código de idioma do template, na grafia da Meta: sublinhado e não hífen. pt_BR, pt_PT, en_US, es_MX, fr. A lista completa está em Supported Languages, na documentação da Meta, e o campo aceita qualquer código que ela publicar, porque um conjunto fechado aqui impediria você de usar o próximo. Ausente vale pt_BR. Atenção ao que o idioma custa: cada idioma é um template separado para a Meta, e o teto é de 250 templates por conta de WhatsApp Business."},"addSecurityRecommendation":{"type":"boolean","example":true,"description":"Se o template deve incluir o aviso de segurança da Meta, que pede para não compartilhar o código. O texto é da Meta, é gerado no idioma do template e não é customizável. Ausente vale falso."},"codeExpirationMinutes":{"type":"integer","minimum":1,"maximum":1440,"example":10,"description":"Em quantos minutos o código expira. Vai para o rodapé do template, num texto da Meta que não é customizável. O teto aceito aqui é uma guarda desta plataforma contra valor absurdo, e não uma afirmação sobre a faixa que a Meta aceita: essa faixa não está publicada na documentação dela."},"buttonText":{"type":"string","minLength":1,"maxLength":25,"example":"Copiar código","description":"O rótulo do botão de copiar código. Omitir é o recomendado: sem ele a Meta usa o rótulo padrão do idioma do template, que já está traduzido. Máximo de 25 caracteres."},"messageSendTtlSeconds":{"type":"integer","minimum":1,"maximum":9007199254740991,"example":60,"description":"Tempo de vida, em segundos, das mensagens enviadas com este template. Ausente, o campo não é enviado à Meta e vale o padrão dela. Um código de verificação que chega tarde demais já não serve, e é para isso que ele existe."}},"required":["wabaId","name","codeExpirationMinutes"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"metaTemplateId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string"},"category":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","metaTemplateId","name","language","category","status","createdAt","updatedAt"],"additionalProperties":false}}}}}}},"/v1/template-assets":{"post":{"summary":"Subir o arquivo de exemplo de um cabeçalho de mídia","tags":["Templates"],"description":"Recebe um arquivo em multipart/form-data no campo file e devolve o handle que a criação de template aceita em headerMedia.handle.\n\nEste handle NÃO é o identificador de mídia que POST /v1/numbers/{id}/media devolve. São dois endpoints da Meta com propósitos diferentes: aquele serve para enviar mensagem, e este serve para o exemplo que a Meta revisa ao aprovar o template. Um identificador de mídia no lugar do handle faz a criação ser recusada. Na prática você reconhece a troca de longe: o identificador de mídia é um número decimal longo, e o handle não é numérico.\n\nO arquivo aqui é o EXEMPLO, e não o conteúdo entregue. Ele existe para que a Meta veja como o template fica. Cada mensagem que você enviar depois fornece a própria mídia. Quem criar o template achando que esta imagem é a que chega ao destinatário vai mandar mensagem sem cabeçalho.\n\nOs tipos aceitos são application/pdf, image/jpeg, image/jpg, image/png e video/mp4. A lista é mais estreita que a do upload de mídia de mensagem porque é a do endpoint que esta rota chama, e para documento ela aceita apenas PDF. Nem GIF nem localização entram, e isso é decisão e não esquecimento: o requisito nomeia imagem, vídeo e documento.\n\nO conteúdo do arquivo é conferido contra o tipo declarado antes de qualquer ida à Meta. Um arquivo declarado como image/png cujos bytes são de outra coisa é recusado com 400 aqui, para não gastar chamada sua.\n\nUm arquivo acima do teto é recusado com 413 antes de qualquer ida à Meta. O teto é o mesmo para todos os clientes: ele existe por capacidade de infraestrutura, e não varia.\n\nQuanto tempo o handle vale: a Meta não publica prazo de validade, e a Luna não guarda o valor. Obtenha o handle e use-o na criação do template em seguida. Se ele deixar de servir, suba o arquivo de novo.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"handle":{"type":"string","description":"O identificador do arquivo de exemplo. Use-o em headerMedia.handle ao criar o template."}},"required":["handle"],"additionalProperties":false,"example":{"handle":"4:cGVkaWRvLnBuZw==:aW1hZ2UvcG5n:ARb0Xk:e:1790451084:1269849425120687:0"}}}}}}}},"/v1/webhooks/endpoints":{"post":{"summary":"Cadastrar o endpoint de webhook","tags":["Webhooks"],"description":"Registra a URL do seu servidor e devolve o segredo que assina as entregas.\n\nO segredo aparece nesta resposta e em mais nenhuma. Guarde-o no momento em que o receber: a Luna guarda apenas a forma cifrada dele, e nenhuma rota o devolve. Se você o perder, rotacione para receber um novo.\n\nA URL precisa usar https e precisa apontar para um endereço público na internet. Endereço de rede interna, de laço local e de serviço de metadados de nuvem são recusados aqui e também no momento de cada entrega, porque um nome pode mudar de endereço entre o cadastro e a chamada.\n\nA Luna não segue redirecionamento: cadastre a URL final.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2048,"description":"A URL do seu servidor que vai receber os eventos. Precisa ser https e precisa resolver para um endereço público."}},"required":["url"],"additionalProperties":false,"description":"O cadastro do endpoint para onde a Luna entrega o que chega no seu número."}}},"description":"O cadastro do endpoint para onde a Luna entrega o que chega no seu número."},"responses":{"201":{"description":"O endpoint recém-criado, com o segredo de assinatura.\n\nEsta é a única resposta que carrega o segredo, junto com a da rotação. Guarde-o agora.\n\nA entrega é at-least-once: a mesma entrega pode chegar mais de uma vez, e isso acontece de verdade, não apenas em teoria. Um tempo limite atingido depois de o seu servidor já ter processado a requisição produz exatamente esse caso.\n\nA ordem é garantida dentro de uma conversa. Ela não é garantida entre conversas: dois eventos de conversas diferentes podem chegar em qualquer ordem, e um evento de uma conversa lenta pode chegar depois de um evento mais recente de outra.\n\nA idempotência é dever seu, e o event_id é o que a plataforma dá para você cumpri-la. Ele é estável: a mesma entrega, repetida, traz o mesmo event_id. Guarde os que já processou e descarte os repetidos.\n\nSe o seu endpoint ficar fora do ar por muito tempo, a plataforma descarta o que ficou acumulado, e você perde esses eventos: eles não serão reenviados. Quando isso acontecer, você recebe um evento backlog.dropped dizendo quantos foram descartados e de que período. Esse aviso não é descartado junto com o acumulado que ele descreve.\n\nNovos tipos de evento podem aparecer sem que a versão do contrato mude. Se o seu servidor receber um event_type que não conhece, responda 2xx e ignore o evento. A versão só muda quando a forma de um tipo que já existe muda, e isso vem com aviso e prazo.\n\nNum número conectado em coexistência, se a importação das conversas anteriores ainda estiver em curso quando o prazo dela se aproxima, você recebe um evento history_sync.deadline_approaching com o número e o prazo como instante. Esse prazo é a estimativa da plataforma: 24 horas contadas de quando ela viu a importação começar. O relógio da Meta começa um pouco antes, então trate o instante como limite e aja com folga. Passado o prazo sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. O aviso sai antes do prazo e nunca depois, e é um só para o número: como toda entrega, ele pode se repetir, sempre com o mesmo event_id, e um número que refaz a conexão depois não recebe um segundo aviso. O estado corrente da importação está em GET /v1/numbers/{id}/health.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador do endpoint, devolvido no cadastro."},"url":{"type":"string","maxLength":2048,"description":"A URL do seu servidor que vai receber os eventos. Precisa ser https e precisa resolver para um endereço público."},"createdAt":{"type":"string","description":"Quando o endpoint foi cadastrado, em UTC."},"updatedAt":{"type":"string","description":"Quando ele foi alterado pela última vez, em UTC."},"previousSecretExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Enquanto este instante estiver no futuro, as entregas carregam duas assinaturas: a do segredo novo e a do anterior. Depois dele, só a do novo."},"secret":{"type":"string","description":"O segredo que assina as entregas para este endpoint. Ele aparece nesta resposta e em mais nenhuma: guarde-o agora. A Luna guarda apenas a forma cifrada dele, e nenhuma rota o devolve. Se você o perder, rotacione para receber um novo."}},"required":["id","url","createdAt","updatedAt","previousSecretExpiresAt","secret"],"additionalProperties":false,"description":"O endpoint recém-criado, com o segredo de assinatura.\n\nEsta é a única resposta que carrega o segredo, junto com a da rotação. Guarde-o agora.\n\nA entrega é at-least-once: a mesma entrega pode chegar mais de uma vez, e isso acontece de verdade, não apenas em teoria. Um tempo limite atingido depois de o seu servidor já ter processado a requisição produz exatamente esse caso.\n\nA ordem é garantida dentro de uma conversa. Ela não é garantida entre conversas: dois eventos de conversas diferentes podem chegar em qualquer ordem, e um evento de uma conversa lenta pode chegar depois de um evento mais recente de outra.\n\nA idempotência é dever seu, e o event_id é o que a plataforma dá para você cumpri-la. Ele é estável: a mesma entrega, repetida, traz o mesmo event_id. Guarde os que já processou e descarte os repetidos.\n\nSe o seu endpoint ficar fora do ar por muito tempo, a plataforma descarta o que ficou acumulado, e você perde esses eventos: eles não serão reenviados. Quando isso acontecer, você recebe um evento backlog.dropped dizendo quantos foram descartados e de que período. Esse aviso não é descartado junto com o acumulado que ele descreve.\n\nNovos tipos de evento podem aparecer sem que a versão do contrato mude. Se o seu servidor receber um event_type que não conhece, responda 2xx e ignore o evento. A versão só muda quando a forma de um tipo que já existe muda, e isso vem com aviso e prazo.\n\nNum número conectado em coexistência, se a importação das conversas anteriores ainda estiver em curso quando o prazo dela se aproxima, você recebe um evento history_sync.deadline_approaching com o número e o prazo como instante. Esse prazo é a estimativa da plataforma: 24 horas contadas de quando ela viu a importação começar. O relógio da Meta começa um pouco antes, então trate o instante como limite e aja com folga. Passado o prazo sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. O aviso sai antes do prazo e nunca depois, e é um só para o número: como toda entrega, ele pode se repetir, sempre com o mesmo event_id, e um número que refaz a conexão depois não recebe um segundo aviso. O estado corrente da importação está em GET /v1/numbers/{id}/health."}}}}}},"get":{"summary":"Listar os endpoints de webhook","tags":["Webhooks"],"description":"Os endpoints cadastrados neste cliente, do mais recente para o mais antigo.\n\nO segredo de assinatura NÃO vem aqui, e não vem de rota nenhuma. Ele sai uma única vez, na resposta do cadastro e na da rotação: a Luna guarda apenas a forma cifrada dele. Se você o perdeu, rotacione para receber um novo: não existe caminho de recuperação, e isso é desenho.\n\nUse esta rota para descobrir se já existe endpoint antes de cadastrar um. Cliente sem nenhum endpoint recebe 200 com lista vazia, e não 404.\n\nquando previousSecretExpiresAt estiver no futuro, aquele endpoint está no meio de uma rotação: as entregas carregam duas assinaturas, e as duas são válidas até aquele instante.","responses":{"200":{"description":"Os endpoints cadastrados neste cliente, do mais recente para o mais antigo.\n\nO segredo de assinatura NÃO vem aqui, e não vem de rota nenhuma: ele sai uma única vez, na resposta do cadastro e na da rotação. A Luna guarda apenas a forma cifrada dele. Se você o perdeu, rotacione para receber um novo: não existe caminho de recuperação.\n\nA resposta não traz total nem contagem, de propósito: nenhum número desta API alimenta cobrança.\n\nLista vazia com 200 é a resposta de quem ainda não cadastrou nenhum endpoint, e não um erro.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador do endpoint, devolvido no cadastro."},"url":{"type":"string","maxLength":2048,"description":"A URL do seu servidor que vai receber os eventos. Precisa ser https e precisa resolver para um endereço público."},"createdAt":{"type":"string","description":"Quando o endpoint foi cadastrado, em UTC."},"updatedAt":{"type":"string","description":"Quando ele foi alterado pela última vez, em UTC."},"previousSecretExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Enquanto este instante estiver no futuro, as entregas carregam duas assinaturas: a do segredo novo e a do anterior. Depois dele, só a do novo."}},"required":["id","url","createdAt","updatedAt","previousSecretExpiresAt"],"additionalProperties":false,"description":"A configuração do endpoint. O segredo NÃO vem aqui — ele sai uma vez, no cadastro e na rotação.\n\nA entrega é at-least-once: a mesma entrega pode chegar mais de uma vez, e isso acontece de verdade, não apenas em teoria. Um tempo limite atingido depois de o seu servidor já ter processado a requisição produz exatamente esse caso.\n\nA ordem é garantida dentro de uma conversa. Ela não é garantida entre conversas: dois eventos de conversas diferentes podem chegar em qualquer ordem, e um evento de uma conversa lenta pode chegar depois de um evento mais recente de outra.\n\nA idempotência é dever seu, e o event_id é o que a plataforma dá para você cumpri-la. Ele é estável: a mesma entrega, repetida, traz o mesmo event_id. Guarde os que já processou e descarte os repetidos.\n\nSe o seu endpoint ficar fora do ar por muito tempo, a plataforma descarta o que ficou acumulado, e você perde esses eventos: eles não serão reenviados. Quando isso acontecer, você recebe um evento backlog.dropped dizendo quantos foram descartados e de que período. Esse aviso não é descartado junto com o acumulado que ele descreve.\n\nNovos tipos de evento podem aparecer sem que a versão do contrato mude. Se o seu servidor receber um event_type que não conhece, responda 2xx e ignore o evento. A versão só muda quando a forma de um tipo que já existe muda, e isso vem com aviso e prazo.\n\nNum número conectado em coexistência, se a importação das conversas anteriores ainda estiver em curso quando o prazo dela se aproxima, você recebe um evento history_sync.deadline_approaching com o número e o prazo como instante. Esse prazo é a estimativa da plataforma: 24 horas contadas de quando ela viu a importação começar. O relógio da Meta começa um pouco antes, então trate o instante como limite e aja com folga. Passado o prazo sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. O aviso sai antes do prazo e nunca depois, e é um só para o número: como toda entrega, ele pode se repetir, sempre com o mesmo event_id, e um número que refaz a conexão depois não recebe um segundo aviso. O estado corrente da importação está em GET /v1/numbers/{id}/health."}}},"required":["data"],"additionalProperties":false,"description":"Os endpoints cadastrados neste cliente, do mais recente para o mais antigo.\n\nO segredo de assinatura NÃO vem aqui, e não vem de rota nenhuma: ele sai uma única vez, na resposta do cadastro e na da rotação. A Luna guarda apenas a forma cifrada dele. Se você o perdeu, rotacione para receber um novo: não existe caminho de recuperação.\n\nA resposta não traz total nem contagem, de propósito: nenhum número desta API alimenta cobrança.\n\nLista vazia com 200 é a resposta de quem ainda não cadastrou nenhum endpoint, e não um erro."}}}}}}},"/v1/webhooks/endpoints/{id}":{"get":{"summary":"Consultar o endpoint de webhook","tags":["Webhooks"],"description":"A configuração do endpoint.\n\nO segredo não vem aqui, e não vem de rota nenhuma. A Luna guarda apenas a forma cifrada dele.\n\nquando previousSecretExpiresAt estiver no futuro, o endpoint está no meio de uma rotação: as entregas carregam duas assinaturas, e as duas são válidas até aquele instante.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true,"description":"O identificador do endpoint, devolvido no cadastro."}],"responses":{"200":{"description":"A configuração do endpoint. O segredo NÃO vem aqui — ele sai uma vez, no cadastro e na rotação.\n\nA entrega é at-least-once: a mesma entrega pode chegar mais de uma vez, e isso acontece de verdade, não apenas em teoria. Um tempo limite atingido depois de o seu servidor já ter processado a requisição produz exatamente esse caso.\n\nA ordem é garantida dentro de uma conversa. Ela não é garantida entre conversas: dois eventos de conversas diferentes podem chegar em qualquer ordem, e um evento de uma conversa lenta pode chegar depois de um evento mais recente de outra.\n\nA idempotência é dever seu, e o event_id é o que a plataforma dá para você cumpri-la. Ele é estável: a mesma entrega, repetida, traz o mesmo event_id. Guarde os que já processou e descarte os repetidos.\n\nSe o seu endpoint ficar fora do ar por muito tempo, a plataforma descarta o que ficou acumulado, e você perde esses eventos: eles não serão reenviados. Quando isso acontecer, você recebe um evento backlog.dropped dizendo quantos foram descartados e de que período. Esse aviso não é descartado junto com o acumulado que ele descreve.\n\nNovos tipos de evento podem aparecer sem que a versão do contrato mude. Se o seu servidor receber um event_type que não conhece, responda 2xx e ignore o evento. A versão só muda quando a forma de um tipo que já existe muda, e isso vem com aviso e prazo.\n\nNum número conectado em coexistência, se a importação das conversas anteriores ainda estiver em curso quando o prazo dela se aproxima, você recebe um evento history_sync.deadline_approaching com o número e o prazo como instante. Esse prazo é a estimativa da plataforma: 24 horas contadas de quando ela viu a importação começar. O relógio da Meta começa um pouco antes, então trate o instante como limite e aja com folga. Passado o prazo sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. O aviso sai antes do prazo e nunca depois, e é um só para o número: como toda entrega, ele pode se repetir, sempre com o mesmo event_id, e um número que refaz a conexão depois não recebe um segundo aviso. O estado corrente da importação está em GET /v1/numbers/{id}/health.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador do endpoint, devolvido no cadastro."},"url":{"type":"string","maxLength":2048,"description":"A URL do seu servidor que vai receber os eventos. Precisa ser https e precisa resolver para um endereço público."},"createdAt":{"type":"string","description":"Quando o endpoint foi cadastrado, em UTC."},"updatedAt":{"type":"string","description":"Quando ele foi alterado pela última vez, em UTC."},"previousSecretExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Enquanto este instante estiver no futuro, as entregas carregam duas assinaturas: a do segredo novo e a do anterior. Depois dele, só a do novo."}},"required":["id","url","createdAt","updatedAt","previousSecretExpiresAt"],"additionalProperties":false,"description":"A configuração do endpoint. O segredo NÃO vem aqui — ele sai uma vez, no cadastro e na rotação.\n\nA entrega é at-least-once: a mesma entrega pode chegar mais de uma vez, e isso acontece de verdade, não apenas em teoria. Um tempo limite atingido depois de o seu servidor já ter processado a requisição produz exatamente esse caso.\n\nA ordem é garantida dentro de uma conversa. Ela não é garantida entre conversas: dois eventos de conversas diferentes podem chegar em qualquer ordem, e um evento de uma conversa lenta pode chegar depois de um evento mais recente de outra.\n\nA idempotência é dever seu, e o event_id é o que a plataforma dá para você cumpri-la. Ele é estável: a mesma entrega, repetida, traz o mesmo event_id. Guarde os que já processou e descarte os repetidos.\n\nSe o seu endpoint ficar fora do ar por muito tempo, a plataforma descarta o que ficou acumulado, e você perde esses eventos: eles não serão reenviados. Quando isso acontecer, você recebe um evento backlog.dropped dizendo quantos foram descartados e de que período. Esse aviso não é descartado junto com o acumulado que ele descreve.\n\nNovos tipos de evento podem aparecer sem que a versão do contrato mude. Se o seu servidor receber um event_type que não conhece, responda 2xx e ignore o evento. A versão só muda quando a forma de um tipo que já existe muda, e isso vem com aviso e prazo.\n\nNum número conectado em coexistência, se a importação das conversas anteriores ainda estiver em curso quando o prazo dela se aproxima, você recebe um evento history_sync.deadline_approaching com o número e o prazo como instante. Esse prazo é a estimativa da plataforma: 24 horas contadas de quando ela viu a importação começar. O relógio da Meta começa um pouco antes, então trate o instante como limite e aja com folga. Passado o prazo sem concluir, a plataforma da Meta desconecta o negócio final e ele precisa refazer a conexão no aparelho. O aviso sai antes do prazo e nunca depois, e é um só para o número: como toda entrega, ele pode se repetir, sempre com o mesmo event_id, e um número que refaz a conexão depois não recebe um segundo aviso. O estado corrente da importação está em GET /v1/numbers/{id}/health."}}}}}}},"/v1/webhooks/endpoints/{id}/rotacionar":{"post":{"summary":"Rotacionar o segredo de assinatura","tags":["Webhooks"],"description":"Emite um segredo novo e devolve ele uma vez. Guarde-o agora, como no cadastro.\n\nO segredo anterior continua produzindo assinatura válida por sete dias, e durante essa janela cada entrega carrega as duas assinaturas no mesmo cabeçalho. Isso existe para que rotacionar não derrube a sua integração: você troca a configuração do seu lado quando puder, dentro da janela, sem perder entrega.\n\nO instante exato em que o anterior deixa de valer vem em previousSecretExpiresAt.\n\nRotacionar de novo antes do fim da janela descarta o mais antigo dos dois: em nenhum momento existem três segredos válidos.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true,"description":"O identificador do endpoint, devolvido no cadastro."}],"responses":{"200":{"description":"O endpoint com o segredo novo. O anterior continua válido até previousSecretExpiresAt, então você pode trocar a configuração do seu lado sem pressa e sem perder entrega.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador do endpoint, devolvido no cadastro."},"url":{"type":"string","maxLength":2048,"description":"A URL do seu servidor que vai receber os eventos. Precisa ser https e precisa resolver para um endereço público."},"createdAt":{"type":"string","description":"Quando o endpoint foi cadastrado, em UTC."},"updatedAt":{"type":"string","description":"Quando ele foi alterado pela última vez, em UTC."},"previousSecretExpiresAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Enquanto este instante estiver no futuro, as entregas carregam duas assinaturas: a do segredo novo e a do anterior. Depois dele, só a do novo."},"secret":{"type":"string","description":"O segredo que assina as entregas para este endpoint. Ele aparece nesta resposta e em mais nenhuma: guarde-o agora. A Luna guarda apenas a forma cifrada dele, e nenhuma rota o devolve. Se você o perder, rotacione para receber um novo."}},"required":["id","url","createdAt","updatedAt","previousSecretExpiresAt","secret"],"additionalProperties":false,"description":"O endpoint com o segredo novo. O anterior continua válido até previousSecretExpiresAt, então você pode trocar a configuração do seu lado sem pressa e sem perder entrega."}}}}}}},"/v1/webhooks/dlq":{"get":{"summary":"Listar as entregas que falharam definitivamente","tags":["Webhooks"],"description":"Os eventos que a Luna tentou entregar ao seu endpoint e desistiu, do mais recente ao mais antigo.\n\nUm evento chega aqui depois de a política de retentativa ter se esgotado: recuo exponencial, disjuntor e drenagem progressiva já tendo sido aplicados. Ele não é entregue de novo sozinho: ou você o re-enfileira, ou ele permanece aqui até a retenção expirar.\n\nA resposta não traz total nem contagem, de propósito: a Luna não conta entregas, e nenhum número desta API alimenta cobrança. Para saber quantos itens há, pagine até nextCursor vir null.\n\nO corpo do evento não vem aqui. Re-enfileire para recebê-lo pelo seu endpoint, assinado como qualquer outra entrega.","parameters":[{"schema":{"type":"string"},"in":"query","name":"cursor","required":false,"description":"O cursor devolvido em nextCursor pela chamada anterior. Omita na primeira página."},{"schema":{"default":50,"type":"integer","minimum":1,"maximum":100},"in":"query","name":"limit","required":false,"description":"Quantos itens trazer nesta página. Máximo 100, igual para todos."}],"responses":{"200":{"description":"Uma página da fila de rejeitados. Sem total e sem contagem: a Luna não conta entregas, e nenhum número desta resposta alimenta cobrança.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador deste item na fila de rejeitados."},"endpointId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O endpoint para o qual a entrega falhou."},"eventType":{"type":"string","enum":["message.received","message.status"],"description":"O tipo do evento que não foi entregue."},"wamid":{"type":"string","description":"O identificador da mensagem na Meta, o mesmo que o evento carrega."},"rejectedAt":{"type":"string","description":"Quando a plataforma desistiu de tentar entregar este evento, em UTC."},"failureReason":{"type":"string","enum":["http_4xx","http_5xx","timeout","conexao","destino_recusado"],"description":"A classe da última falha. http_4xx é o seu servidor tendo entendido e recusado; http_5xx é ele tendo aceitado e quebrado; timeout é ele não ter respondido a tempo; conexao é não ter sido possível estabelecer a conexão; destino_recusado é a URL ter deixado de apontar para um endereço público."},"requeuedAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Quando este item foi re-enfileirado, ou null se ainda não foi. Pedir o re-enfileiramento de um item que já tem este campo preenchido responde 409, e não republica de novo."}},"required":["id","endpointId","eventType","wamid","rejectedAt","failureReason","requeuedAt"],"additionalProperties":false,"description":"Um evento que a plataforma tentou entregar e desistiu."},"description":"Os itens desta página, do mais recente ao mais antigo."},"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Passe este valor em cursor para obter a página seguinte. null significa que esta é a última página. Não existe total: para saber quantos itens há, pagine até o fim."}},"required":["items","nextCursor"],"additionalProperties":false,"description":"Uma página da fila de rejeitados. Sem total e sem contagem: a Luna não conta entregas, e nenhum número desta resposta alimenta cobrança."}}}}}}},"/v1/webhooks/dlq/{id}/reenfileirar":{"post":{"summary":"Re-enfileirar uma entrega que falhou","tags":["Webhooks"],"description":"Recoloca o evento na fila de entrega. A resposta é 202: o pedido foi registrado, e a entrega acontece de forma assíncrona.\n\nRepetir a chamada para o mesmo item responde 409 com o instante do primeiro pedido, e não republica de novo. Isso existe para que dois cliques, ou uma retentativa sua, não produzam duas entregas do mesmo evento.\n\nRe-enfileirar não fura a fila nem pula as defesas: se o seu endpoint ainda estiver fora do ar, a entrega será adiada e retentada pela política de sempre, sem que nenhuma requisição chegue a sair. Não é necessário esperar o seu servidor voltar para pedir, mas também não adianta pedir em massa para acelerar.\n\nItem que não é seu responde 404, igual a item que não existe.","parameters":[{"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"in":"path","name":"id","required":true,"description":"O identificador deste item na fila de rejeitados."}],"responses":{"202":{"description":"O item foi recolocado na fila de entrega. A entrega em si é assíncrona, como qualquer outra: se o seu endpoint ainda estiver fora do ar, ela será adiada e retentada pela mesma política de sempre.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$","description":"O identificador deste item na fila de rejeitados."},"requeuedAt":{"type":"string","description":"O instante em que o re-enfileiramento foi registrado, em UTC."}},"required":["id","requeuedAt"],"additionalProperties":false,"description":"O item foi recolocado na fila de entrega. A entrega em si é assíncrona, como qualquer outra: se o seu endpoint ainda estiver fora do ar, ela será adiada e retentada pela mesma política de sempre."}}}}}}}},"servers":[{"url":"https://api.lunahia.com.br","description":"A API pública da Luna"}],"security":[{"apiKey":[]}],"tags":[{"name":"Onboarding","description":"Conectar o WhatsApp de um cliente final. O fluxo abre uma sessão, o cliente autoriza na janela da Meta, e a conclusão troca o código por acesso permanente.\n\nMigrar um número que já está na API de outro provedor é caminho suportado, e não exige número novo nem perda do número atual. Um número já registrado na Cloud API de outro provedor quase sempre tem verificação em duas etapas configurada lá, e a Meta exige o PIN existente para registrá-lo de novo: não há endpoint para ler nem para desligar esse PIN, então ele precisa vir do negócio final. Informe o PIN em POST /v1/numbers/{id}/pin, ou peça ao negócio final que desligue a verificação em duas etapas no WhatsApp Manager dele. Os outros pré-requisitos também são do lado dele: ser o dono da conta de WhatsApp Business e ter o número liberado no provedor anterior.\n\nUm número que hoje só existe no WhatsApp Business App, o aplicativo de celular, é o outro caso, e a Meta o chama de coexistência. Ele nunca passou por API nenhuma. A conexão parte da mesma janela da Meta, e a opção só aparece quando a conta do negócio final está habilitada para isso do lado da Meta e o aplicativo no aparelho está na versão 2.24.17 ou maior.\n\nConectado assim, o número continua funcionando no aplicativo de celular, e as conversas anteriores do aparelho são importadas para a plataforma: elas aparecem em GET /v1/messages junto das novas. A importação tem prazo para concluir, e quem diz onde ela está é GET /v1/numbers/{id}, nos campos sincronizacaoDoHistorico e sincronizacaoPrazo. O mesmo recurso traz tetoDeVazaoPorSegundo, que é o teto fixo de vazão de um número em coexistência e não sobe com o tempo. E o que o dono do negócio responder pelo aplicativo de celular também chega, na conversa do destinatário."},{"name":"Números","description":"Os números conectados: estado, qualidade, vazão e a saúde da conexão com a Meta."},{"name":"Mensagens","description":"Enviar e consultar mensagens. O caminho quente da plataforma."},{"name":"Templates","description":"Mensagens pré-aprovadas pela Meta, necessárias para iniciar conversa fora da janela de 24 horas.\n\nAprovado não é liberado para volume: a Meta aplica um ritmo próprio sobre os primeiros envios de um template novo, e não publica prazo nem mecânica. Escalone o primeiro disparo em vez de programá-lo para uma data, e acompanhe o estado do template.\n\nAcima de três botões, apenas dois aparecem na mensagem entregue. A Meta aceita o template com mais e entrega dois, então o arranjo precisa ser decidido antes de o fluxo ser desenhado."},{"name":"Chaves de API","description":"Criar, listar e revogar as credenciais que autenticam suas chamadas."},{"name":"Serviço","description":"Sonda de disponibilidade. Sem autenticação."},{"name":"Console","description":"Rotas que o console web da Luna usa para autenticar operadores. Você normalmente não precisa delas. Elas existem aqui porque o console é cliente desta mesma API, sem rota privilegiada."}]}