Skip to main content
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 completopayment, 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.

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.

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.