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):
| Domain | Cobre |
|---|---|
pix-payments | Pix — pix_payment.completed, pix_payment.failed, pix_payment.rejected, pix_payment.error (Pix enviado) + pix_payment.received (Pix recebido / crédito entrante, status RECEIVED) |
onboarding | Conta digital — onboarding.completed, onboarding.declined, onboarding.failed, onboarding.canceled, onboarding.waiting_correction |
transfers | TED — transfer.in.completed, transfer.in.failed, transfer.out.completed, transfer.out.failed, transfer.out.canceled |
acquiring | Adquirência (POS + Pix Checkout) — acquiring.financial_transaction, acquiring.payout, acquiring.financial_cycle, acquiring.payment_slip_notification, acquiring.merchant_onboarding_status |
cards | Cartões (banking) — eventos do ciclo de vida + antifraude + transações. Ver detalhamento abaixo. |
dda | DDA (Débito Direto Autorizado) — dda.payer_status_updated (enrollment do pagador) + dda.bill_status_updated (ciclo do boleto). Ver detalhamento abaixo. |
pix-keys | Portabilidade 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. |
payments | Pagamento de conta/boleto/tributo a partir da conta (débito) — payment.completed (status PAID), payment.failed (DENIED/ROLLBACK/ERROR). |
boleto | Boleto 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
cards em detalheApenas cartões cadastrados na plataforma Autra geram webhook. Cartões fora da plataforma são ignorados (não despacham).
| Event type | Quando dispara |
|---|---|
card.created | Cartão emitido |
card.status_updated | Mudança de status (ativo/bloqueado/cancelado) |
card.pin_updated | Troca/cadastro de PIN |
card.updated | Mudanças genéricas de atributos do cartão |
card.reissued | Cartão reemitido (2ª via) |
card.fraud_action | Bloqueio/ação por antifraude |
card.authorization | Autorização de compra (pass-through — payload da operação em operationData.payload) |
card.charge | Cobranç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
dda em detalheApenas contas cadastradas na plataforma Autra geram webhook. Contas fora da plataforma são ignoradas (não despacham).
| Event type | Quando dispara |
|---|---|
dda.payer_status_updated | Mudança de status do enrollment do pagador — PENDING → PROCESSING → ACTIVE, ou DISABLED / DENIED. O POST /dda retorna PENDING síncrono; a transição final pra ACTIVE chega por este webhook. |
dda.bill_status_updated | Qualquer 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
pix-keys em detalheCobre a portabilidade (claim) de chave Pix. Apenas contas/chaves da plataforma Autra geram webhook.
| Event type | Quando dispara |
|---|---|
pix_keys.claim.requested | Alguém pediu a portabilidade de uma chave que é sua (lado doador) — o cliente precisa autorizar ou contestar dentro do prazo |
pix_keys.claim.confirmed | Confirmação do doador recebida — chave em transferência |
pix_keys.claim.completed | Chave transferida com sucesso |
pix_keys.claim.denied | Portabilidade negada/contestada |
pix_keys.claim.canceled | Portabilidade 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.payloadtraz 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)
productType (acquiring)Valores observados na base de produção da Autra (eventos acquiring.financial_transaction e acquiring.payout):
productType | Significado |
|---|---|
CREDIT | Cartão de crédito |
DEBIT | Cartão de débito |
PREPAID_CREDIT | Cartão pré-pago, função crédito |
PREPAID_DEBIT | Cartão pré-pago, função débito |
PIX | Transação Pix (QR / maquininha) |
VOUCHER | Voucher / 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.
| Campo | Tipo | Descrição |
|---|---|---|
eventId | UUID | Identificador único do evento. Use pra idempotência (mesmo eventId pode chegar até 16 vezes em retry). |
eventType | string | Ex.: acquiring.financial_transaction, pix_payment.completed. |
occurredAt | ISO-8601 UTC | Momento em que o evento foi gerado pela Autra. |
tenantId | UUID | Tenant dono do evento (caso o BFF atenda vários tenants). |
userDocument | string | CPF (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. |
ownerId | UUID | ID 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. |
status | string | Status terminal (COMPLETED, AUTHORIZED, FAILED etc.). |
counterpartyName | string | Nome 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. |
flavor | string | Variante 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. |
operationData | object | Conteú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
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Autra-Webhooks/1.0 |
X-Autra-Signature | sha256=<hex lowercase> — ver seção HMAC |
Authorization | Opcional, conforme auth da subscription (Basic ou Bearer) |
| (custom) | Opcional, conforme auth.type=HEADER da subscription |
4. Validação do X-Autra-Signature (HMAC-SHA256)
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
- Capturar o body raw da request — bytes exatos que chegaram na conexão, antes de qualquer parse/re-serialize do JSON.
- Ler o header
X-Autra-Signature. - Calcular
expected = "sha256=" + hex(HMAC-SHA256(secret, raw_body)). - 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.parse→JSON.stringifymuda 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
toUpperCasena 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" -hexSe 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 base645. 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.type | Comportamento |
|---|---|
NONE (default) | Nada além do HMAC. |
BASIC | Envia Authorization: Basic base64(username:password). Exige username + password. |
BEARER | Envia Authorization: Bearer <token>. Exige token. |
HEADER | Envia 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,tokenevalueaparecem apenas na resposta do PUT (mesmo tratamento do secret). GET/LIST devolvem sóauth.type(eauth.name, setype=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
| Fase | O que acontece | Coluna 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 |
| Esgotado | Apó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
eventIdpode chegar até 16 vezes. UseeventIdcomo 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 emacquiringe agora vai criar empix-payments(mesmo tenant), basta omitirsecretque 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 ondesecret,password,tokenevalueaparecem. Salve agora em vault —GET /v1/banking/webhookseGET /v1/banking/webhooks/{domain}sempre redactam (devolvem sóauth.typeeauth.nameseHEADER).
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.
