Saltar para o conteúdo principal
Os webhooks avisam você em tempo real, no seu próprio servidor, sempre que um pagamento muda de estado — sem precisar consultar a API em loop. A 1to1 envia um POST assinado para a URL que você registrar, com os detalhes do pagamento.
Estes webhooks são de saída (1to1 → seu servidor). Você não os chama: você os recebe. Cada entrega é assinada (padrão Standard Webhooks) para que você verifique que veio de nós e não foi alterada.

Configurar um endpoint

No dashboard, em Configurações → API e Conexões, adicione a URL do seu sistema e escolha os eventos que quer receber. Ao criar, você recebe um signing secret (whsec_…) mostrado uma única vez — guarde-o como um segredo: é a chave com que você verifica cada webhook. Você pode registrar até 10 endpoints por negócio.
A URL deve ser https:// e pública. Guarde o whsec_… em um lugar seguro do servidor (variável de ambiente, secret manager) — nunca em código cliente nem no repositório. Se perdê-lo, rotacione o secret pelo dashboard.

Catálogo de eventos

payment.failed cobre apenas os pagamentos manuais marcados como falhos pela sua equipe. As falhas de pagamentos online (cartão recusado, sessão expirada) não emitem webhook.

O payload

Cada POST leva um corpo JSON com esta forma. O exemplo é um pagamento manual creditado:
event_at é o momento em que o evento do pagamento ocorreu (para payment.failed, a hora da falha). É diferente do header webhook-timestamp, que é o instante de envio usado para a assinatura anti-replay.
Alguns campos são exclusivos de pagamentos manuais (provider: "manual"): bank_account (a etiqueta da conta em que o dinheiro entrou, ex. "BANORTE0876"), bank_datetime (data/hora no banco, opcional) e files (comprovantes enviados). Em pagamentos online ficam null / []. Em pagamentos manuais, transaction_id é o ID de banco capturado pelo operador ao creditar (null se não foi capturado); em pagamentos online é o id do provedor. Em todo pagamento online creditado receipt_url traz sempre uma URL do nosso domínio (https://…/pay/receipt/<uuid>), nunca o recibo hospedado do provedor; em pagamentos manuais é null. Essa página não exige autenticação e não tem vencimento por tempo (ao contrário dos links do provedor); deixa de servir se o pagamento for excluído ou deixar de estar creditado. O PDF do recibo é baixado adicionando o sufixo /pdf a essa mesma URL.
O payload cresce com o tempo: podemos adicionar campos novos ao webhook sem aviso prévio, e isso não conta como mudança incompatível. Os que já estão documentados aqui não são removidos nem renomeados sem anunciar antes, e uma mudança incompatível chega como um tipo de evento novo, nunca alterando um existente.Por isso seu parser deve ignorar o que não reconhecer —tanto chaves quanto tipos de evento— em vez de falhar: não configure additionalProperties: false na validação de schema, nem use structs que rejeitem campos desconhecidos. Uma entrega que seu endpoint rejeita é reenviada por ~3 dias e pode acabar desabilitando a assinatura.Isso se aplica ao shape do payload. Os limites de tamanho e o conteúdo dos campos de texto livre podem mudar: se você dimensionou seu armazenamento pelos topos acima, revise este guia antes de assumir que continuam os mesmos.

description e custom_fields: o contexto que sua equipe escreve

Dois campos que descrevem do que se trata o pagamento, vindos do que seu próprio negócio captura:
  • description é a nota do pagamento. Quem o registra a escreve no painel, ou seu AI employee a redige se você configurou um guia para isso. É prosa livre: null quando ninguém escreveu nada.
  • custom_fields são os campos personalizados que você definiu na sua conta —«Tipo de pagamento», «Pedido», os que precisar— com o valor capturado naquele pagamento. Enquanto o campo estiver em liberação por etapas a chave pode não vir; quando vem é sempre um objeto: {} se nenhum foi capturado.
As chaves de custom_fields são os nomes que você deu, literalmente, então renomear um campo na sua conta muda a chave nos pagamentos seguintes. Se for mapeá-los para o seu sistema, mapeie pelo nome que usa hoje e revise esse mapeamento ao renomear. Quando as duas chaves vêm, hoje quem as preenche é só o fluxo de pagamentos manuais: em pagamentos com cartão ou link chegam como null e {}.
Trate-os como texto de um humano, não confiável para lógica: escape-os antes de renderizá-los em HTML e não derive decisões automáticas da description nem dos valores de custom_fields. Podem trazer quebras de linha, acentos e emoji. A description é limitada a 2000 caracteres, cada valor de custom_fields a 500, o nome de cada campo a 100 e até 50 campos por pagamento, mas dimensione seu armazenamento por esses limites, não pelo que você vir nas primeiras entregas.
Disponibilidade: esses dois campos estão sendo habilitados por etapas no nível da plataforma, não conta por conta. Se você ainda não os vê nas suas entregas é porque ainda não foram publicados — fale com a gente e avisamos quando forem.

Se você recebe eventos por duas vias, deduplique por payment.uuid

Além destes webhooks, um AI employee do tipo JSON pode despachar o pagamento para uma URL sua como parte do que extrai da conversa. Se usar as duas vias, o mesmo pagamento vai chegar duas vezes, com o mesmo objeto payment em ambas. Deduplique por data.payment.uuid, que é estável entre as duas. Não use o folio: pagamentos com cartão e link nem sempre trazem um. Se você também usa um AI employee do tipo JSON que anexa o pagamento, o que recebe dentro do campo dele é este mesmo objeto data completo —payment, contact_info e conversation_info—, então o código que já escreveu para processá-lo serve igual. A única coisa que não leva é o envelope (id, type, event_at), porque esse descreve esta entrega em particular.
Ali conversation_info.is_tester pode chegar como true. Estes webhooks nunca lhe enviam pagamentos de teste, mas o AI employee do tipo JSON despacha sim a partir do Tester, para a URL que você configurou — é o que permite testar sua integração de ponta a ponta.Quando isso acontece, o pagamento existe de verdade no nosso sistema mas não representa dinheiro cobrado: é uma execução de teste da sua própria equipe. O folio dele sai da mesma série que os reais, então dentro do objeto payment nada os distingue.Esse campo reflete o domínio do pagamento, não o da conversa em que ele ocorreu: mesmo viajando dentro de conversation_info, quem o preenche é a linha do pagamento. É o correto para decidir se lançar a cobrança — e no caso raro em que os dois divirjam, quem manda para a sua contabilidade é o do pagamento.Se você reutiliza um único parser para as duas vias, ramifique por conversation_info.is_tester antes de lançar a cobrança. O corpo que o AI employee envia leva também is_test: true na raiz, mas isso está um nível acima do objeto data — se sua função recebe só o pacote do pagamento, não o vê.
Esse campo tem duas formas, e o AI employee escolhe qual conforme o que escreve nele.
  • Se não escreve nada ali, ou escreve um texto, você recebe o array direto: "pagamento": [ { "payment": …, "contact_info": …, "conversation_info": … } ]
  • Se escreve um objeto com a leitura dele, o pacote fica aninhado sob data: "pagamento": { "tipo": "entrada", "data": [ { "payment": … } ] }
A segunda forma existe para que o campo leve junto o que o AI entendeu e o dado duro do pagamento, em vez de reparti-los em duas chaves que você tenha que cruzar por nome.Aceite as duas: se o campo é um array, esses são os pagamentos; se é um objeto, estão na chave data dele. Um parser escrito só contra a primeira forma deixa de encontrar o pagamento no dia em que o AI escrever um objeto ali.Essa chave data não é o objeto data do webhook: é o array desses objetos. E a mesma regra vale para os campos de arquivos.
As duas vias se complementam, e por isso vale a pena ter ambas: este webhook cobre os desfechos —um pagamento que sua equipe credita dias depois, quando ninguém está conversando— e o do AI employee cobre o contexto do momento da conversa.
Disponibilidade: esta via está sendo habilitada por etapas no nível da plataforma, não conta por conta. Fale com a gente se quiser que avisemos quando for publicada.

O que seu endpoint responde pode continuar a conversa

Quando o AI employee do tipo JSON despacha para a sua URL —ou uma ação send_json, que envia a você uma requisição fixa sem que um modelo a redija—, o que seu servidor responder pode ser lido por outro funcionário AI para continuar escrevendo ao cliente, se a equipe do negócio configurou esse processamento: no funcionário JSON pelo painel, ou na própria ação. Não é automático: vem desligado. Uma vez configurado no funcionário JSON, não importa como ele foi disparado: pelo painel, como ação de outro funcionário AI, dentro de uma campanha ou por esta API (run-ai, ai-assistance). Isso não muda o contrato desta API: mesmos shapes, mesmos status, mesmos códigos de erro, e sua resposta continua sendo registrada como hoje. A única diferença é o que nós fazemos com ela. Ainda assim, há cinco coisas que vale saber antes de decidir o que você devolve:
  • O corpo da sua resposta é mostrado a um modelo. Responda só com aquilo que você aceita que o cliente venha a saber: nem credenciais, nem dados de outro cliente, nem dumps internos. Se for longo, é truncado. Antes de mostrá-lo ocultamos os valores dos campos com nome de credencial (token, password, api_key…), os números de 18 dígitos —mesmo separados por espaços ou hífens— e a parte /pay/… dos links, então se a sua resposta trouxer uma CLABE ou um link com /pay/…, o AI não vai conseguir repassá-los ao cliente. Esse filtro é uma rede de proteção, não uma garantia: ele não reconhece todos os nomes de campo nem os links de outras gateways, então não envie credenciais nem dados bancários contando com ele.
  • Só é lida se você responder 2xx e com corpo, e mesmo assim não é garantido. Um erro ou uma resposta vazia não continuam nada, e esse despacho não é repetido se você responder 4xx ou 5xx. Ele só se repete uma vez se você não responder em 30 segundos ou a conexão falhar, então pode chegar duas vezes e sem chave de idempotência: este despacho não leva webhook-id nem assinatura, então a deduplicação fica por sua conta. Os reenvios, o timeout e o webhook-id de Entrega e reenvios são apenas dos webhooks de eventos de pagamento. Continua, sim, mesmo que naquele momento uma pessoa esteja atendendo a conversa ou ela esteja pendente. Não há um prazo garantido entre a sua resposta e a mensagem ao cliente.
  • A cadeia tem um teto, e ele depende de onde saiu o despacho. Se a continuação despachar de novo para a sua URL, essa resposta também pode continuar a conversa, até o teto:
    • Até duas continuações quando o despacho sai do que abre o turno: o funcionário JSON que roda por conta própria —o atribuído, ou um disparado diretamente pelo painel, por uma campanha ou por esta API—, uma ação send_json avulsa em um grupo de ações, ou o action set do funcionário que responde a uma mensagem do cliente, a uma mensagem agendada ou ao botão «Executar» sobre o atribuído (com um send_json dele ou um funcionário JSON que esse set dispara).
    • Uma só quando despacha o action set de um funcionário que outro disparou: outro action set, esta API (run_ai, ai_assistance, assign_ai_employee com run: true ou Smart Actions), uma campanha, ou «Executar» sobre um funcionário que não é o atribuído. Também fica em uma o funcionário JSON que esse set dispara.
    O teto é por cadeia, não por conversa: cada mensagem do cliente a começa de novo.
  • Uma campanha despacha para você uma vez por destinatário. Se a equipe do negócio coloca um send_json ou um funcionário JSON em uma campanha, chega a você uma requisição por destinatário, e com o processamento configurado cada resposta pode continuar a sua conversa. Um send_json que vai antes da primeira ação de AI da campanha sai no ritmo do envio. Se vai depois, sai de uma fila que limita quantos começam ao mesmo tempo por negócio, mas não quantos continuam rodando: um que demora pode liberar o seu lugar antes de terminar. Não há um ritmo garantido. Dimensione o seu endpoint pelo tamanho da audiência, não pelo de uma conversa.
  • Não se aplica aos despachos que levam o campo do pagamento, mesmo que não traga nenhum pagamento (um array vazio, ou um objeto com data vazio). Ali a resposta não é lida, por design: um endpoint que ecoa a requisição devolveria dados bancários, e o corte não distingue um campo sem pagamentos de um com pagamentos.
Disponibilidade: está sendo habilitado por etapas no nível da plataforma, não conta a conta.

fail_reason: texto livre escrito por uma pessoa

Em payment.failed, fail_reason traz o motivo que sua equipe escreveu à mão ao marcar o pagamento como falho — por que não conseguiram verificá-lo. É prosa no idioma de quem escreveu, não um código de uma lista fechada: não o use como chave de uma tabela de traduções nem tente fazer parsing dele. O painel o exige antes de deixar marcar o pagamento, mas faça null-check mesmo assim: o endpoint ainda o aceita vazio enquanto durar a implantação desta função. Em qualquer outro evento é null. Como payment.failed cobre apenas pagamentos manuais (as falhas de gateway não emitem webhook), você nunca vai receber um código de provedor por este campo.
Trate-o como texto de um humano, não confiável para lógica: escape-o antes de renderizá-lo em HTML e não derive decisões automáticas do seu conteúdo. Pode trazer quebras de linha, acentos e emoji. Seu tamanho é limitado a 300 caracteres, mas dimensione seu armazenamento pelo limite, não pelo que você vir nas primeiras entregas.
conversation_info.uuid pode ser null se a conversa associada foi excluída — o pagamento notifica mesmo assim (o webhook é do ciclo de vida do pagamento, não da conversa). Sempre correlacione por data.payment.uuid.

Headers de cada entrega

string
Id único da entrega. Estável entre reenvios de um mesmo evento — use-o para deduplicar (ver Entrega).
integer
Instante de envio (segundos Unix). Recomputado a cada reenvio e faz parte da assinatura. Rejeite os que estiverem fora de uma janela razoável (±5 min) para se proteger de replays.
string
Uma ou mais assinaturas v1,<base64> separadas por espaço (haverá duas durante uma rotação de secret). A entrega é válida se alguma coincidir.
string
O tipo de evento (payment.credited, payment.failed, …). Coincide com type do corpo.
string
Sempre 1to1-Webhooks/1.0.

Verificar a assinatura

A assinatura é um HMAC-SHA256 do conteúdo {webhook-id}.{webhook-timestamp}.{body}, onde body é o corpo cru exato que você recebeu (não o re-serialize). A chave é seu whsec_… com o prefixo removido e o resto decodificado de base64.
Verifique sobre o corpo cru (raw body), antes de parsear o JSON. Re-serializar o objeto muda bytes (espaços, ordem das chaves) e quebra a assinatura.
A forma mais simples é a biblioteca oficial do Standard Webhooks, que cuida da rotação e da comparação em tempo constante por você:
Se preferir verificar na mão (sem dependências), replique o HMAC e compare em tempo constante contra cada assinatura do header:

Entrega e reenvios

1

Responda 2xx rápido

Responda com qualquer 2xx assim que receber o webhook. Se demorar mais de 10 segundos ou responder outro código, tratamos como falha e reenviamos.
2

Deduplique por webhook-id

A entrega é at-least-once: um mesmo evento pode chegar mais de uma vez (um reenvio após um timeout, por exemplo). O webhook-id é estável entre reenvios — guarde-o e descarte os repetidos.
3

Não assuma ordem

Os eventos não chegam garantidamente em ordem. Use data.payment.uuid + status como a verdade, não a ordem de chegada: um payment.registered que chegue atrasado não deve sobrescrever um payment.credited que você já processou.
4

Reenvios ~3 dias

Um endpoint fora do ar recebe reenvios com espaçamento crescente por ~3 dias. Se continuar falhando, a assinatura é desabilitada automaticamente (após 5 falhas consecutivas esgotadas) — você a reativa pelo dashboard.
5

Um aviso pode chegar atrasado

Se uma entrega não foi gerada por uma falha do nosso lado, podemos recuperá-la, normalmente em cerca de 15 minutos (até 3 dias depois do evento). O aviso recuperado traz em event_at a hora original do evento, não a de envio: use event_at para datar o pagamento, nunca a hora em que recebe o webhook. Ao reativar uma assinatura pausada ou desabilitada, ou ao adicionar um evento a ela, você pode receber de uma vez avisos de pagamentos de até 3 dias atrás. O objeto payment traz o estado do pagamento no momento do envio, não o do evento: um payment.registered recuperado pode chegar com status: "credited" ou "failed". Não é uma nova cobrança: como em «Não assuma ordem», use data.payment.uuid + status como a verdade.

Rotacionar o secret

Pelo dashboard você pode rotacionar o signing secret quando quiser. Durante um período de graça de 24 horas, cada webhook é assinado com o secret novo e o anterior ao mesmo tempo (duas assinaturas no header). Assim você atualiza seu sistema sem perder nem rejeitar eventos: valide contra qualquer uma das duas; quando termina o período de graça de 24 h, o secret anterior deixa de assinar automaticamente.

Comprovantes

Em um pagamento manual, files[] lista os comprovantes enviados com referências estáveis — uuid, name e mime_type — mas sem URL de download: o payload é um snapshot reenviado por dias, e uma URL assinada expiraria no caminho. Para baixar o arquivo, peça um link assinado com a mesma API key da sua integração — sem configuração adicional. O {paymentUuid} do path é data.payment.uuid, e o {fileUuid} é o uuid do comprovante dentro de data.payment.files[], ambos vindos do próprio evento:
A resposta traz a URL assinada e seu vencimento:
O download_url vence em 30 minutos (expires_at); se expirar antes de você usá-lo, repita o mesmo GET para obter um novo.
Trate-o como uma credencial temporária: vale 30 minutos, não o publique, não o reenvie, não o deixe em logs nem em tickets. É um link privado portador — quem o tiver baixa o comprovante sem precisar da sua API key — e revogar a key não invalida as URLs já emitidas.
O endpoint responde 404 em dois casos, ambos terminais — veja o detalhe no catálogo de erros: PAYMENT_NOT_FOUND se o paymentUuid não resolve para sua API key, e FILE_NOT_FOUND se o fileUuid não é um comprovante confirmado desse pagamento.

Próximos passos

Autenticação

Como sua integração se autentica com a API key.

Erros

O contrato de erros da API pública.