AtmosPay
Entrar

Webhooks

Eventos, payloads, cabeçalhos, assinatura (Node.js e PHP), tentativas, testes e reenvio.

1. Introdução

Webhooks são chamadas HTTP que o AtmosPay faz pra um endpoint da sua aplicação sempre que algo muda no ciclo de vida de uma venda (criação, aprovação, cancelamento, estorno, geração de PIX/boleto, falha de pagamento). É o caminho recomendado pra reagir a eventos do gateway em tempo real, sem polling.

Quando o AtmosPay dispara

  • Cliente concluiu o checkout (order.created).
  • Gateway confirmou pagamento (payment.approved).
  • Um produto da venda foi entregue: o principal e cada order bump, um evento por produto (produto.liberado).
  • PIX/Boleto foram emitidos (pix.generated, boleto.generated).
  • Cancelamento via gateway ou estorno manual (order.cancelled, order.refunded).
  • Cartão recusado / antifraude bloqueou (payment.failed).
  • Assinatura ativada após 1º pagamento (subscription.created) ou cancelada (subscription.cancelled).
  • Assinatura com fatura vencida: cron diário às 06:00 (horário de Brasília), a partir do dia seguinte ao vencimento (subscription.overdue).
  • Acesso de um produto encerrado, por cancelamento, inadimplência, expiração ou estorno (produto.revogado).
  • Fatura de renovação PIX emitida: cron diário às 08:00 (horário de Brasília), 10 dias antes do vencimento, ou quando o cliente/API adianta (invoice.created).
  • Fatura de renovação paga, por qualquer método, inclusive baixa manual (invoice.paid).
  • Fatura PIX pendente que vence em 3 dias ou hoje: cron diário às 09:00 (horário de Brasília) (invoice.due_soon, invoice.due_today).
  • Carrinho pendente há mais de 30min: cron a cada 30min (abandoned_cart.detected).
  • Participante validado na portaria de evento (event.checkin).

Como configurar

  1. Vá para Developers → Webhooks e clique em Novo webhook.
  2. Informe um nome, a URL HTTPS do seu endpoint e selecione os eventos desejados.
  3. O secret assina cada entrega (HMAC-SHA256, seção 2). Deixe o campo em branco e o AtmosPay gera um forte (whsec_...); depois de criar, copie em Secret.
  4. (Opcional) Restrinja o webhook a produtos específicos. Sem isso, vale para todos. Nos eventos da venda (order.created, pix.generated, boleto.generated, payment.approved, payment.failed, order.cancelled, order.refunded) o filtro casa com qualquer produto da venda: um webhook restrito ao produto de um order bump recebe os eventos da venda em que o bump saiu, da criação ao estorno (uma entrega por endpoint, mesmo com mais de um produto da venda no filtro). Em subscription.created, subscription.cancelled e subscription.overdue casa com qualquer produto que a assinatura libera (itens[]). produto.liberado e produto.revogado são roteados pelo produto_id do próprio evento; os invoice.*, pelo produto principal da assinatura.
  5. Use o botão Testar na lista pra disparar um payload de exemplo para sua URL antes de subir em produção.
Tentativas e reenvio
Entrega que não recebe 2xx em até 30 s é tentada de novo: 6 tentativas no total, a primeira na hora e as outras 1 min, 5 min, 30 min, 2 h e 6 h depois da anterior. Cada tentativa fica no histórico do webhook, e a que falhou pode ser reenviada pelo painel com o mesmo event_id. Detalhes na seção 5.

2. Autenticação e segurança

Toda entrega é um POST com o corpo em JSON e estes cabeçalhos:

User-AgentAtmosPay-Webhook/1.0 (nos testes: AtmosPay-Webhook/1.0 (test))
Content-Typeapplication/json
X-Webhook-EventNome do evento (ex: payment.approved)
X-Webhook-Event-IdO event_id do envelope. É o MESMO em todas as tentativas e no reenvio de uma entrega pelo painel: deduplique por ele, antes de ler o corpo
X-Webhook-TimestampO timestamp do envelope (ISO 8601, UTC): quando o evento foi emitido. Igual em todas as tentativas e no reenvio, e NÃO é assinado; para a janela de tempo, use o t da X-AtmosPay-Signature
X-Webhook-Delivery-IdUUID desta tentativa: muda a cada tentativa e a cada reenvio. É o id que aparece no histórico de entregas do painel
X-Webhook-AttemptNúmero da tentativa, a partir de 1 (o reenvio manual começa de novo em 1)
X-AtmosPay-Signaturet=<segundos unix>,v1=<hex>: HMAC-SHA256 com o secret sobre "<t>.<corpo bruto>". O t é o instante desta tentativa. Nas 24 h depois de uma troca de secret, dois v1 (o novo e o anterior). Só quando o webhook tem secret. Recomendada
X-Webhook-SignatureLegada: hex do HMAC-SHA256 com o secret sobre o corpo bruto, sem horário, sempre com o secret atual. Só quando o webhook tem secret
X-Webhook-Test1, só nos disparos do botão Testar (e do testar_webhook do MCP). O corpo traz test: true
X-Webhook-Resend1, só nos reenvios manuais. O corpo traz reenvio: true

De onde vem o secret

Cada webhook tem o seu. Ao criar em Developers → Webhooks, deixe o campo em branco e o AtmosPay gera um forte (whsec_ + 32 bytes aleatórios em base64url), ou digite o seu (8 a 100 caracteres). Depois, o botão Secret do webhook mostra o valor atual (Revelar), gera um para o webhook que ainda não tem ou troca o atual. Só o dono e os administradores da conta fazem isso, e cada ação fica registrada.

Webhook criado antes, sem secret, continua sem assinatura até alguém clicar em Gerar secret: nada muda sozinho nas entregas dele.

Troca do secret: a próxima entrega já sai assinada com o secret novo. Por 24 horas depois da troca, o X-AtmosPay-Signature leva dois v1: o do secret novo primeiro e o do anterior depois (t=<segundos>,v1=<novo>,v1=<anterior>); aceite se qualquer um bater com o secret que a sua aplicação tem. O X-Webhook-Signature (legado) leva só o do secret novo: quem confere a legada precisa trocar o secret na aplicação junto com a troca no painel.

Assinatura v1 (recomendada): X-AtmosPay-Signature

  1. Leia o corpo bruto (os bytes exatos), antes de qualquer parse do JSON.
  2. Separe o cabeçalho pelas vírgulas: t (segundos desde 1970, UTC) e v1 (64 caracteres hexadecimais).
  3. Recuse se a diferença entre o seu relógio e t passar de 5 minutos (300 s), para trás ou para a frente: é o que impede reproduzir um pedido capturado. Mantenha o relógio do servidor sincronizado (NTP).
  4. Calcule o HMAC-SHA256 com o secret sobre a string <t>.<corpo bruto> (o t, um ponto e o corpo) e compare com cada v1 do cabeçalho, em hexadecimal e em tempo constante: basta um bater (nas 24 horas depois de uma troca de secret vêm dois).
  5. Deduplique pelo event_id: a mesma emissão pode chegar mais de uma vez (seção 6).
Node.js
const crypto = require('crypto')

const TOLERANCIA_SEGUNDOS = 300 // 5 minutos

// cabecalho: "t=1767225600,v1=f151d8a7..."
function verificarAtmosPay(rawBody, cabecalho, secret) {
  let t = null
  const assinaturas = []
  for (const parte of String(cabecalho || '').split(',')) {
    const [chave, valor] = parte.split('=').map((s) => s.trim())
    if (chave === 't' && /^\d+$/.test(valor)) t = Number(valor)
    if (chave === 'v1' && /^[0-9a-f]{64}$/i.test(valor)) assinaturas.push(valor.toLowerCase())
  }
  if (t === null || assinaturas.length === 0) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCIA_SEGUNDOS) return false

  const esperado = crypto
    .createHmac('sha256', secret)
    .update(t + '.')
    .update(rawBody) // os bytes exatos recebidos (Buffer ou string)
    .digest()
  return assinaturas.some((v1) => {
    const recebido = Buffer.from(v1, 'hex')
    return recebido.length === esperado.length && crypto.timingSafeEqual(recebido, esperado)
  })
}

// Express: o corpo BRUTO, nunca o JSON já interpretado
app.post('/webhooks/atmospay', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verificarAtmosPay(req.body, req.get('X-AtmosPay-Signature'), process.env.ATMOSPAY_WEBHOOK_SECRET)
  if (!ok) return res.status(401).send('assinatura invalida')

  const evento = JSON.parse(req.body.toString('utf8'))
  // deduplique por evento.event_id (igual ao header X-Webhook-Event-Id) e processe em segundo plano
  return res.status(200).send('ok')
})
PHP
<?php
// O corpo BRUTO (os bytes exatos), antes do json_decode.
// No Laravel: $request->getContent() e $request->header('X-AtmosPay-Signature').
$corpo = file_get_contents('php://input');
$cabecalho = $_SERVER['HTTP_X_ATMOSPAY_SIGNATURE'] ?? '';
$secret = getenv('ATMOSPAY_WEBHOOK_SECRET');

function verificarAtmosPay(string $corpo, string $cabecalho, string $secret, int $tolerancia = 300): bool
{
    $t = null;
    $assinaturas = [];
    foreach (explode(',', $cabecalho) as $parte) {
        $par = explode('=', trim($parte), 2);
        if (count($par) !== 2) {
            continue;
        }
        if ($par[0] === 't' && ctype_digit($par[1])) {
            $t = (int) $par[1];
        } elseif ($par[0] === 'v1') {
            $assinaturas[] = strtolower($par[1]);
        }
    }
    if ($t === null || count($assinaturas) === 0) {
        return false;
    }
    if (abs(time() - $t) > $tolerancia) {
        return false; // fora dos 5 minutos: recuse (reprodução de um pedido antigo)
    }
    $esperado = hash_hmac('sha256', $t . '.' . $corpo, $secret);
    foreach ($assinaturas as $v1) {
        if (hash_equals($esperado, $v1)) { // comparação em tempo constante
            return true;
        }
    }
    return false;
}

if (!verificarAtmosPay($corpo, $cabecalho, $secret)) {
    http_response_code(401);
    exit('assinatura invalida');
}

$evento = json_decode($corpo, true);
// deduplique por $evento['event_id'] (igual a $_SERVER['HTTP_X_WEBHOOK_EVENT_ID']) e processe em segundo plano
http_response_code(200);
echo 'ok';

Assinatura legada: X-Webhook-Signature

Continua saindo em toda entrega com secret, exatamente como antes: o hexadecimal do HMAC-SHA256 com o secret sobre o corpo bruto. Ela não tem horário, então um pedido capturado continua válido: em integração nova, confira a v1. Quem já confere a legada não precisa mudar nada.

Node.js
const crypto = require('crypto')

function verificarLegada(rawBody, signatureHeader, secret) {
  const esperado = crypto
    .createHmac('sha256', secret)
    .update(rawBody)              // os bytes exatos do corpo
    .digest('hex')

  // Comparação em tempo constante evita timing attack
  const a = Buffer.from(esperado, 'hex')
  const b = Buffer.from(signatureHeader || '', 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

// verificarLegada(req.body, req.get('X-Webhook-Signature'), process.env.ATMOSPAY_WEBHOOK_SECRET)
PHP
<?php
$corpo = file_get_contents('php://input');
$recebida = strtolower($_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '');
$esperada = hash_hmac('sha256', $corpo, getenv('ATMOSPAY_WEBHOOK_SECRET'));

if (!hash_equals($esperada, $recebida)) {
    http_response_code(401);
    exit('assinatura invalida');
}

Vetor de teste

Confira a sua implementação com estes valores fixos antes do primeiro evento (o corpo é uma linha só, sem espaço nem quebra de linha no fim). Para testar a v1 com o t fixo, desligue a conferência dos 5 minutos ou fixe o relógio.

secret: whsec_teste_atmospay_0113
t:      1767225600
corpo:  {"evento":"payment.approved","event_id":"9f1c2b3a-4d5e-4f60-8a7b-0c1d2e3f4a5b","timestamp":"2026-01-01T00:00:00.000Z","data":{"venda_id":"5b6c7d8e-9f00-4a1b-8c2d-3e4f5a6b7c8d","valor":99.9}}

X-Webhook-Signature:  0a61faca6d256aa7a27ec76499459534481555bb09578c1de0145a0e6b1360d8
X-AtmosPay-Signature: t=1767225600,v1=f151d8a7113e1f2205774ae28143c58c4429d6755f90ae1a0d5bda9b69bbec15
Importante
As duas assinaturas são calculadas sobre o corpo bruto (raw body), antes de qualquer parse JSON. Use express.raw(), request.text() no Next.js, php://input no PHP ou o equivalente. Reserializar o JSON antes do HMAC quebra a comparação.
Sem secret não há assinatura nenhuma. Nesse caso, confirme o pedido pela API (GET /api/v1/orders/{id}) antes de liberar qualquer coisa, ou gere um secret no painel.

3. Estrutura do payload

Todo webhook segue o mesmo envelope:

{
  "evento": "string",              // ex: 'payment.approved'
  "event_id": "uuid",              // identifica ESTA emissão (tentativas e reenvio da entrega repetem o mesmo)
  "reenvio": true,                 // só presente no reenvio manual
  "timestamp": "ISO 8601",         // quando o evento foi emitido (igual em todas as tentativas)
  "data": {
    // ... campos específicos do evento (ver seção 4)
  }
}
eventostringNome canônico do evento (ver seção 4).
event_iduuidIdentidade desta emissão. As tentativas automáticas e o Reenviar de uma entrega no painel repetem o MESMO event_id: deduplique por ele. O reenvio do pagamento de uma venda (botão na venda, ou reenviar_webhook_venda no MCP) é outra emissão do mesmo fato: vem com event_id novo.
reenvioboolean (opcional)Presente como true apenas nos reenvios manuais (Reenviar de uma entrega, reenvio do pagamento de uma venda), junto com o cabeçalho X-Webhook-Resend: 1. data é idêntico ao da emissão original: decida se reprocessa.
timestampISO 8601Quando o evento foi emitido (UTC). O mesmo em todas as tentativas e no Reenviar de uma entrega, e igual ao X-Webhook-Timestamp (não confundir com pago_em do data).
dataobjectPayload específico: formato varia por evento.
testboolean (opcional)Presente como true apenas nos disparos do botão Testar/payload de exemplo.

4. Eventos disponíveis

Cada evento tem um payload próprio. Quando o evento envolve assinatura, o AtmosPay disponibiliza três cenários distintos: alterne nas abas para ver o JSON correspondente.

order.createdPedido criado

Disparado assim que o checkout ou a API cria a venda, em estado pendente, antes do pagamento. Sai também para o cartão recusado segundos depois. Não significa compra feita (para isso, purchase.approved). Não sai na renovação: as faturas de renovação nascem sem este evento.

Payload de exemplo
{
  "evento": "order.created",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "tipo_checkout": "produto_digital",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "forma_pagamento": "pix",
    "status": "pendente",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "criado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidIdentificador da venda no AtmosPay.
order_idstringIdentificador humano (ORD-xxxx).
organizacao_iduuidOrg dona do produto vendido.
assinatura_iduuid|nullPreenchido apenas em ofertas recorrentes.
tipostringunica (compra única) ou invoice (fatura de assinatura).
invoice_referenciastring|nullYYYY-MM do ciclo coberto, quando recorrente: o mês do ciclo (= proxima_cobranca), que é o do vencimento. A fatura PIX é emitida 10 dias antes, então pode ser o mês seguinte ao de criado_em.
valor_brutonumberValor da venda sem descontar taxas. Em REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
forma_pagamentostringpix | boleto | cartao.
statusstringSempre pendente nesse evento.
produtoobject{ id, nome, tipo } do produto comprado.
ofertaobject{ id, nome, recorrente, intervalo_cobranca }.
cliente_*stringnome, email, telefone, documento do cliente.
criado_emISO 8601Quando a venda foi criada.
order.cancelledPedido cancelado (não enviado hoje)

NÃO É ENVIADO HOJE: o único caminho que o disparava (a rota antiga de gateway) está desativado desde 17/09/2026. Recusa de cartão sai como payment.failed; PIX e boleto que expiram sem pagamento não geram evento (use abandoned_cart.detected); renovação recusada vira subscription.overdue e, no fim, subscription.cancelled.

Payload de exemplo
{
  "evento": "order.cancelled",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "tipo_checkout": "produto_digital",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "cancelado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidIdentificador da venda cancelada.
assinatura_iduuid|nullPreenchido se a venda pertencia a assinatura.
tipostringunica | invoice.
tipo_checkout / forma_pagamentostringOrigem e método da venda cancelada.
valor_bruto / valor_liquidonumberValores da venda. Em REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
cancelado_emISO 8601Momento do cancelamento.
produto / ofertaobjectDados contextuais do que foi cancelado.
cliente_*stringnome, email, telefone, documento do cliente afetado.
order.refundedPedido estornado

Disparado quando a venda é devolvida (inclusive uma fatura de renovação): estorno pelo painel (ou rota interna), reembolso aprovado, devolução feita direto no gateway (motivo gateway) e chargeback (motivo chargeback). No reembolso total de uma cobrança de assinatura, a assinatura também é cancelada e sai subscription.cancelled; o estorno parcial não cancela. Estorno parcial não emite.

Estorno parcial não emite este evento. O campo motivo não é uma lista fechada: chargeback e gateway são fixos; no estorno pelo painel vem o texto do operador (ou null) e no reembolso, o texto do pedido.
Entregue aos webhooks de qualquer produto da venda (principal ou order bump). No estorno pelo painel e no reembolso aprovado, o acesso do principal e de cada bump é revogado e sai um produto.revogado por produto, com venda_id. É por ele que se corta produto a produto. Na cobrança de assinatura, o acesso termina agora quando ela pagava o período atual; quando era de um período anterior, vai até o fim do período que outra cobrança paga, e o produto.revogado chega com encerrado_em no futuro.
Payload de exemplo
{
  "evento": "order.refunded",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "tipo_checkout": "produto_digital",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "motivo": "Solicitação do cliente",
    "estornado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidIdentificador da venda estornada.
assinatura_iduuid|nullQuando o estorno é de fatura de assinatura.
tipo / tipo_checkout / forma_pagamentostringContexto da venda estornada.
valor_bruto / valor_liquidonumberValores da venda. Em REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
motivostringJustificativa (chargeback quando via disputa no cartão).
estornado_emISO 8601Quando o estorno foi processado.
cliente_*stringnome, email, telefone, documento do cliente.
cobrancasarray (opcional)Só em venda paga com 2 cartões: uma por cartão, com { ordem, charge_id, bandeira, ultimos_digitos, valor (na mesma unidade de valor_bruto), parcelas, status }. Ausente nas outras vendas.
pix.generatedPIX gerado

Disparado quando uma venda nasce com PIX: checkout, link de pagamento e as rotas de criação da API v1 (POST /orders, /charges, /subscriptions, incluindo a primeira fatura da assinatura). Não dispara para fatura de renovação: a emissão automática, 10 dias antes do vencimento, não gera PIX, e o QR que o cliente gera na hora de pagar (link de cobrança, extrato da assinatura, POST /invoices/{id}/pix) também não emite este evento. Inclui o código copia-e-cola pronto para o cliente pagar.

O QR Code vale 1 hora (15 min em ingresso de evento). É a validade do PIX, não o vencimento de uma fatura de assinatura: essa segue pagável, com um QR novo, até vencer. Sua aplicação não precisa renderizar o QR: use qrcode direto em <img src={qrcode} />.
Payload de exemplo
{
  "evento": "pix.generated",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "tipo_checkout": "produto_digital",
    "forma_pagamento": "pix",
    "valor": 87,
    "valor_bruto": 87,
    "valor_liquido": 87,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "qrcode": "https://gateway.example.com/qrcode-fake.png",
    "copia_cola": "00020126570014BR.GOV.BCB.PIX0114+5511999999999...EXEMPLO",
    "expira_em": "2026-05-14T12:30:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda à qual a cobrança PIX pertence.
assinatura_iduuid|nullPreenchido quando a venda é a primeira fatura de uma assinatura.
valornumberValor cobrado. Em REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
qrcodestring (url)URL HTTPS da imagem PNG do QR Code.
copia_colastringBR Code (EMV) pra colar no app do banco.
expira_emISO 8601Validade do QR Code (1 hora; 15 min em ingresso de evento). Não é o vencimento de fatura de assinatura.
produto / ofertaobjectDados contextuais do que foi comprado.
cliente_*stringnome, email, telefone, documento do cliente.
boleto.generatedBoleto gerado

Disparado quando um boleto bancário é emitido. Inclui URL HTML do boleto, linha digitável e data de vencimento.

Payload de exemplo
{
  "evento": "boleto.generated",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "tipo_checkout": "produto_digital",
    "forma_pagamento": "boleto",
    "valor": 87,
    "valor_bruto": 87,
    "valor_liquido": 87,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "boleto_url": "https://gateway.example.com/boleto-fake.html",
    "boleto_barcode": "23791.23408 60000.000000 00000.000017 9 12340000008700",
    "boleto_vencimento": "2026-05-17T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda à qual o boleto pertence.
valornumberEm REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
boleto_urlstring (url)Página HTML do boleto pra impressão.
boleto_barcodestringLinha digitável (47 dígitos).
boleto_vencimentoISO 8601Data limite de pagamento.
produto / ofertaobjectDados contextuais do que foi comprado.
cliente_*stringnome, email, telefone, documento do cliente.
purchase.approvedCompra aprovada (nova compra)

O pagamento de uma compra NOVA: produto avulso ou a 1ª cobrança (contratação) de uma assinatura, inclusive quitada por baixa manual do vendedor (que não emite payment.approved). Nunca sai na renovação. É o evento certo para mensagem de boas-vindas ou de "compra aprovada" (WhatsApp, e-mail, CRM). O payload é o MESMO do payment.approved, com ocasiao = compra ou primeira_cobranca.

Criado em 30/09/2026. Antes, quem assinava payment.approvedpara avisar "compra aprovada" mandava a mensagem de novo a cada renovação mensal do cliente.
Marque este OU o payment.approved no mesmo endpoint: com os dois, cada compra nova chega duas vezes (uma por evento). Renovação paga: invoice.paid. Liberar/estender acesso todo mês: payment.approved ou produto.liberado.
Payload de exemplo
{
  "evento": "purchase.approved",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "valor_bruto": 114,
    "valor_liquido": 114,
    "forma_pagamento": "pix",
    "pago_em": "2026-05-14T12:00:00.000Z",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "assinatura": null,
    "order_bumps": [
      {
        "orderbump_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
        "valor": 27,
        "produto": {
          "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
          "nome": "Bônus: Templates Prontos",
          "tipo": "digital"
        },
        "oferta": {
          "id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
          "nome": "Oferta do Bônus",
          "recorrente": false,
          "intervalo_cobranca": null
        },
        "venda_id": null,
        "assinatura_id": null
      }
    ],
    "ocasiao": "compra"
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda que foi paga.
organizacao_iduuidOrg dona do produto vendido.
assinatura_iduuid|nullnull em compras únicas; UUID em recorrentes.
tipostringunica | invoice.
invoice_referenciastring|nullYYYY-MM do ciclo coberto: o mês do ciclo, não o do pagamento (em PIX ele pode chegar até 10 dias antes do ciclo).
valor_bruto / valor_liquidonumberValor bruto e líquido em reais. Já incluem os order bumps à vista.
pago_emISO 8601Confirmação do banco.
produto / ofertaobjectDados contextuais do que foi comprado (produto PRINCIPAL).
produto.redirect_urlstring|nullPágina de obrigado cadastrada no produto (aba Geral → URL de redirecionamento após pagamento), já com ?order_id=. É para lá que o checkout do AtmosPay leva o comprador. null quando o produto não tem redirect, e o campo inteiro não existe quando a venda não tem produto (doação, link de pagamento). Útil pra quem cobra pela API e precisa mandar o comprador ao mesmo lugar que o checkout mandaria.
cliente_*stringnome, email, telefone, documento do cliente.
order_bumpsarrayBumps aceitos na compra ([] quando não houve). Cada item traz orderbump_id, valor (reais), produto (com o redirect_url DELE, quando tem) e oferta. Libere o acesso de CADA um, além do principal.
order_bumps[].venda_id / assinatura_iduuid|nullPreenchidos só em bump de oferta recorrente, que vira venda/assinatura própria (e dispara o próprio payment.approved). null em bump à vista.
assinaturaobjectQuando recorrente: { status, proxima_cobranca, primeira_cobranca, ... }. proxima_cobranca já reflete este pagamento: use-a, e não pago_em + 1 intervalo, porque em renovação PIX o pagamento pode chegar até 10 dias antes do vencimento. Se ainda há mês em aberto (cliente devia mais de um), quem diz é cobranca.em_atraso na API.
assinatura.primeira_cobrancabooleantrue na primeira fatura da assinatura. Use pra liberar acesso.
ocasiaostringcompra (venda sem assinatura), primeira_cobranca (a contratação da assinatura) ou renovacao. É o jeito direto de não mandar boas-vindas de novo a cada mês.
external_idstringO seu id (conta, tenant, pedido), vindo de ?external_id= no checkout /p/ ou de metadata.external_id na criação pela API. Chave ausente quando não existe. Não vem nas faturas de renovação (use assinatura_id).
external_affiliate_idstringO afiliado de um sistema EXTERNO que indicou a venda, vindo de ?aff= (ou ?external_affiliate_id=) no checkout /p/ ou de metadata.external_affiliate_id na API. O AtmosPay só guarda e devolve, não paga comissão por ele: use para creditar a comissão ou criar o vínculo do seu lado. Chave ausente quando não existe. Não vem no payment.approved das renovações: para comissão de renovação use o invoice.paid, que traz o campo lido da assinatura (os subscription.* também trazem).
compradoraobject{ id } da organização compradora, vindo de ?org={uuid} no checkout /p/. Chave ausente quando não existe.
trackingobjectOrigem da venda: utm_source, utm_medium, utm_campaign, utm_content, utm_term, fbclid, gclid, ttclid, msclkid, ref, src, sck (os que vieram na URL do checkout /p/; a loja grava os mesmos). Chave ausente quando não houve nenhum.
payment.approvedPagamento aprovado (toda cobrança, inclusive renovações)

Todo pagamento confirmado pelo banco/gateway: a compra, a contratação da assinatura E cada renovação paga. O campo ocasiao diz qual (compra | primeira_cobranca | renovacao). Não sai na baixa manual (a renovação quitada assim emite só invoice.paid). Para mensagem de "compra aprovada", use purchase.approved. Este aqui repete a cada renovação.

Em primeira_cobranca, assinatura.primeira_cobranca vem como true: use isso pra criar registro do cliente no seu banco e enviar e-mail de boas-vindas. Em renovações vem false.
O que foi comprado NÃO está só em produto: se o cliente aceitou order bumps, cada produto extra vem em order_bumps[] com seu próprio produto.id: libere o acesso de todos eles. Se você integra por produto (e não por venda), prefira produto.liberado: ele sai um por produto, já roteado pelo produto certo.
Roteamento: um webhook restrito a produtos recebe este evento quando o produto principal OU qualquer order bump da venda está no filtro: o dono do bump recebe a venda em que o bump saiu. Uma entrega por endpoint, sempre. Todos os eventos da venda (order.created, pix.generated, order.cancelled, order.refunded…) são roteados assim.
Cobrando pela API? produto.redirect_url traz a página de obrigado do produto já com ?order_id=, o mesmo destino do checkout do AtmosPay. Bump com redirect próprio traz o dele em order_bumps[].produto.redirect_url.
Payload de exemplo
{
  "evento": "payment.approved",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "invoice_referencia": null,
    "valor_bruto": 114,
    "valor_liquido": 114,
    "forma_pagamento": "pix",
    "pago_em": "2026-05-14T12:00:00.000Z",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "assinatura": null,
    "order_bumps": [
      {
        "orderbump_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
        "valor": 27,
        "produto": {
          "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
          "nome": "Bônus: Templates Prontos",
          "tipo": "digital"
        },
        "oferta": {
          "id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
          "nome": "Oferta do Bônus",
          "recorrente": false,
          "intervalo_cobranca": null
        },
        "venda_id": null,
        "assinatura_id": null
      }
    ],
    "ocasiao": "compra"
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda que foi paga.
organizacao_iduuidOrg dona do produto vendido.
assinatura_iduuid|nullnull em compras únicas; UUID em recorrentes.
tipostringunica | invoice.
invoice_referenciastring|nullYYYY-MM do ciclo coberto: o mês do ciclo, não o do pagamento (em PIX ele pode chegar até 10 dias antes do ciclo).
valor_bruto / valor_liquidonumberValor bruto e líquido em reais. Já incluem os order bumps à vista.
pago_emISO 8601Confirmação do banco.
produto / ofertaobjectDados contextuais do que foi comprado (produto PRINCIPAL).
produto.redirect_urlstring|nullPágina de obrigado cadastrada no produto (aba Geral → URL de redirecionamento após pagamento), já com ?order_id=. É para lá que o checkout do AtmosPay leva o comprador. null quando o produto não tem redirect, e o campo inteiro não existe quando a venda não tem produto (doação, link de pagamento). Útil pra quem cobra pela API e precisa mandar o comprador ao mesmo lugar que o checkout mandaria.
cliente_*stringnome, email, telefone, documento do cliente.
order_bumpsarrayBumps aceitos na compra ([] quando não houve). Cada item traz orderbump_id, valor (reais), produto (com o redirect_url DELE, quando tem) e oferta. Libere o acesso de CADA um, além do principal.
order_bumps[].venda_id / assinatura_iduuid|nullPreenchidos só em bump de oferta recorrente, que vira venda/assinatura própria (e dispara o próprio payment.approved). null em bump à vista.
assinaturaobjectQuando recorrente: { status, proxima_cobranca, primeira_cobranca, ... }. proxima_cobranca já reflete este pagamento: use-a, e não pago_em + 1 intervalo, porque em renovação PIX o pagamento pode chegar até 10 dias antes do vencimento. Se ainda há mês em aberto (cliente devia mais de um), quem diz é cobranca.em_atraso na API.
assinatura.primeira_cobrancabooleantrue na primeira fatura da assinatura. Use pra liberar acesso.
ocasiaostringcompra (venda sem assinatura), primeira_cobranca (a contratação da assinatura) ou renovacao. É o jeito direto de não mandar boas-vindas de novo a cada mês.
external_idstringO seu id (conta, tenant, pedido), vindo de ?external_id= no checkout /p/ ou de metadata.external_id na criação pela API. Chave ausente quando não existe. Não vem nas faturas de renovação (use assinatura_id).
external_affiliate_idstringO afiliado de um sistema EXTERNO que indicou a venda, vindo de ?aff= (ou ?external_affiliate_id=) no checkout /p/ ou de metadata.external_affiliate_id na API. O AtmosPay só guarda e devolve, não paga comissão por ele: use para creditar a comissão ou criar o vínculo do seu lado. Chave ausente quando não existe. Não vem no payment.approved das renovações: para comissão de renovação use o invoice.paid, que traz o campo lido da assinatura (os subscription.* também trazem).
compradoraobject{ id } da organização compradora, vindo de ?org={uuid} no checkout /p/. Chave ausente quando não existe.
trackingobjectOrigem da venda: utm_source, utm_medium, utm_campaign, utm_content, utm_term, fbclid, gclid, ttclid, msclkid, ref, src, sck (os que vieram na URL do checkout /p/; a loja grava os mesmos). Chave ausente quando não houve nenhum.
produto.liberadoProduto liberado

Disparado uma vez POR ENTREGÁVEL da venda aprovada (o produto principal e cada order bump), no momento em que a entrega dele é concluída. Entregue aos webhooks daquele produto: um endpoint restrito ao produto do order bump recebe o evento do bump, sem precisar ler o payment.approved da venda inteira. Sai na compra única, na primeira cobrança e em cada renovação da assinatura (o acesso é renovado por ciclo).

payment.approved fala da VENDA (um evento, order_bumps[] dentro); produto.liberado fala de UM PRODUTO. É o par simétrico do produto.revogado: o que este libera, aquele corta. Case os dois por (produto_id, cliente.id, assinatura_id na assinatura ou venda_id na compra avulsa).
Só sai quando o comprador tem o que comprou: acesso.situacao liberado (acesso concedido aqui, pedido de envio criado) ou nada_a_liberar (download, e-book, link, área externa: o conteúdo já está na área do cliente e no e-mail; se a área de membros é sua, é a sua vez). Quando a entrega FALHA (pagou e não recebeu) o evento NÃO sai: o AtmosPay alerta a operação e reprocessa; o evento chega quando a entrega concluir.
Entrega única por (venda, produto): reprocessar uma venda já entregue não reemite. Como todo evento, um retry de rede repete o mesmo event_id.
Venda da loja (e-commerce), com carrinho e sem produto principal, NÃO emite este evento, mesmo quando um item do carrinho libera acesso digital. Para a loja, reaja ao payment.approved da venda.
Payload de exemplo
{
  "evento": "produto.liberado",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "produto_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "produto_nome": "Bônus: Templates Prontos",
    "oferta_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "order_bump",
    "ocasiao": "compra",
    "cliente": {
      "id": "33333333-3333-3333-3333-333333333333",
      "nome": "Cliente de Teste",
      "email": "teste@exemplo.com"
    },
    "acesso": {
      "situacao": "liberado",
      "validade": null,
      "vitalicio": true
    },
    "redirect_url": "https://seusite.com/obrigado-bonus?order_id=ORD-TEST-EXAMPLE",
    "liberado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
produto_iduuidO produto entregue. É por ele que o evento é roteado: um webhook restrito a este produto recebe só os dele; um sem filtro recebe todos os produtos de todas as vendas.
produto_nomestring|nullNome do produto no momento da entrega.
oferta_iduuid|nullOferta pela qual este produto foi vendido (a do principal, ou a do bump).
venda_id / order_iduuid / stringA venda em que o produto saiu: a MESMA do payment.approved, inclusive para o order bump (que não tem venda própria quando é à vista).
organizacao_iduuidOrg dona da venda.
assinatura_iduuid|nullQuando a venda é de assinatura (primeira cobrança ou renovação): é a mesma que virá em produto.revogado quando o acesso acabar. null em compra única.
tipostringprincipal (o produto da venda) ou order_bump (produto extra aceito no checkout).
ocasiaostringcompra, primeira_cobranca (contratação) ou renovacao. Este evento sai de novo em CADA renovação (o acesso é renovado por ciclo): use ocasiao para não repetir mensagem de boas-vindas.
clienteobject{ id, nome, email } de quem recebeu. id é o usuário do AtmosPay, o mesmo cliente_id do payment.approved.
acesso.situacaostringliberado: o AtmosPay concedeu algo ativamente (acesso à área de membros, pedido de envio de produto físico). nada_a_liberar: o produto não tem liberação ativa aqui (download, e-book, link, área externa). O conteúdo já está na área do cliente e no e-mail; se a área é SUA, é a sua vez de liberar.
acesso.validadeISO 8601|nullAté quando o acesso concedido vale (assinatura: um ciclo; produto com validade em dias: pagamento + N dias). null quando não há data para acabar, ou quando o acesso não é gerido aqui.
acesso.vitaliciobooleantrue quando o acesso não tem data para acabar. Pode vir false com validade preenchida mesmo em renovação: aí o acesso foi ESTENDIDO até a nova data.
redirect_urlstring|nullPágina de obrigado DESTE produto (aba Geral → URL de redirecionamento após pagamento), já com ?order_id=. É a mesma que o checkout usa. null quando o produto não tem redirect.
liberado_emISO 8601Quando a entrega foi concluída.
payment.failedPagamento falhou

Disparado quando o gateway recusa a cobrança (cartão recusado, sem saldo, expirado, antifraude). Não é disparado para PIX/boleto que apenas expiram sem pagamento.

Payload de exemplo
Compra única
{
  "evento": "payment.failed",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "valor": 87,
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "forma_pagamento": "cartao",
    "erro": "Pagamento recusado. Verifique os dados do cartão e tente novamente."
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda cujo pagamento falhou.
valornumberValor que seria cobrado. Em REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
forma_pagamentostringMétodo tentado (ex: cartao).
errostringMensagem amigável da falha (recusa, expirado, etc).
cliente_*stringnome, email, telefone, documento do cliente, para notificar/recuperar.
subscription.createdAssinatura criada

Disparado quando a 1ª cobrança da assinatura é aprovada e o status transita de pendente para ativa, uma vez por assinatura. Não sai em compra avulsa, na renovação nem na reativação. Atenção: hoje pode faltar em parte das assinaturas de cartão (nacionais e internacionais), e não sai para a assinatura de order bump mensal comprada junto de produto à vista. Para boas-vindas, prefira purchase.approved.

primeira_cobranca vem sempre true aqui. Para renovações subsequentes, use payment.approved em vez deste ou, na régua de cobrança (WhatsApp), invoice.paid. Compra única (não recorrente) não dispara esse evento.
Assinatura que libera mais de um produto (principal + order bump recorrente) traz todos em itens[] e é entregue também aos webhooks restritos aos produtos adicionais (uma entrega por endpoint, como em qualquer evento).
Payload de exemplo
Primeira cobrança
{
  "evento": "subscription.created",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "venda_inicial_id": "11111111-1111-1111-1111-111111111111",
    "status": "ativa",
    "intervalo": "mensal",
    "valor": 87,
    "proxima_cobranca": "2026-06-13T12:00:00.000Z",
    "primeira_cobranca": true,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "itens": [
      {
        "produto_id": "44444444-4444-4444-4444-444444444444",
        "produto_nome": "Produto de Teste",
        "oferta_id": "55555555-5555-5555-5555-555555555555",
        "oferta_nome": "Plano Mensal",
        "papel": "principal",
        "valor": 87,
        "status": "ativo",
        "encerra_em": null,
        "cancelado_em": null
      }
    ],
    "criada_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
assinatura_iduuidIdentificador da assinatura no AtmosPay.
organizacao_iduuidOrg dona do produto.
venda_inicial_iduuid|nullID da venda da 1ª cobrança que ativou a assinatura.
statusstringSempre ativa neste evento.
intervalostringmensal | trimestral | semestral | anual.
valornumberValor da recorrência em reais.
proxima_cobrancaISO 8601Data do próximo ciclo, e o vencimento da fatura desse ciclo. PIX: a fatura é emitida 10 dias antes desta data e o campo não muda na emissão; avança um intervalo no dia do ciclo, ou antes, se a fatura for paga antes. Cartão: data da próxima cobrança automática.
primeira_cobrancabooleanSempre true: é o canônico de ativação.
produto / oferta / cliente_*object/stringDados contextuais do produto PRINCIPAL.
external_idstringO metadata.external_id da assinatura, quando existir (ausente caso contrário).
external_affiliate_idstringO metadata.external_affiliate_id da assinatura: afiliado de sistema externo (?aff= no checkout ou metadata na API). Ausente quando não existe.
itensobject[]Os produtos que esta assinatura libera por ciclo, principal primeiro. Assinatura de um produto só traz um item, espelhando produto/oferta. Campo novo e aditivo: produto, oferta e valor continuam sendo os do principal.
itens[].papelstringprincipal (o produto da assinatura) ou order_bump (produto recorrente aceito junto, na mesma cobrança).
itens[].produto_id / produto_nome / oferta_id / oferta_nomeuuid/stringO que este item libera.
itens[].valornumberQuanto este item custa por ciclo, em reais. A soma dos itens ativos é o valor da assinatura.
itens[].statusstringativo ou cancelado. Segue o estado da assinatura: em subscription.cancelled nenhum item vem como ativo. Item cancelado continua na lista como histórico: filtre por ativo para saber o que vale agora.
itens[].encerra_emISO 8601|nullQuando o acesso deste item cai (fim do ciclo já pago). null enquanto ele está ativo, e também quando o item caiu junto com a assinatura sem data registrada: aí a data de corte de cada produto vem em produto.revogado.
itens[].cancelado_emISO 8601|nullQuando ESTE item foi cancelado à parte. null quando ele saiu junto com a assinatura.
subscription.cancelledAssinatura cancelada

Disparado a partir de QUALQUER fonte de cancelamento: cliente (app/portal cliente), admin (dashboard ou API v1), gateway (cancelamento confirmado pelo gateway) e o reembolso total de uma cobrança da assinatura (motivo admin quando o vendedor ou a AtmosPay reembolsou, gateway quando foi no painel do gateway). O campo motivo identifica a origem.

O cancelamento interrompe cobranças futuras mas não estorna o que já foi pago. Para estornos individuais use order.refunded. Fatura de um ciclo que ainda não começou (inclusive a PIX emitida 10 dias antes) é cancelada; fatura de ciclo já iniciado e não paga continua pagável: a dívida não some com o cancelamento.
Quando a assinatura libera mais de um produto, itens[] lista todos e o evento é entregue também aos webhooks restritos aos produtos adicionais (uma entrega por endpoint). Para cortar acesso produto a produto (com a data em que cada um acaba), use produto.revogado.
Payload de exemplo
Renovação
{
  "evento": "subscription.cancelled",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "status": "cancelada",
    "intervalo": "mensal",
    "valor": 87,
    "cancelada_em": "2026-05-14T12:00:00.000Z",
    "motivo": "cliente",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "itens": [
      {
        "produto_id": "44444444-4444-4444-4444-444444444444",
        "produto_nome": "Produto de Teste",
        "oferta_id": "55555555-5555-5555-5555-555555555555",
        "oferta_nome": "Plano Mensal",
        "papel": "principal",
        "valor": 87,
        "status": "cancelado",
        "encerra_em": null,
        "cancelado_em": null
      },
      {
        "produto_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
        "produto_nome": "Bônus: Templates Prontos",
        "oferta_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
        "oferta_nome": "Oferta do Bônus",
        "papel": "order_bump",
        "valor": 27,
        "status": "cancelado",
        "encerra_em": null,
        "cancelado_em": null
      }
    ]
  },
  "test": true
}
Campos relevantes
assinatura_iduuidAssinatura cancelada.
statusstringcancelada, ou expirada quando motivo = expiracao. Em cartão/boleto o gateway confirma depois e o evento pode chegar de novo (motivo gateway, status cancelada): deduplique por assinatura_id.
cancelada_emISO 8601Momento do cancelamento.
motivostringOrigem do cancelamento: cliente | admin | gateway | expiracao (cartão encerrado pelo cron por fatura vencida sem pagamento).
intervalostringIntervalo configurado (mensal etc).
valornumberValor da recorrência em reais.
produto / oferta / cliente_*object/stringDados contextuais do produto PRINCIPAL.
external_idstringO metadata.external_id da assinatura, quando existir (ausente caso contrário).
external_affiliate_idstringO metadata.external_affiliate_id da assinatura: afiliado de sistema externo (?aff= no checkout ou metadata na API). Ausente quando não existe.
itensobject[]Os produtos que esta assinatura libera por ciclo, principal primeiro. Assinatura de um produto só traz um item, espelhando produto/oferta. Campo novo e aditivo: produto, oferta e valor continuam sendo os do principal.
itens[].papelstringprincipal (o produto da assinatura) ou order_bump (produto recorrente aceito junto, na mesma cobrança).
itens[].produto_id / produto_nome / oferta_id / oferta_nomeuuid/stringO que este item libera.
itens[].valornumberQuanto este item custa por ciclo, em reais. A soma dos itens ativos é o valor da assinatura.
itens[].statusstringativo ou cancelado. Segue o estado da assinatura: em subscription.cancelled nenhum item vem como ativo. Item cancelado continua na lista como histórico: filtre por ativo para saber o que vale agora.
itens[].encerra_emISO 8601|nullQuando o acesso deste item cai (fim do ciclo já pago). null enquanto ele está ativo, e também quando o item caiu junto com a assinatura sem data registrada: aí a data de corte de cada produto vem em produto.revogado.
itens[].cancelado_emISO 8601|nullQuando ESTE item foi cancelado à parte. null quando ele saiu junto com a assinatura.
subscription.overdueAssinatura inadimplente

Disparado pelo cron das 06:00 (horário de Brasília) para cada assinatura ativa/pausada com fatura de renovação pendente cujo vencimento já passou, a partir do dia seguinte ao vencimento. No máximo uma vez a cada 24 h por assinatura (na prática, a cada 1 ou 2 dias): para régua D+1/D+2, use faturas_atrasadas[].dias_atraso, não a contagem de eventos. PIX e boleto; cartão recusado vai direto para a expiração. Inclui link público de cobrança (pode vir null no boleto) e o link do extrato.

Idempotência: marcamos assinaturas.ultimo_aviso_overdue_em ao disparar: uma assinatura recebe no máximo 1 webhook overdue por dia.
PIX: a assinatura não expira por atraso e segue ativa. Bloqueie o acesso desde o vencimento e libere de novo no payment.approved. Com 5 competências vencidas sem pagamento, a emissão de novas faturas pausa (não cancela) e volta sozinha quando o cliente quita.
Cartão: sem pagamento em 7 dias após o vencimento, o cron expirar-assinaturas encerra a assinatura com status="expirada" e dispara subscription.cancelled com motivo="expiracao".
Payload de exemplo
Renovação
{
  "evento": "subscription.overdue",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "status": "ativa",
    "intervalo": "mensal",
    "valor": 87,
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "faturas_atrasadas": [
      {
        "venda_id": "11111111-1111-1111-1111-111111111111",
        "order_id": "ORD-TEST-EXAMPLE",
        "invoice_referencia": "2026-05",
        "valor_bruto": 87,
        "forma_pagamento": "pix",
        "criado_em": "2026-04-24T12:00:00.000Z",
        "vencimento": "2026-05-04T12:00:00.000Z",
        "dias_atraso": 10
      }
    ],
    "total_atrasado": 87,
    "quantidade_faturas": 1,
    "link_cobranca": "https://dashboard.atmospay.com/cobranca/abcdef0123456789abcdef0123456789",
    "link_extrato": "https://dashboard.atmospay.com/assinatura/0123456789abcdef0123456789abcdef",
    "detectado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
assinatura_iduuidAssinatura com fatura vencida.
faturas_atrasadasobject[]Array com cada fatura vencida: { venda_id, order_id, invoice_referencia, valor_bruto, forma_pagamento, criado_em, vencimento, dias_atraso }.
faturas_atrasadas[].criado_emISO 8601Emissão da fatura. Em PIX ela sai 10 dias antes do vencimento. Não use para contar atraso.
faturas_atrasadas[].vencimentoISO 8601Vencimento: fim do dia no horário de Brasília (23:59:59 -03:00), em UTC. Converta para America/Sao_Paulo antes de exibir. Fatura sem vencimento gravado (anterior a esta regra) segue a regra antiga: emissão + 7 dias, ou o início do mês da competência se for depois.
faturas_atrasadas[].dias_atrasointegerDias civis (Brasília) desde o vencimento: 1 no dia seguinte a ele.
total_atrasadonumberSoma dos valores das faturas vencidas, em reais.
quantidade_faturasintegerQuantas faturas vencidas e não pagas a assinatura tem (fatura emitida e ainda no prazo não entra).
link_cobrancastring|nullURL pública (sem login) para o cliente pagar a fatura vencida mais antiga (PIX/QR Code/cópia-cola).
link_extratostring|nullURL pública (sem login) do extrato da assinatura: todas as faturas em aberto (as vencidas e a do ciclo atual) numa página só, com o PIX gerado na hora de pagar. Melhor para quem deve mais de um mês.
external_idstringO metadata.external_id da assinatura, quando existir (ausente caso contrário).
external_affiliate_idstringO metadata.external_affiliate_id da assinatura, quando existir (ausente caso contrário).
statusstringStatus atual da assinatura (ativa ou pausada).
detectado_emISO 8601Quando o cron diário detectou a inadimplência.
produto / oferta / cliente_*object/stringDados contextuais.
produto.revogadoAcesso a produto revogado

Disparado uma vez POR PRODUTO cujo acesso acabou: cancelamento de assinatura, encerramento por inadimplência, expiração ou estorno/reembolso. Entregue aos webhooks daquele produto: um endpoint restrito ao produto do order bump recebe o dele, mesmo quando a assinatura (ou a venda estornada) é do produto principal.

Uma assinatura pode liberar mais de um produto (principal + order bump recorrente): subscription.cancelled conta que a assinatura acabou, e produto.revogado diz, um a um, qual acesso cortar. O mesmo vale para a venda devolvida: order.refunded fala da venda, e este evento sai para o principal E para cada order bump dela (motivo = estorno, venda_id preenchido). É o par do produto.liberado da mesma venda.
encerrado_em pode estar no FUTURO: cancelar no meio de um ciclo já pago mantém o acesso até o fim dele. Corte no seu lado nessa data, não na chegada do evento. Quando vier null, o acesso também vale até o fim do ciclo já pago, só que a data não foi registrada: corte pelo fim do ciclo da assinatura, nunca na chegada do evento.
Não dispara quando NENHUM acesso precisou cair: o cliente continua com o conteúdo por OUTRO vínculo vivo (outra assinatura ativa, ou uma compra vitalícia que dá o mesmo conteúdo). Quando parte do produto fica de pé por esse motivo e o resto cai, o evento SAI e traz os conteúdos preservados em cursos_mantidos: confira esse campo antes de cortar o produto inteiro. E não dispara no bloqueio diário do PIX inadimplente: ali a assinatura segue viva e o acesso volta quando o cliente quita (acompanhe subscription.overdue).
Não há garantia de entrega única: um estorno reprocessado pode reemitir o mesmo evento. Trate pela chave de negócio da seção 6: (assinatura_id ?? venda_id) + evento + produto_id + cliente.id.
Payload de exemplo
Renovação
{
  "evento": "produto.revogado",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "produto_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "produto_nome": "Bônus: Templates Prontos",
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "venda_id": null,
    "cliente": {
      "id": "33333333-3333-3333-3333-333333333333",
      "nome": "Cliente de Teste",
      "email": "teste@exemplo.com"
    },
    "motivo": "cancelamento",
    "encerrado_em": "2026-05-26T12:00:00.000Z",
    "cursos_mantidos": []
  },
  "test": true
}
Campos relevantes
produto_iduuidO produto cujo acesso acabou. É por ele que o evento é roteado: um webhook restrito a este produto recebe; um sem filtro recebe todos.
produto_nomestring|nullNome do produto no momento da revogação.
assinatura_iduuid|nullA assinatura que acabou. null em estorno de compra avulsa, que não tem assinatura.
venda_iduuid|nullA venda devolvida, quando o motivo é estorno ou chargeback: a mesma do order.refunded e do produto.liberado desta venda (inclusive para o order bump, que não tem venda própria quando é à vista). null quando o acesso acabou pela assinatura (cancelamento, inadimplência, expiração).
clienteobject{ id, nome, email } de quem perdeu o acesso.
motivostringcancelamento (cliente, admin ou API) | inadimplencia (assinatura encerrada por falta de pagamento, o mesmo encerramento que manda subscription.cancelled com motivo=expiracao) | expiracao | estorno (estorno pelo painel, reembolso aprovado ou estorno feito direto no gateway) | chargeback (contestação no cartão: o dinheiro voltou para o comprador e o acesso cai junto). Em estorno e chargeback sai um evento por produto da venda: principal e order bumps.
encerrado_emISO 8601|nullQuando o acesso acaba, NÃO quando o cancelamento foi pedido. Cancelar no meio de um ciclo já pago mantém o acesso até o fim dele, e é essa data que vem aqui; pode estar no futuro. Em estorno e em inadimplência é o instante do disparo. null significa "o acesso vale até o fim do ciclo já pago e a data não foi registrada": corte pelo fim do ciclo da assinatura, nunca na chegada do evento.
cursos_mantidosstring[]Conteúdos DESTE produto que continuam liberados porque outro vínculo vivo do cliente (outra assinatura, uma compra vitalícia) também os dá. Vazio no caso normal; quando vem preenchido, a revogação foi PARCIAL: cortar o produto inteiro do seu lado tiraria o que o cliente continua pagando.
invoice.createdFatura gerada

Disparado quando uma fatura de renovação PIX é emitida: pelo cron diário às 08:00 (horário de Brasília), 10 dias antes do vencimento, ou antes disso a pedido do cliente (adiantar pelo portal) ou da API (POST /api/v1/subscriptions/{id}/generate-invoice). O campo origem diz qual dos três. Traz o telefone em E.164 e o link do extrato: é o primeiro aviso da régua de cobrança por WhatsApp.

Não dispara na 1ª cobrança da assinatura (para ela, use order.created / pix.generated), em cartão (cobrado automaticamente pelo gateway; acompanhe invoice.paid) nem em boleto. Sai no máximo uma vez por fatura: se o ciclo já tem fatura em aberto, adiantar de novo não reemite o evento.
Payload de exemplo
Renovação
{
  "evento": "invoice.created",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "chave_idempotencia": "invoice.created:11111111-1111-1111-1111-111111111111",
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "tipo": "invoice",
    "tipo_produto": "digital",
    "invoice_referencia": "2026-05",
    "status": "pendente",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "criado_em": "2026-05-14T12:00:00.000Z",
    "vencimento": "2026-05-25T02:59:59.000Z",
    "vencimento_data": "2026-05-24",
    "vencimento_formatado": "24/05/2026",
    "dias_para_vencer": 10,
    "pago_em": null,
    "origem": "cron",
    "link_extrato": "https://dashboard.atmospay.com/assinatura/0123456789abcdef0123456789abcdef",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "cliente_telefone_e164": "+5511999999999",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "assinatura": {
      "id": "22222222-2222-2222-2222-222222222222",
      "status": "ativa",
      "intervalo": "mensal",
      "valor": 87,
      "proxima_cobranca": "2026-05-25T02:59:59.000Z"
    },
    "vencidas_em_aberto": 0,
    "total_vencidas_em_aberto": 0
  },
  "test": true
}
Campos relevantes
chave_idempotenciastring<evento>:<venda_id> (ex.: invoice.due_today:1111…). Cada evento sai no máximo uma vez por fatura, então a chave é única por (evento, fatura): grave-a e descarte o que já existir. Um retry reentrega o mesmo payload.
venda_iduuidA fatura: uma venda com tipo = invoice.
order_idstringIdentificador humano (ORD-xxxx).
organizacao_iduuidOrg vendedora.
assinatura_iduuidAssinatura a que a fatura pertence.
external_idstringO metadata.external_id da ASSINATURA (a fatura de renovação não tem metadata própria). A chave vem ausente quando não existe, como nos subscription.*.
external_affiliate_idstringO metadata.external_affiliate_id da ASSINATURA: o afiliado de sistema externo, para comissão de renovação do seu lado. Ausente quando não existe.
tipo / tipo_produtostringtipo é sempre invoice. tipo_produto (digital, doacao, …) ajuda quando produto vem null (assinatura de doação ou de link).
invoice_referenciastring|nullCompetência YYYY-MM do ciclo coberto. Quase sempre é o mês de vencimento_data, mas pode diferir na virada do mês (a competência é contada em UTC) ou quando a fatura saiu atrasada e ganhou o prazo mínimo. null na venda-espelho da troca PIX→cartão.
statusstringpendente em created, due_soon e due_today; aprovado em paid.
forma_pagamentostringSempre pix em created e due_*. Em paid, o método que pagou: pix | cartao | boleto | …
valor_bruto / valor_liquidonumberEm REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
criado_emISO 8601Emissão da fatura (PIX: ~10 dias antes do vencimento). Não use para contar atraso nem como data de vencimento.
vencimentoISO 8601Instante do vencimento: 23:59:59 de Brasília, expresso em UTC. Convertido para UTC ele já cai no DIA SEGUINTE (02:59:59Z): quem formatar a data em UTC mostra um dia a mais. Para mensagem, use vencimento_data; para exibir o instante, converta para America/Sao_Paulo.
vencimento_dataYYYY-MM-DDDia civil do vencimento em Brasília, já convertido. É a data certa para mensagens e para comparar dias.
vencimento_formatadoDD/MM/AAAAO mesmo dia, pronto para o template de WhatsApp ("sua fatura vence em 18/09/2026").
dias_para_vencerintegerDias civis (Brasília) de hoje, no momento do disparo, até o vencimento: 3 em due_soon, 0 em due_today. Em created: 10 quando a origem é o cron (nunca menos que 7), e pode ser bem maior quando o cliente ou a API adiantam a fatura (origem cliente/api). Em paid fica negativo quando a fatura foi paga em atraso.
pago_emISO 8601|nullPreenchido só em invoice.paid.
origemstringQuem gerou o evento. created: cron (emissão automática) | cliente (adiantou pelo portal) | api (POST /api/v1/subscriptions/{id}/generate-invoice). paid: gateway | baixa_manual (pagamento registrado pelo vendedor). due_soon e due_today: cron.
link_extratostring|nullURL pública (sem login) do extrato da assinatura: o cliente vê esta fatura e as vencidas e gera o PIX na hora de pagar. O payload não traz código PIX porque um PIX vale só 1 hora: mande o link, não o código. null se o link não pôde ser gerado.
cliente_telefone_e164string|nullTelefone em E.164 (+5511999999999), pronto para WhatsApp. No wa.me/ use os dígitos sem o +. null quando não dá para garantir o código do país (ex.: número sem DDD); nesse caso, caia para cliente_telefone.
cliente_telefonestring|nullTelefone bruto, como o cliente digitou no checkout (sem normalização).
cliente_id / cliente_nome / cliente_email / cliente_documentostring|nullDados do cliente: o snapshot gravado na fatura primeiro, o cadastro como reserva.
produtoobject|null{ id, nome, tipo }. null em assinatura sem produto (doação, link de pagamento): veja tipo_produto.
ofertaobject|null{ id, nome, recorrente, intervalo_cobranca }.
assinaturaobject{ id, status, intervalo, valor, proxima_cobranca }, lido no momento do disparo. Em paid, proxima_cobranca já avançou por este pagamento, e status pode ser cancelada quando o cliente quita uma dívida de assinatura já encerrada.
vencidas_em_abertointegerQuantas OUTRAS faturas de renovação desta assinatura estão vencidas e sem pagamento (esta não conta). Use para mudar o tom da mensagem quando o cliente já deve meses anteriores.
total_vencidas_em_abertonumberSoma dessas outras faturas vencidas, em reais.
invoice.paidFatura paga

Disparado quando uma fatura de renovação é paga, por qualquer método: PIX pelo extrato ou pelo link de cobrança, cartão, e também pagamento registrado pelo vendedor (baixa manual, origem = baixa_manual). Vem com status = aprovado e pago_em preenchido.

payment.approved TAMBÉM dispara para a mesma renovação quando ela é paga pelo gateway (PIX, cartão); a baixa manual emite só invoice.paid. Numa régua de WhatsApp, assine um dos dois para o cliente não receber a confirmação em dobro: invoice.paid é o que cobre também a baixa manual.
Não dispara na 1ª cobrança: ela chega só como payment.approved (assinatura.primeira_cobranca = true) + subscription.created.
Quitar dívida de assinatura já cancelada também dispara: confira assinatura.statusantes de mandar uma mensagem de "renovação confirmada".
Payload de exemplo
Renovação
{
  "evento": "invoice.paid",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "chave_idempotencia": "invoice.paid:11111111-1111-1111-1111-111111111111",
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "tipo": "invoice",
    "tipo_produto": "digital",
    "invoice_referencia": "2026-05",
    "status": "aprovado",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "criado_em": "2026-05-06T12:00:00.000Z",
    "vencimento": "2026-05-17T02:59:59.000Z",
    "vencimento_data": "2026-05-16",
    "vencimento_formatado": "16/05/2026",
    "dias_para_vencer": 2,
    "pago_em": "2026-05-14T12:00:00.000Z",
    "origem": "gateway",
    "link_extrato": "https://dashboard.atmospay.com/assinatura/0123456789abcdef0123456789abcdef",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "cliente_telefone_e164": "+5511999999999",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "assinatura": {
      "id": "22222222-2222-2222-2222-222222222222",
      "status": "ativa",
      "intervalo": "mensal",
      "valor": 87,
      "proxima_cobranca": "2026-06-15T12:00:00.000Z"
    },
    "vencidas_em_aberto": 0,
    "total_vencidas_em_aberto": 0
  },
  "test": true
}
Campos relevantes
chave_idempotenciastring<evento>:<venda_id> (ex.: invoice.due_today:1111…). Cada evento sai no máximo uma vez por fatura, então a chave é única por (evento, fatura): grave-a e descarte o que já existir. Um retry reentrega o mesmo payload.
venda_iduuidA fatura: uma venda com tipo = invoice.
order_idstringIdentificador humano (ORD-xxxx).
organizacao_iduuidOrg vendedora.
assinatura_iduuidAssinatura a que a fatura pertence.
external_idstringO metadata.external_id da ASSINATURA (a fatura de renovação não tem metadata própria). A chave vem ausente quando não existe, como nos subscription.*.
external_affiliate_idstringO metadata.external_affiliate_id da ASSINATURA: o afiliado de sistema externo, para comissão de renovação do seu lado. Ausente quando não existe.
tipo / tipo_produtostringtipo é sempre invoice. tipo_produto (digital, doacao, …) ajuda quando produto vem null (assinatura de doação ou de link).
invoice_referenciastring|nullCompetência YYYY-MM do ciclo coberto. Quase sempre é o mês de vencimento_data, mas pode diferir na virada do mês (a competência é contada em UTC) ou quando a fatura saiu atrasada e ganhou o prazo mínimo. null na venda-espelho da troca PIX→cartão.
statusstringpendente em created, due_soon e due_today; aprovado em paid.
forma_pagamentostringSempre pix em created e due_*. Em paid, o método que pagou: pix | cartao | boleto | …
valor_bruto / valor_liquidonumberEm REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
criado_emISO 8601Emissão da fatura (PIX: ~10 dias antes do vencimento). Não use para contar atraso nem como data de vencimento.
vencimentoISO 8601Instante do vencimento: 23:59:59 de Brasília, expresso em UTC. Convertido para UTC ele já cai no DIA SEGUINTE (02:59:59Z): quem formatar a data em UTC mostra um dia a mais. Para mensagem, use vencimento_data; para exibir o instante, converta para America/Sao_Paulo.
vencimento_dataYYYY-MM-DDDia civil do vencimento em Brasília, já convertido. É a data certa para mensagens e para comparar dias.
vencimento_formatadoDD/MM/AAAAO mesmo dia, pronto para o template de WhatsApp ("sua fatura vence em 18/09/2026").
dias_para_vencerintegerDias civis (Brasília) de hoje, no momento do disparo, até o vencimento: 3 em due_soon, 0 em due_today. Em created: 10 quando a origem é o cron (nunca menos que 7), e pode ser bem maior quando o cliente ou a API adiantam a fatura (origem cliente/api). Em paid fica negativo quando a fatura foi paga em atraso.
pago_emISO 8601|nullPreenchido só em invoice.paid.
origemstringQuem gerou o evento. created: cron (emissão automática) | cliente (adiantou pelo portal) | api (POST /api/v1/subscriptions/{id}/generate-invoice). paid: gateway | baixa_manual (pagamento registrado pelo vendedor). due_soon e due_today: cron.
link_extratostring|nullURL pública (sem login) do extrato da assinatura: o cliente vê esta fatura e as vencidas e gera o PIX na hora de pagar. O payload não traz código PIX porque um PIX vale só 1 hora: mande o link, não o código. null se o link não pôde ser gerado.
cliente_telefone_e164string|nullTelefone em E.164 (+5511999999999), pronto para WhatsApp. No wa.me/ use os dígitos sem o +. null quando não dá para garantir o código do país (ex.: número sem DDD); nesse caso, caia para cliente_telefone.
cliente_telefonestring|nullTelefone bruto, como o cliente digitou no checkout (sem normalização).
cliente_id / cliente_nome / cliente_email / cliente_documentostring|nullDados do cliente: o snapshot gravado na fatura primeiro, o cadastro como reserva.
produtoobject|null{ id, nome, tipo }. null em assinatura sem produto (doação, link de pagamento): veja tipo_produto.
ofertaobject|null{ id, nome, recorrente, intervalo_cobranca }.
assinaturaobject{ id, status, intervalo, valor, proxima_cobranca }, lido no momento do disparo. Em paid, proxima_cobranca já avançou por este pagamento, e status pode ser cancelada quando o cliente quita uma dívida de assinatura já encerrada.
vencidas_em_abertointegerQuantas OUTRAS faturas de renovação desta assinatura estão vencidas e sem pagamento (esta não conta). Use para mudar o tom da mensagem quando o cliente já deve meses anteriores.
total_vencidas_em_abertonumberSoma dessas outras faturas vencidas, em reais.
invoice.due_soonFatura vence em 3 dias

Disparado pelo cron diário às 09:00 (horário de Brasília) para cada fatura de renovação PIX pendente cujo vencimento cai exatamente daqui a 3 dias (dias_para_vencer = 3). Pronto para o lembrete "sua fatura vence em DD/MM" com o link_extrato.

No máximo 1 disparo por fatura: a fatura é marcada por evento antes do envio, então rodar o cron de novo no mesmo dia não duplica. Se o cron não rodar num dia, o lembrete daquele dia não é reenviado depois. Só PIX de assinatura ativa: cartão, boleto e assinatura pausada ficam de fora.
O e-mail de lembrete do AtmosPay sai no mesmo horário, mas os dois são independentes: o webhook chega mesmo que o e-mail falhe. Não há reenvio manual destes eventos; o botão Testar manda um payload de exemplo.
Payload de exemplo
Renovação
{
  "evento": "invoice.due_soon",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "chave_idempotencia": "invoice.due_soon:11111111-1111-1111-1111-111111111111",
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "tipo": "invoice",
    "tipo_produto": "digital",
    "invoice_referencia": "2026-05",
    "status": "pendente",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "criado_em": "2026-05-07T12:00:00.000Z",
    "vencimento": "2026-05-18T02:59:59.000Z",
    "vencimento_data": "2026-05-17",
    "vencimento_formatado": "17/05/2026",
    "dias_para_vencer": 3,
    "pago_em": null,
    "origem": "cron",
    "link_extrato": "https://dashboard.atmospay.com/assinatura/0123456789abcdef0123456789abcdef",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "cliente_telefone_e164": "+5511999999999",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "assinatura": {
      "id": "22222222-2222-2222-2222-222222222222",
      "status": "ativa",
      "intervalo": "mensal",
      "valor": 87,
      "proxima_cobranca": "2026-05-18T02:59:59.000Z"
    },
    "vencidas_em_aberto": 0,
    "total_vencidas_em_aberto": 0
  },
  "test": true
}
Campos relevantes
chave_idempotenciastring<evento>:<venda_id> (ex.: invoice.due_today:1111…). Cada evento sai no máximo uma vez por fatura, então a chave é única por (evento, fatura): grave-a e descarte o que já existir. Um retry reentrega o mesmo payload.
venda_iduuidA fatura: uma venda com tipo = invoice.
order_idstringIdentificador humano (ORD-xxxx).
organizacao_iduuidOrg vendedora.
assinatura_iduuidAssinatura a que a fatura pertence.
external_idstringO metadata.external_id da ASSINATURA (a fatura de renovação não tem metadata própria). A chave vem ausente quando não existe, como nos subscription.*.
external_affiliate_idstringO metadata.external_affiliate_id da ASSINATURA: o afiliado de sistema externo, para comissão de renovação do seu lado. Ausente quando não existe.
tipo / tipo_produtostringtipo é sempre invoice. tipo_produto (digital, doacao, …) ajuda quando produto vem null (assinatura de doação ou de link).
invoice_referenciastring|nullCompetência YYYY-MM do ciclo coberto. Quase sempre é o mês de vencimento_data, mas pode diferir na virada do mês (a competência é contada em UTC) ou quando a fatura saiu atrasada e ganhou o prazo mínimo. null na venda-espelho da troca PIX→cartão.
statusstringpendente em created, due_soon e due_today; aprovado em paid.
forma_pagamentostringSempre pix em created e due_*. Em paid, o método que pagou: pix | cartao | boleto | …
valor_bruto / valor_liquidonumberEm REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
criado_emISO 8601Emissão da fatura (PIX: ~10 dias antes do vencimento). Não use para contar atraso nem como data de vencimento.
vencimentoISO 8601Instante do vencimento: 23:59:59 de Brasília, expresso em UTC. Convertido para UTC ele já cai no DIA SEGUINTE (02:59:59Z): quem formatar a data em UTC mostra um dia a mais. Para mensagem, use vencimento_data; para exibir o instante, converta para America/Sao_Paulo.
vencimento_dataYYYY-MM-DDDia civil do vencimento em Brasília, já convertido. É a data certa para mensagens e para comparar dias.
vencimento_formatadoDD/MM/AAAAO mesmo dia, pronto para o template de WhatsApp ("sua fatura vence em 18/09/2026").
dias_para_vencerintegerDias civis (Brasília) de hoje, no momento do disparo, até o vencimento: 3 em due_soon, 0 em due_today. Em created: 10 quando a origem é o cron (nunca menos que 7), e pode ser bem maior quando o cliente ou a API adiantam a fatura (origem cliente/api). Em paid fica negativo quando a fatura foi paga em atraso.
pago_emISO 8601|nullPreenchido só em invoice.paid.
origemstringQuem gerou o evento. created: cron (emissão automática) | cliente (adiantou pelo portal) | api (POST /api/v1/subscriptions/{id}/generate-invoice). paid: gateway | baixa_manual (pagamento registrado pelo vendedor). due_soon e due_today: cron.
link_extratostring|nullURL pública (sem login) do extrato da assinatura: o cliente vê esta fatura e as vencidas e gera o PIX na hora de pagar. O payload não traz código PIX porque um PIX vale só 1 hora: mande o link, não o código. null se o link não pôde ser gerado.
cliente_telefone_e164string|nullTelefone em E.164 (+5511999999999), pronto para WhatsApp. No wa.me/ use os dígitos sem o +. null quando não dá para garantir o código do país (ex.: número sem DDD); nesse caso, caia para cliente_telefone.
cliente_telefonestring|nullTelefone bruto, como o cliente digitou no checkout (sem normalização).
cliente_id / cliente_nome / cliente_email / cliente_documentostring|nullDados do cliente: o snapshot gravado na fatura primeiro, o cadastro como reserva.
produtoobject|null{ id, nome, tipo }. null em assinatura sem produto (doação, link de pagamento): veja tipo_produto.
ofertaobject|null{ id, nome, recorrente, intervalo_cobranca }.
assinaturaobject{ id, status, intervalo, valor, proxima_cobranca }, lido no momento do disparo. Em paid, proxima_cobranca já avançou por este pagamento, e status pode ser cancelada quando o cliente quita uma dívida de assinatura já encerrada.
vencidas_em_abertointegerQuantas OUTRAS faturas de renovação desta assinatura estão vencidas e sem pagamento (esta não conta). Use para mudar o tom da mensagem quando o cliente já deve meses anteriores.
total_vencidas_em_abertonumberSoma dessas outras faturas vencidas, em reais.
invoice.due_todayFatura vence hoje

Mesmo cron (09:00, horário de Brasília), no dia do vencimento (dias_para_vencer = 0): a fatura PIX segue pendente e ainda pode ser paga até 23:59 de hoje. Se continuar sem pagamento, subscription.overdue chega no dia seguinte, às 06:00.

No máximo 1 disparo por fatura: a fatura é marcada por evento antes do envio, então rodar o cron de novo no mesmo dia não duplica. Se o cron não rodar num dia, o lembrete daquele dia não é reenviado depois. Só PIX de assinatura ativa: cartão, boleto e assinatura pausada ficam de fora.
O e-mail de lembrete do AtmosPay sai no mesmo horário, mas os dois são independentes: o webhook chega mesmo que o e-mail falhe. Não há reenvio manual destes eventos; o botão Testar manda um payload de exemplo.
Payload de exemplo
Renovação
{
  "evento": "invoice.due_today",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "chave_idempotencia": "invoice.due_today:11111111-1111-1111-1111-111111111111",
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": "22222222-2222-2222-2222-222222222222",
    "tipo": "invoice",
    "tipo_produto": "digital",
    "invoice_referencia": "2026-05",
    "status": "pendente",
    "forma_pagamento": "pix",
    "valor_bruto": 87,
    "valor_liquido": 87,
    "criado_em": "2026-05-04T12:00:00.000Z",
    "vencimento": "2026-05-15T02:59:59.000Z",
    "vencimento_data": "2026-05-14",
    "vencimento_formatado": "14/05/2026",
    "dias_para_vencer": 0,
    "pago_em": null,
    "origem": "cron",
    "link_extrato": "https://dashboard.atmospay.com/assinatura/0123456789abcdef0123456789abcdef",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "cliente_telefone_e164": "+5511999999999",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Plano Mensal",
      "recorrente": true,
      "intervalo_cobranca": "mensal"
    },
    "assinatura": {
      "id": "22222222-2222-2222-2222-222222222222",
      "status": "ativa",
      "intervalo": "mensal",
      "valor": 87,
      "proxima_cobranca": "2026-06-13T12:00:00.000Z"
    },
    "vencidas_em_aberto": 0,
    "total_vencidas_em_aberto": 0
  },
  "test": true
}
Campos relevantes
chave_idempotenciastring<evento>:<venda_id> (ex.: invoice.due_today:1111…). Cada evento sai no máximo uma vez por fatura, então a chave é única por (evento, fatura): grave-a e descarte o que já existir. Um retry reentrega o mesmo payload.
venda_iduuidA fatura: uma venda com tipo = invoice.
order_idstringIdentificador humano (ORD-xxxx).
organizacao_iduuidOrg vendedora.
assinatura_iduuidAssinatura a que a fatura pertence.
external_idstringO metadata.external_id da ASSINATURA (a fatura de renovação não tem metadata própria). A chave vem ausente quando não existe, como nos subscription.*.
external_affiliate_idstringO metadata.external_affiliate_id da ASSINATURA: o afiliado de sistema externo, para comissão de renovação do seu lado. Ausente quando não existe.
tipo / tipo_produtostringtipo é sempre invoice. tipo_produto (digital, doacao, …) ajuda quando produto vem null (assinatura de doação ou de link).
invoice_referenciastring|nullCompetência YYYY-MM do ciclo coberto. Quase sempre é o mês de vencimento_data, mas pode diferir na virada do mês (a competência é contada em UTC) ou quando a fatura saiu atrasada e ganhou o prazo mínimo. null na venda-espelho da troca PIX→cartão.
statusstringpendente em created, due_soon e due_today; aprovado em paid.
forma_pagamentostringSempre pix em created e due_*. Em paid, o método que pagou: pix | cartao | boleto | …
valor_bruto / valor_liquidonumberEm REAIS, como em todos os webhooks (87 = R$ 87,00), não em centavos.
criado_emISO 8601Emissão da fatura (PIX: ~10 dias antes do vencimento). Não use para contar atraso nem como data de vencimento.
vencimentoISO 8601Instante do vencimento: 23:59:59 de Brasília, expresso em UTC. Convertido para UTC ele já cai no DIA SEGUINTE (02:59:59Z): quem formatar a data em UTC mostra um dia a mais. Para mensagem, use vencimento_data; para exibir o instante, converta para America/Sao_Paulo.
vencimento_dataYYYY-MM-DDDia civil do vencimento em Brasília, já convertido. É a data certa para mensagens e para comparar dias.
vencimento_formatadoDD/MM/AAAAO mesmo dia, pronto para o template de WhatsApp ("sua fatura vence em 18/09/2026").
dias_para_vencerintegerDias civis (Brasília) de hoje, no momento do disparo, até o vencimento: 3 em due_soon, 0 em due_today. Em created: 10 quando a origem é o cron (nunca menos que 7), e pode ser bem maior quando o cliente ou a API adiantam a fatura (origem cliente/api). Em paid fica negativo quando a fatura foi paga em atraso.
pago_emISO 8601|nullPreenchido só em invoice.paid.
origemstringQuem gerou o evento. created: cron (emissão automática) | cliente (adiantou pelo portal) | api (POST /api/v1/subscriptions/{id}/generate-invoice). paid: gateway | baixa_manual (pagamento registrado pelo vendedor). due_soon e due_today: cron.
link_extratostring|nullURL pública (sem login) do extrato da assinatura: o cliente vê esta fatura e as vencidas e gera o PIX na hora de pagar. O payload não traz código PIX porque um PIX vale só 1 hora: mande o link, não o código. null se o link não pôde ser gerado.
cliente_telefone_e164string|nullTelefone em E.164 (+5511999999999), pronto para WhatsApp. No wa.me/ use os dígitos sem o +. null quando não dá para garantir o código do país (ex.: número sem DDD); nesse caso, caia para cliente_telefone.
cliente_telefonestring|nullTelefone bruto, como o cliente digitou no checkout (sem normalização).
cliente_id / cliente_nome / cliente_email / cliente_documentostring|nullDados do cliente: o snapshot gravado na fatura primeiro, o cadastro como reserva.
produtoobject|null{ id, nome, tipo }. null em assinatura sem produto (doação, link de pagamento): veja tipo_produto.
ofertaobject|null{ id, nome, recorrente, intervalo_cobranca }.
assinaturaobject{ id, status, intervalo, valor, proxima_cobranca }, lido no momento do disparo. Em paid, proxima_cobranca já avançou por este pagamento, e status pode ser cancelada quando o cliente quita uma dívida de assinatura já encerrada.
vencidas_em_abertointegerQuantas OUTRAS faturas de renovação desta assinatura estão vencidas e sem pagamento (esta não conta). Use para mudar o tom da mensagem quando o cliente já deve meses anteriores.
total_vencidas_em_abertonumberSoma dessas outras faturas vencidas, em reais.
abandoned_cart.detectedCarrinho abandonado

Disparado a cada 30 minutos para vendas com PIX ou boleto gerado ainda em status=pendente há mais de 30 minutos e menos de 24 horas. Não retoma a sessão: o checkout_url leva a um checkout novo (venda nova). Cada tentativa é uma venda: o mesmo cliente pode gerar vários eventos, e quem pagou em outra tentativa ainda recebe o desta. Confira o payment.approved antes de mandar. Boleto costuma gerar alerta falso (vale dias).

Idempotência: o flag vendas.webhook_abandoned_disparado_em garante que cada venda recebe o webhook só uma vez. Vendas mais antigas que 24h não entram no cron (fora de janela de remarketing).
Payload de exemplo
{
  "evento": "abandoned_cart.detected",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "venda_id": "11111111-1111-1111-1111-111111111111",
    "order_id": "ORD-TEST-EXAMPLE",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "assinatura_id": null,
    "tipo": "unica",
    "valor_bruto": 87,
    "forma_pagamento": "pix",
    "status": "pendente",
    "cliente_id": "33333333-3333-3333-3333-333333333333",
    "cliente_nome": "Cliente de Teste",
    "cliente_email": "teste@exemplo.com",
    "cliente_telefone": "(11) 99999-9999",
    "cliente_documento": "12345678900",
    "produto": {
      "id": "44444444-4444-4444-4444-444444444444",
      "nome": "Produto de Teste",
      "tipo": "digital"
    },
    "oferta": {
      "id": "55555555-5555-5555-5555-555555555555",
      "nome": "Compra Única",
      "recorrente": false,
      "intervalo_cobranca": null
    },
    "checkout_url": "https://secure.atmospay.com/p/44444444-4444-4444-4444-444444444444?retomar=11111111-1111-1111-1111-111111111111",
    "criado_em": "2026-05-14T12:00:00.000Z",
    "minutos_abandonado": 35,
    "detectado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
venda_iduuidVenda em estado pendente abandonada no checkout.
order_idstringIdentificador humano (ORD-xxxx), útil pra retomar a sessão.
checkout_urlstring|nullURL pública do checkout original (varia por tipo: /p/, /l/, /d/, /e/). Pode ser null quando o produto não tem slug.
valor_brutonumberValor da venda em reais.
forma_pagamentostringMétodo escolhido no checkout (pix/cartao/boleto).
minutos_abandonadointegerQuanto tempo o cliente já está sem concluir o pagamento.
detectado_emISO 8601Quando o cron de 30min detectou o abandono.
cliente_nome / cliente_email / cliente_telefonestringSnapshot do cliente que iniciou o checkout. Use pra remarketing.
produto / ofertaobjectDados contextuais do que ele tentou comprar.
event.checkinCheck-in em evento

Disparado imediatamente após um participante ser validado na portaria (via dashboard web /api/eventos/[id]/checkin ou app scanner /api/mobile/eventos/scanner/checkin). Ideal para reativar credenciais externas (RFID, controle de acesso) ou abastecer dashboards em tempo real.

Eventos com múltiplos check-ins (entrada/saída) podem disparar vários webhooks para o mesmo participante: diferencie pelo tipo_checkin + checkin_id.
Payload de exemplo
Compra única
{
  "evento": "event.checkin",
  "event_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "timestamp": "2026-05-14T12:00:00.000Z",
  "data": {
    "checkin_id": "88888888-8888-8888-8888-888888888888",
    "organizacao_id": "00000000-0000-0000-0000-0000000000a1",
    "evento": {
      "id": "66666666-6666-6666-6666-666666666666",
      "produto_id": "44444444-4444-4444-4444-444444444444",
      "nome": "Evento de Teste"
    },
    "participante": {
      "id": "77777777-7777-7777-7777-777777777777",
      "nome": "Cliente de Teste",
      "email": "teste@exemplo.com",
      "documento": "12345678900",
      "codigo_ingresso": "ING-ABCD1234",
      "lote": {
        "id": "99999999-9999-9999-9999-999999999999",
        "nome": "Pista"
      }
    },
    "venda": {
      "id": "11111111-1111-1111-1111-111111111111",
      "order_id": "ORD-TEST-EXAMPLE",
      "comprador_id": "33333333-3333-3333-3333-333333333333",
      "comprador_nome": "Cliente de Teste",
      "comprador_email": "teste@exemplo.com",
      "comprador_telefone": "(11) 99999-9999",
      "comprador_documento": "12345678900"
    },
    "tipo_checkin": "entrada",
    "origem": "app",
    "total_checkins": 1,
    "max_checkins": 1,
    "realizado_em": "2026-05-14T12:00:00.000Z"
  },
  "test": true
}
Campos relevantes
checkin_iduuidRegistro de check-in criado.
eventoobject{ id, produto_id, nome } do evento físico.
participanteobject{ id, nome, email, documento, codigo_ingresso, lote: { id, nome } }.
vendaobject|null{ id, order_id, comprador_id, comprador_nome, comprador_email, comprador_telefone, comprador_documento }: contato de quem pagou (pode diferir do participante; é o canal com telefone).
tipo_checkinstringentrada (padrão) ou saida (eventos com controle de fluxo).
origemstringweb (dashboard admin) ou app (scanner mobile).
total_checkinsintegerQuantos check-ins esse participante já fez (inclui o atual).
max_checkinsintegerLimite configurado no lote (geralmente 1).
realizado_emISO 8601Momento exato do check-in (na portaria).

5. Tentativas e reenvio

6 tentativas no total: a primeira na hora e as outras 1 min, 5 min, 30 min, 2 h e 6 h depois da anterior (cerca de 8 h 36 min da primeira à última). As novas tentativas saem de uma fila verificada a cada minuto, então cada espera pode passar até 1 minuto do previsto. Cada nova tentativa é assinada de novo (outro t), com outro X-Webhook-Delivery-Id e o X-Webhook-Attempt da tentativa (2 a 6), e lê a URL e o secret atuais do webhook; o envelope (o event_id, o timestamp e o data) é o mesmo. A primeira resposta 2xx encerra as tentativas. Esgotadas as 6, a falha fica no histórico e pode ser reenviada pelo painel.

O que conta como falha

  • Resposta fora de 2xx: 3xx final, qualquer 4xx (inclusive 401, 408, 409, 422 e 429) e 5xx.
  • Tempo esgotado: 30 s por tentativa, para o pedido inteiro (com até 3 redirecionamentos seguidos).
  • Erro de rede: DNS que não resolve, TLS, conexão recusada ou derrubada no meio.

O que nunca é tentado de novo

  • A resposta 2xx (o corpo da resposta é ignorado; os primeiros 1000 caracteres ficam no histórico).
  • O destino recusado pela trava de segurança: URL que não é pública ou que resolve para endereço interno, porta fora de 443 e 80, mais de 3 redirecionamentos. Corrija a URL.
  • O webhook removido ou desativado entre uma tentativa e outra.

Reenviar uma entrega que falhou (painel)

Em Developers → Webhooks, em Entregas dos últimos 30 dias → Falhas recentes, cada falha tem o botão Reenviar. Ele manda o mesmo envelope (mesmo event_id, timestamp e data) com reenvio: true e o cabeçalho X-Webhook-Resend: 1, para a URL e com o secret atuais do webhook. O X-Webhook-Timestamp continua o da emissão original (é o timestamp do envelope), mas a assinatura é nova: o t do X-AtmosPay-Signature é o do reenvio, então a janela de 5 minutos vale normalmente. A entrega nova ganha outro X-Webhook-Delivery-Id, começa no X-Webhook-Attempt: 1, entra no histórico e, se der certo, encerra as novas tentativas automáticas que ainda estivessem marcadas para a mesma emissão. Quem deduplica pelo event_id e já tinha processado o evento descarta; quem não tinha, processa. Só o dono e os administradores da conta reenviam, e o reenvio fica registrado. Webhook desativado não recebe: ative antes.

Reenviar o pagamento de uma venda

Pelo painel (botão Reenviar webhook na venda) ou pelo MCP (reenviar_webhook_venda). Só para venda aprovada. Reenvia o payment.approved e, quando a venda é compra ou contratação (não renovação), também o purchase.approved, cada um com reenvio: true, X-Webhook-Resend: 1 e um event_id novo. O data é o mesmo da emissão original, mas é outra emissão: quem deduplica por event_id não o confunde com uma nova tentativa e decide se reprocessa. Para não repetir uma mensagem ao cliente, deduplique por data.venda_id.

Testar via UI

Em Developers → Webhooks, clique no ícone Enviar ao lado do webhook desejado. Selecione um evento + cenário e dispare. O payload é o mesmo que esta doc usa nos exemplos, com um event_id novo a cada clique. O teste se identifica de três jeitos: test: true no corpo, o cabeçalho X-Webhook-Test: 1 e o User-Agent AtmosPay-Webhook/1.0 (test). Os outros cabeçalhos e as duas assinaturas são os da entrega real. O teste espera até 15 s, não entra no histórico e nunca é tentado de novo.

6. Boas práticas

Idempotência

Consumers devem ignorar eventos duplicados: retries podem entregar o mesmo evento mais de uma vez. Todo envelope traz event_id: os retries repetem o mesmo valor, então ele é a chave única natural. Para descartar também um reenvio manual (que vem com event_id novo e reenvio: true), guarde ainda a chave de negócio data.venda_id + evento + (data.invoice_referencia ?? "") ou, nos eventos por produto (produto.liberado, produto.revogado), (data.venda_id ?? data.assinatura_id) + evento + data.produto_id + data.cliente.id: o produto.revogado de uma assinatura não tem venda_id, e o de um estorno não tem assinatura_id. A venda vem primeiro de propósito: produto.liberado sai uma vez por ciclo (cada renovação é uma venda nova) e sempre tem venda_id; chavear pela assinatura repetiria a mesma chave em todos os ciclos e faria você descartar a liberação das renovações. Cada endpoint recebe cada evento uma vez, inclusive os de assinatura com mais de um produto.

// 1) Retry da mesma emissão: mesmo event_id
if (await jaProcessei(payload.event_id)) return 200

// 2) Reenvio manual (event_id novo, reenvio: true): mesma chave de negócio
const d = payload.data
const chaveNegocio = d.produto_id
  ? [d.venda_id ?? d.assinatura_id, payload.evento, d.produto_id, d.cliente?.id].join(':')
  : [d.venda_id, payload.evento, d.invoice_referencia ?? ''].join(':')
if (payload.reenvio && await jaProcessei(chaveNegocio)) return 200

await registrarComoProcessado(payload.event_id, chaveNegocio)
// ... regra de negócio

Os eventos invoice.* já trazem a chave pronta em data.chave_idempotencia (<evento>:<venda_id>): use-a direto, sem montar a composição acima. Como cada um sai no máximo uma vez por fatura, a chave só se repete quando é retry da mesma entrega: exatamente o que você quer descartar antes de mandar a mensagem ao cliente.

Responda em < 5 segundos

O timeout do envio é 30s, mas a recomendação é responder200rapidamente (idealmente < 5s) e processar a regra de negócio em background (fila, worker, Inngest). Latência alta no consumer atrapalha throughput de outros eventos.

Resposta assíncrona

  1. Receba o evento, valide a assinatura e o envelope.
  2. Persista o evento bruto na sua tabela webhook_inbox.
  3. Responda 200 imediatamente.
  4. Processe em job separado (com retry e DLQ próprios).

Logs de eventos recebidos

Persista pelo menos: headers, body bruto, status retornado, processado_em. Isso é essencial para auditoria e para diferenciar bug seu de bug do AtmosPay quando alguém reportar “não chegou”.

Não confie na ordem

Retries fazem com que eventos cheguem fora de ordem. É possível receber payment.approved antes de order.created, produto.liberado antes de payment.approved, ou umorder.refunded chegar antes do payment.approved em um caso de race. Trate cada evento de forma idempotente, usando timestamp + status atual do recurso, não o histórico de chegadas.

7. Códigos HTTP

Como o AtmosPay reage à resposta do seu endpoint:

2xxSucessoEntrega concluída. Nenhuma nova tentativa.
3xxRedirecionamentoSeguido até 3 vezes, cada destino conferido; vale a resposta final. 301, 302 e 303 de um POST viram GET sem corpo: aponte o webhook direto para a URL final.
4xx (todos)FalhaConta como falha, como qualquer resposta fora de 2xx (401, 408, 409, 422 e 429 inclusive): fica no histórico e é tentada de novo conforme a seção 5. Corrija e use Testar ou Reenviar.
5xxFalhaMesmo tratamento do 4xx (seção 5).
timeout (30 s)Sem respostaMesmo tratamento do 4xx. O prazo vale para o pedido inteiro, redirecionamentos inclusive.
destino recusadoTrava de segurançaURL que não é pública, porta fora de 443 e 80 ou redirecionamentos demais. Fica no histórico e nunca é tentada de novo: corrija a URL.

8. Glossário

Organização (org)Tenant dono do produto vendido. Todo webhook respeita o escopo da org: você só recebe eventos de vendas dos produtos que pertencem à organização configurada.
VendaLinha unitária de transação. Pode ser compra única (avulsa) ou fatura (tipo = invoice) gerada por uma assinatura.
AssinaturaRelação contínua entre cliente e produto recorrente. Gera várias faturas ao longo do tempo. Cada fatura é representada por uma venda com assinatura_id preenchido.
Fatura / invoiceSinônimo de venda no contexto de assinatura. O invoice_referencia (YYYY-MM) identifica o ciclo coberto. Na renovação PIX a fatura é emitida 10 dias antes do vencimento, que é o dia do ciclo (proxima_cobranca).
invoice_referenciaPeríodo (YYYY-MM) a que a cobrança se refere: o mês do ciclo. Ex: assinatura mensal com ciclo em 04/jun tem referencia 2026-06, mesmo com a fatura PIX emitida em 25/mai.
CompradoraOrganização externa que abriu o checkout via ?org={uuid}. Identifica vendas geradas por parceiros.

9. Redirecionamento após o pagamento

Além do webhook, o checkout pode levar o cliente para uma página SUA depois do pagamento aprovado. A URL é cadastrada no produto (aba Geral → URL de Redirecionamento Após Pagamento). Vale para o checkout de produto (/p/…): cartão redireciona na hora da aprovação; PIX redireciona assim que o pagamento é confirmado na tela do QR Code (a tela consulta a cada 3 s). Boleto não redireciona: o pagamento chega dias depois e só o webhook avisa.

Estrutura da URL

Um único parâmetro é anexado, order_id, com o identificador legível do pedido (o mesmo data.order_id dos webhooks). Se a URL cadastrada já tem query string, ele entra com &; senão, com ?.

# URL cadastrada: https://seusite.com/obrigado
https://seusite.com/obrigado?order_id=ORD-1789516000000-AB12CD

# URL cadastrada: https://seusite.com/obrigado?utm_source=atmospay
https://seusite.com/obrigado?utm_source=atmospay&order_id=ORD-1789516000000-AB12CD

# Formato do order_id: ORD-<timestamp em ms>-<6 caracteres>

Como usar com segurança

O redirecionamento não é assinado: qualquer pessoa pode abrir a sua URL com um order_id inventado ou de outro pedido. Use o parâmetro só para saber qual pedido mostrar e confirme o pagamento por uma das duas fontes assinadas antes de liberar acesso, entregar ou agradecer com dados do pedido:

// 1) Pelo webhook (recomendado): você já recebeu payment.approved com
//    data.order_id = "ORD-…". Procure no seu banco pelo order_id.

// 2) Pela API v1, com a sua chave (nunca no navegador):
const res = await fetch(`https://www.atmospay.com/api/v1/orders/${orderId}`, {
  headers: { Authorization: `Bearer ${process.env.ATMOSPAY_API_KEY}` },
})
const pedido = await res.json()
if (pedido.status !== 'aprovado') return mostrarAguardando()
// GET /orders/{id} aceita tanto o UUID quanto o order_id "ORD-…"

Webhook e redirecionamento chegam por caminhos independentes: o webhook pode chegar alguns segundos depois do redirecionamento (ou antes, no PIX). Se a sua página de obrigado não achar o pedido ainda, mostre "confirmando" e consulte a API. Não trate como erro.

Precisa de um evento que ainda não existe ou tem dúvida sobre estrutura? Abra um ticket: adicionamos sob demanda. Documentação relacionada: API REST v1.