Registra a URL do BFF do cliente que receberá os eventos do domain.
Operação idempotente: PUT no mesmo domain substitui a subscription.
Assinatura HMAC (X-Autra-Signature)
X-Autra-Signature)Cada POST enviado para a url carrega o header X-Autra-Signature:
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 (pix-payments, onboarding, transfers,
acquiring). 1 secret por tenant; não há secret separado por domínio.
Como obter o secret (1ª subscription do tenant):
- Self-service (recomendado) — omita
secretno body do PUT. O
servidor gera 64 chars hex (256 bits) e devolve no camposecret
da resposta. Aparece apenas nesta resposta (_secretShownOnce: true).
GET/LIST nunca devolvem. - Forneça o seu — passe
secretno body (mínimo 32 chars).
A resposta ainda inclui o valor pra confirmação. Gere com
openssl rand -hex 32.
Subscriptions seguintes do mesmo tenant: PUT em outros domínios
(ex.: já tem acquiring, agora cria pix-payments) reusam o secret
existente do tenant automaticamente — pode omitir o campo. Se passar
secret diferente, ROTACIONA pra todas as subscriptions do tenant.
Rotação: novo PUT com secret diferente em qualquer domínio →
atualiza o secret em TODAS as subscriptions do tenant (atomic).
Próximos retries usam o secret novo automaticamente.
Headers enviados em cada POST
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Autra-Webhooks/1.0 |
X-Autra-Signature | sha256=<hex lowercase> |
Authorization | Opcional — conforme auth da subscription (Basic/Bearer) |
| (custom) | Opcional — conforme auth.type=HEADER da subscription |
Protocolo de validação no BFF do cliente
- 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.
Node.js / Express (precisa do raw body — express.json({ verify: (req, _r, buf) => req.rawBody = buf })):
const crypto = require('crypto');
function verify(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), 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.stringify
muda 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.
Domínios suportados
-
pix-payments— eventos terminais de Pix outbound (pix_payment.completed,
pix_payment.failed,pix_payment.rejected,pix_payment.error).
Eventos não-terminais (ACCEPTED, PROCESSING) não são enviados — use
GET /v1/banking/accounts/{accountId}/pix-payments/{id}para polling. -
onboarding— eventos de proposta de conta digital: terminais
(onboarding.completed,onboarding.declined,onboarding.failed,
onboarding.canceled) eonboarding.waiting_correction(a Autra
rejeitou dados/arquivos e o portador precisa reenviar — acione o
fluxo de correção no app). Demais estados intermediários
(PROCESS_STARTED, FILES_PENDING, etc.) não disparam webhook —
useGET /v1/banking/onboarding/{id}para acompanhar.
Emonboarding.completed,operationDatacarregadockAccountId,
dockAgencyedockAccountNumberda conta recém-criada. Em
onboarding.waiting_correction,operationData.analysislista os
arquivos rejeitados (invalid_files[]). -
transfers— eventos de TED (Autra Pay). Status terminais separados
por direção:transfer.in.completed/transfer.in.failed(TED
recebida — crédito na conta do cliente) etransfer.out.completed/
transfer.out.failed/transfer.out.canceled(TED enviada pela
conta do cliente). Status intermediários da TED não disparam
webhook.operationDatacarregaoperationInstanceId,externalId,
directionecounterparty(dados da contraparte). -
acquiring— eventos de adquirência (PCH/Autra acquiring) entregues
via SNS pela Autra. Pass-through: forwardamos o payload bruto da
Autra dentro deoperationData.payload. Cobre transações tanto de
maquininha (CREDIT/DEBIT/PREPAID) quanto Pix Checkout (PIX). Event
types:acquiring.financial_transaction,acquiring.payout,
acquiring.financial_cycle,acquiring.payment_slip_notification,
acquiring.merchant_onboarding_status. Roteamento do tenant é por
slugMerchant(Autra) → tabelamerchants; eventos sem merchant
identificável (ORPHAN) não são enviados. -
cards— eventos de cartões (banking). Event types:- Lifecycle (derivados de
global_card):card.created,
card.status_updated,card.pin_updated,card.updated,
card.reissued. - Antifraude (
global_fraud_prevention_actions_cards):
card.fraud_action. - Transações (pass-through, payload bruto em
operationData.payload):card.authorization(de
global_authorization),card.charge(deglobal_charge_document).
Roteamento de tenant:
dock_card_iddo payload → tabelaautra.cards.
Cartões fora do nosso DB viram ORPHAN. Estes eventos NUNCA carregam
PAN ou CVV em claro — sómasked_pan(ex.:421847******4639). - Lifecycle (derivados de
-
dda— Débito Direto Autorizado. Eventos (Autra Notifications Central):dda.bill_status_updated— derivado deglobal_dda_bill_payments.
UM único event_type cobre todo o ciclo do boleto; o BFF discrimina
pelostatus_ddano payload (APRESENTADO|ACEITO|RECUSADO|
AGENDADO|PAGO|BAIXADO). Pass-through: payload bruto Autra
emoperationData.payload(beneficiário, valor, vencimento,
barcode, linha digitável, juros, multa, desconto, etc.).dda.payer_status_updated— derivado deglobal_dda_payer.
Mudança no enrollment do pagador (PENDING|PROCESSING|
ACTIVE|DISABLED|DENIED). Disparado após enable/disable
(que são assíncronos — REST respondePENDINGe este webhook
traz o estado final).
Roteamento de tenant:
account_iddo payload Autra →
autra.banking_accounts.provider_account_uuid. Endpoints REST de
gerência (enable/disable/consult/list-bills) sob
/v1/banking/accounts/{accountId}/dda(ver menuDDA).
Cada domain tem uma subscription independente — registre uma URL por
domínio (podem ser URLs diferentes).
Entrega
- Síncrono inicial + retry automático — Tentativa síncrona ao
gerar o evento (attempt 1). Se falhar (timeout, 5xx, conn refused),
a entrega vai prastate = PENDING_RETRYcomnext_retry_at. O
workerAutra_Webhook_Retry(EventBridge cron 1/min) reenvia até
16 tentativas em ~60 min:- attempts 2–11 →
+1 mincada (10 retries de 1 em 1 min); - attempts 12–16 →
+10 mincada (5 retries de 10 em 10 min); - esgotado →
state = DESPREZADO.
- attempts 2–11 →
- HMAC re-assinado a cada retry com o secret atual da subscription
(rotação de secret reflete nos próximos retries). - Timeout 5s — BFF do cliente deve responder rápido (idealmente
enfileirar internamente e retornar 200 imediato). - Idempotência — cada evento tem
eventIdúnico (UUID v7). Trate
duplicatas no lado do BFF (mesmoeventIdpode chegar até 16 vezes
em caso de retry). - Validação do
X-Autra-Signature— veja a seção "Assinatura HMAC"
acima nesta página (snippets em Node/Java/Python/Go + pegadinhas).
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
400INVALID_URL (precisa https://),
SECRET_TOO_SHORT (mínimo 32 chars, quando secret informado),
INVALID_EVENT_TYPE,
UNSUPPORTED_DOMAIN.
