Outbound Webhooks

A plataforma Autra emite webhooks outbound pra notificar o BFF do cliente quando algo terminal acontece num evento (pagamento concluído, TED creditada, onboarding aprovado, transação acquiring autorizada etc.).

Esta página explica:

  • Como o cliente cadastra a URL que vai receber os eventos.
  • O formato exato do payload que enviamos.
  • Como validar o X-Autra-Signature (HMAC-SHA256).
  • Como tratar retries (até 16 tentativas em ~60 min).

Pra cadastrar/atualizar uma subscription, use PUT /v1/banking/webhooks/{domain}. Pra listar / consultar / remover, ver os outros endpoints em "Outbound Webhooks" na referência.

1. Como funciona

+-------------------+        +-------------------+        +------------------+
| Evento terminal   |        |   Autra Service   |        |  BFF do cliente  |
| (Pix completo,    | -----> | Dispatch (sync)   | -----> | (URL cadastrada) |
| TED, onboarding,  |        | + HMAC + Auth     |        |                  |
| acquiring etc.)   |        +-------------------+        +------------------+
+-------------------+                 |
                                      | em caso de falha (timeout, 5xx, conn refused)
                                      v
                              +------------------+
                              | Autra_Webhook_   |
                              | Retry (1/min,    | -----> tenta de novo até 16x
                              | EventBridge)     |        em ~60 min
                              +------------------+
  • Tentativa síncrona = attempt 1 (no momento do evento).
  • Retry automático = attempts 2–16 (worker que roda 1×/min).
  • Esgotado = state = DESPREZADO, não tenta mais.

2. Domínios suportados

Cada domain tem uma subscription independente. Registre uma URL por domínio (podem ser URLs diferentes):

DomainCobre
pix-paymentsPix — pix_payment.completed, pix_payment.failed, pix_payment.rejected, pix_payment.error (Pix enviado) + pix_payment.received (Pix recebido / crédito entrante, status RECEIVED)
onboardingConta digital — onboarding.completed, onboarding.declined, onboarding.failed, onboarding.canceled, onboarding.waiting_correction
transfersTED — transfer.in.completed, transfer.in.failed, transfer.out.completed, transfer.out.failed, transfer.out.canceled
acquiringAdquirência (POS + Pix Checkout) — acquiring.financial_transaction, acquiring.payout, acquiring.financial_cycle, acquiring.payment_slip_notification, acquiring.merchant_onboarding_status
cardsCartões (banking) — eventos do ciclo de vida + antifraude + transações. Ver detalhamento abaixo.
ddaDDA (Débito Direto Autorizado) — dda.payer_status_updated (enrollment do pagador) + dda.bill_status_updated (ciclo do boleto). Ver detalhamento abaixo.
pix-keysPortabilidade de chave Pix (claim) — pix_keys.claim.requested, pix_keys.claim.confirmed, pix_keys.claim.completed, pix_keys.claim.denied, pix_keys.claim.canceled. Ver detalhamento abaixo.
paymentsPagamento de conta/boleto/tributo a partir da conta (débito) — payment.completed (status PAID), payment.failed (DENIED/ROLLBACK/ERROR).
boletoBoleto emitido pela conta foi liquidado (crédito entrante) — boleto.paid (status PAID).

Eventos intermediários (PROCESSING, ACCEPTED, FILES_PENDING etc.) não disparam webhook — use os endpoints de GET pra polling quando precisar do estado corrente.

Domain cards em detalhe

Apenas cartões cadastrados na plataforma Autra geram webhook. Cartões fora da plataforma são ignorados (não despacham).

Event typeQuando dispara
card.createdCartão emitido
card.status_updatedMudança de status (ativo/bloqueado/cancelado)
card.pin_updatedTroca/cadastro de PIN
card.updatedMudanças genéricas de atributos do cartão
card.reissuedCartão reemitido (2ª via)
card.fraud_actionBloqueio/ação por antifraude
card.authorizationAutorização de compra (pass-through — payload da operação em operationData.payload)
card.chargeCobrança/estorno (pass-through)

Segurança: estes eventos nunca carregam PAN ou CVV em claro. Só trafega masked_pan (ex.: 421847******4639).

Domain dda em detalhe

Apenas contas cadastradas na plataforma Autra geram webhook. Contas fora da plataforma são ignoradas (não despacham).

Event typeQuando dispara
dda.payer_status_updatedMudança de status do enrollment do pagador — PENDINGPROCESSINGACTIVE, ou DISABLED / DENIED. O POST /dda retorna PENDING síncrono; a transição final pra ACTIVE chega por este webhook.
dda.bill_status_updatedQualquer mudança no ciclo de vida de um boleto DDA. Um único event_type cobre todo o ciclo — discrimine pelo status_dda no payload: APRESENTADO, ACEITO, RECUSADO, AGENDADO, PAGO/PAID, BAIXADO.

Pré-requisito: o pagador precisa estar ACTIVE pra começar a receber dda.bill_status_updated. Antes disso só chega dda.payer_status_updated.

Pass-through: o payload completo da operação vai inteiro em operationData.payload — inclui o boleto completo (bar_code, digitable_line_number, bill_amount, original_beneficiary, interest, fine, calculated_amount etc.). Discrimine sempre pelo conteúdo do payload, não pelo eventType do envelope.

Domain pix-keys em detalhe

Cobre a portabilidade (claim) de chave Pix. Apenas contas/chaves da plataforma Autra geram webhook.

Event typeQuando dispara
pix_keys.claim.requestedAlguém pediu a portabilidade de uma chave que é sua (lado doador) — o cliente precisa autorizar ou contestar dentro do prazo
pix_keys.claim.confirmedConfirmação do doador recebida — chave em transferência
pix_keys.claim.completedChave transferida com sucesso
pix_keys.claim.deniedPortabilidade negada/contestada
pix_keys.claim.canceledPortabilidade cancelada pelo solicitante

3. Formato do payload (envelope Autra)

Todos os domínios usam o mesmo envelope. Só muda eventType + o conteúdo de operationData.

Request

POST <sua-url-cadastrada>
Authorization: Basic <opcional, conforme auth da subscription>
Content-Type: application/json
User-Agent: Autra-Webhooks/1.0
X-Autra-Signature: sha256=<hex-lowercase>

{
  "eventId": "01963e6f-aa00-7c00-9d00-000000000001",
  "eventType": "acquiring.financial_transaction",
  "occurredAt": "2026-05-20T15:25:00Z",
  "tenantId": "2916bfe7-ecec-46bc-bc51-8a19aa593c1a",
  "userDocument": "25055662000190",
  "ownerId": "019d4f6e-c21d-4199-9424-9c37a2a1c65d",
  "status": "AUTHORIZED",
  "counterpartyName": "Fulano de Tal",
  "operationData": {
    "subject": "FINANCIAL_TRANSACTION",
    "productType": "CREDIT",
    "slugMerchant": "57DA076635814851A7A4B36FEDB340BB",
    "payload": {
      "rrn": "010008094933",
      "muid": "26F9758DB70336D7547CCA2ECA1C76D4",
      "slug": "4C037E901ADD45E29BA9C80C00A71745",
      "totalAmount": 12.50,
      "productType": "PREPAID_DEBIT",
      "transactionStatus": "AUTHORIZED",
      "...": "demais campos da operação — pass-through"
    }
  }
}
⚠️

operationData.payload traz os dados da operação, repassados sem transformação. Parseie de forma tolerante — campos podem ser adicionados sem aviso. Em especial:

  • totalAmount é decimal em reais (ex.: 12.50, 0.01) — não é inteiro em centavos. Desserialize como número decimal/float, nunca como int/long.
  • productType é string — veja os valores conhecidos na tabela abaixo. Trate como lista aberta: não use enum fechado, novos valores podem surgir sem aviso.

Valores de productType (acquiring)

Valores observados na base de produção da Autra (eventos acquiring.financial_transaction e acquiring.payout):

productTypeSignificado
CREDITCartão de crédito
DEBITCartão de débito
PREPAID_CREDITCartão pré-pago, função crédito
PREPAID_DEBITCartão pré-pago, função débito
PIXTransação Pix (QR / maquininha)
VOUCHERVoucher / benefício

⚠️ Lista não fechada — reflete o que já trafegou; podem surgir valores novos. Desserialize productType como string e tenha um caminho de fallback para valores desconhecidos.

Campos top-level (sempre presentes)

userDocument e ownerId são preenchidos para o tráfego do app portador/merchant. Em alguns casos legado podem vir ausentes — nesse caso, roteie pelos dados em operationData.

CampoTipoDescrição
eventIdUUIDIdentificador único do evento. Use pra idempotência (mesmo eventId pode chegar até 16 vezes em retry).
eventTypestringEx.: acquiring.financial_transaction, pix_payment.completed.
occurredAtISO-8601 UTCMomento em que o evento foi gerado pela Autra.
tenantIdUUIDTenant dono do evento (caso o BFF atenda vários tenants).
userDocumentstringCPF (portador) ou CNPJ (merchant) do dono do evento — quem deve receber a notificação no app. tenantId sozinho não distingue qual usuário do tenant é o destinatário; roteie por (tenantId, userDocument). Resolvido pela conta/cartão/merchant. Pode vir ausente em casos legado.
ownerIdUUIDID interno estável do dono (não-PII): persons.id/legal_entities.id. Alternativa ao userDocument quando preferir não indexar por CPF/CNPJ. Mesma ressalva de ausência no legado.
statusstringStatus terminal (COMPLETED, AUTHORIZED, FAILED etc.).
counterpartyNamestringNome da contraparte (opcional): destinatário no envio (Pix/TED out), pagador no recebimento (Pix/TED in). Ausente quando não aplicável — use com fallback.
flavorstringVariante white-label do app do tenant (ex.: merci). Opcional — presente só quando o tenant usa um app white-label; ausente para o app padrão (autra). Use para roteamento/branding por marca.
operationDataobjectConteúdo específico do evento. Em acquiring, traz payload com os dados da operação — parse tolerante; totalAmount decimal, productType string aberta (ver aviso acima).

Headers enviados

HeaderValor
Content-Typeapplication/json
User-AgentAutra-Webhooks/1.0
X-Autra-Signaturesha256=<hex lowercase> — ver seção HMAC
AuthorizationOpcional, conforme auth da subscription (Basic ou Bearer)
(custom)Opcional, conforme auth.type=HEADER da subscription

4. Validação do X-Autra-Signature (HMAC-SHA256)

A cada POST mandamos:

X-Autra-Signature: sha256=<hex>

Onde <hex> é HMAC-SHA256(secret, raw_body) em lowercase hexadecimal. O secret é uma string de 32+ chars única por tenant — compartilhada entre todos os domínios. Não há secret separado por domínio.

⚠️

O secret aparece apenas uma vez na resposta do PUT (_secretShownOnce: true). GET/LIST nunca o devolvem. Guarde em vault no momento do cadastro.

  • 1ª subscription do tenant: sem secret no body → servidor gera. Com secret no body → usa o passado.
  • Subscriptions seguintes do mesmo tenant: sem secret → reusa o secret existente (recomendado). Com secret diferente → ROTACIONA pra todas as subscriptions do tenant (atomic; retries em andamento passam a usar o novo).

Protocolo de validação no BFF

  1. Capturar o body raw da request — bytes exatos que chegaram na conexão, antes de qualquer parse/re-serialize do JSON.
  2. Ler o header X-Autra-Signature.
  3. Calcular expected = "sha256=" + hex(HMAC-SHA256(secret, raw_body)).
  4. Comparar com constant-time ao valor recebido. Igual → processa. Diferente → responder 401.

Snippets prontos

Node.js / Express — registre o verify no middleware pra ter o raw body:

const crypto = require('crypto');

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function verifyAutraSignature(req, secret) {
  const header = req.get('X-Autra-Signature') || '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.rawBody)
    .digest('hex');

  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Java / Spring — use ContentCachingRequestWrapper ou um Filter que cacheie o byte[] antes do JSON binding:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public boolean verify(byte[] rawBody, String header, String secret) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] hash = mac.doFinal(rawBody);

    StringBuilder hex = new StringBuilder("sha256=");
    for (byte b : hash) hex.append(String.format("%02x", b));

    return MessageDigest.isEqual(
        hex.toString().getBytes(StandardCharsets.UTF_8),
        header.getBytes(StandardCharsets.UTF_8)
    );
}

Python / FastAPI — use await request.body() antes de qualquer parse:

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header or "")

Go (net/http):

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verify(rawBody []byte, header, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(header))
}

Pegadinhas comuns

  • Body raw, NÃO o JSON re-serializado. JSON.parseJSON.stringify muda a ordem das chaves / espaços e quebra a assinatura. Capture os bytes ANTES de qualquer parse.
  • Comparação constant-time (timingSafeEqual / MessageDigest.isEqual / hmac.compare_digest / hmac.Equal). Não use == / .equals() — vazam timing.
  • Hex lowercase. Nossa implementação emite lowercase. Não aplique toUpperCase na comparação.
  • Prefixo sha256= faz parte do header. Compare a string inteira ("sha256=" + hex) ou strippe de ambos os lados.

Teste de mesa (validar o cálculo localmente)

echo -n '{"test":"hello"}' | openssl dgst -sha256 -hmac "SEU_SECRET" -hex

Se o hex resultante for igual ao que a Autra envia para o mesmo body + secret, o BFF está pronto.

Como gerar um secret forte (caso queira fornecer o seu, em vez de deixar o servidor gerar)

openssl rand -hex 32                       # 64 chars hex (256 bits)
openssl rand -base64 48 | tr -d '\n'       # ~64 chars base64

5. Auth opcional no transporte (Basic / Bearer / Header custom)

Além do HMAC (que é integridade do payload), a subscription suporta autenticação de transporte opcional pra satisfazer um gateway/proxy do cliente que exige Authorization ou um header de API key. Tipos:

auth.typeComportamento
NONE (default)Nada além do HMAC.
BASICEnvia Authorization: Basic base64(username:password). Exige username + password.
BEAREREnvia Authorization: Bearer <token>. Exige token.
HEADEREnvia header custom (ex.: x-api-key: <value>). Exige name + value. Não pode usar nomes reservados (Host, Content-Length, Content-Type, X-Autra-Signature, User-Agent).
🔒

password, token e value aparecem apenas na resposta do PUT (mesmo tratamento do secret). GET/LIST devolvem só auth.type (e auth.name, se type=HEADER — o nome do header em si não é segredo). Pra trocar, faça outro PUT com o novo valor.

6. Política de entrega + retry

FaseO que aconteceColuna state
Síncrono (attempt 1)Tentativa imediata no momento do evento. Timeout 5s.SUCCEEDED se 2xx; senão PENDING_RETRY
Retry rápido (attempts 2–11)Worker reenvia +1 min cada → 10 tentativas em 10 min.PENDING_RETRY
Retry lento (attempts 12–16)Worker reenvia +10 min cada → 5 tentativas em 50 min.PENDING_RETRY
EsgotadoApós attempt 16 falho, não tenta mais.DESPREZADO

Total: 16 tentativas em ~60 min antes de DESPREZAR. Cada retry re-assina o HMAC com o secret atual do tenant (rotação propaga pra todas as subscriptions do tenant; retries em voo passam a usar o novo).

O que o BFF deve fazer

  • 2xx → entrega marcada como sucesso. Não retenta.
  • Qualquer outro status / timeout / erro de rede → entra na fila de retry.
  • Timeout: 5s. BFF do cliente deve responder rápido. Recomendação: enfileirar internamente (ex.: SQS, Kafka, fila in-memory) e retornar 200 imediato; processar de forma assíncrona.
  • Idempotência: o mesmo eventId pode chegar até 16 vezes. Use eventId como chave de dedup no BFF.

7. Como cadastrar a subscription

O secret é opcional — se omitido, o servidor gera 64 chars hex (256 bits). Recomendado pra self-service: você não precisa cuidar da geração nem do canal de entrega.

🔑

1 secret por tenant — compartilhado entre todos os domínios. Se você já criou uma subscription em acquiring e agora vai criar em pix-payments (mesmo tenant), basta omitir secret que o servidor reusa o existente. Você não precisa rastrear secrets diferentes por domínio.

Variante A — server gera o secret (recomendado)

curl -X PUT https://api.autra.io/v1/banking/webhooks/acquiring \
  -H "Authorization: Bearer <JWT-do-tenant>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://bff-do-cliente.com.br/webhooks/autra",
    "eventTypes": ["*"],
    "enabled": true,
    "auth": {
      "type": "BASIC",
      "username": "autra-webhook",
      "password": "<senha-do-bff>"
    }
  }'

Variante B — você fornece o secret

curl -X PUT https://api.autra.io/v1/banking/webhooks/acquiring \
  -H "Authorization: Bearer <JWT-do-tenant>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://bff-do-cliente.com.br/webhooks/autra",
    "secret": "<32+ chars; gere com openssl rand -hex 32>",
    "eventTypes": ["*"],
    "enabled": true,
    "auth": { "type": "BASIC", "username": "autra-webhook", "password": "..." }
  }'

Resposta do PUT (contém secret revelado — guarde agora!)

{
  "id": "9f7a...",
  "tenantId": "2916bfe7-ecec-46bc-bc51-8a19aa593c1a",
  "domain": "acquiring",
  "url": "https://bff-do-cliente.com.br/webhooks/autra",
  "secret": "EXAMPLE_3f019b734d51e1ed4298ac49fea42a26bef159724247d83bddf0e561",
  "eventTypes": ["*"],
  "enabled": true,
  "auth": {
    "type": "BASIC",
    "username": "autra-webhook",
    "password": "<senha-cru>"
  },
  "createdAt": "2026-05-20T15:25:00Z",
  "updatedAt": "2026-05-20T15:25:00Z",
  "_secretShownOnce": true
}
⚠️

Esta é a única resposta onde secret, password, token e value aparecem. Salve agora em vault — GET /v1/banking/webhooks e GET /v1/banking/webhooks/{domain} sempre redactam (devolvem só auth.type e auth.name se HEADER).

Ver detalhes completos em PUT /v1/banking/webhooks/{domain} na referência da API.

8. Suporte

Em caso de dúvida sobre validação HMAC, retries não chegando, ou cadastro de domínios, fale com o time Autra. Em pedidos de troubleshooting, anexe eventId e timestamp aproximado — conseguimos localizar a delivery e ver attempt, state, response_status e error registrados.