Gate transacional
Gate transacional
O módulo transacao avalia a transação que você nos envia e devolve allow, step_up ou deny, com as razões e os pontos de risco de cada sinal que disparou. Ele olha valor fora do padrão do próprio titular, cadência, contraparte nova ou concentradora, troca de aparelho, horário, e se aquele titular já foi verificado por você aqui. Roda sobre o seu histórico, sem consórcio com outros clientes.
deny é a nossa recomendação, não um bloqueio: o módulo é portão suave e nunca recusa a verificação sozinho.Disponível desde 11 de setembro de 2026. Ele aparece na tabela de preços e no GET /v1/capabilities com preço e status available, pode ser ligado num flow, e a ingestão de transações deixou de responder 422 transaction_not_supported em flow que o contenha. Até essa data as duas travas eram o preço e o registro do módulo como consumidor de transação, e elas caíram juntas: abrir só uma teria posto o módulo à venda com a porta de entrada ainda fechada.
Num flow que contenha o gate, o lote não é aceito: transactions[] responde 422 batch_not_supported_for_gate, e só a forma unitária transaction é avaliada. A razão é de produto: um gate síncrono decide um pagamento, e um lote não teria veredito. Isso não quebra integração nenhuma, e o argumento é verificável: até esta data nenhum flow podia conter o módulo, porque a criação de flow o recusava, então a regra nasce junto com a possibilidade.
Só o movimento que o próprio titular iniciou, e que não falhou, recebe veredito. São deposit, withdraw, transfer, payment e bet, com status confirmed ou pending. settlement e reversal são o ciclo de vida de um pagamento que já foi julgado, bet_profit e bet_loss são o resultado que a casa apurou, status: failed é dinheiro que não se moveu, e o evento de backfill que chega fora da janela é um pagamento que você já liquidou. Nenhum deles tem uma pergunta em aberto: todos continuam sendo ingeridos normalmente, entram na trilha, na retenção e no monitoramento, e simplesmente não geram verificação nem cobrança.
Duas escalas convivem aqui, e elas apontam para lados opostos. risk_score vai de 0 a 100 com 100 sendo o pior, e é o eixo do gate. O score do check ao lado é o de negócio, e é ternário e fixo: allow vale 100, step_up vale 40 e deny vale 0. Um não é o complemento do outro, e a conta 100 menos risk_score não reproduz o segundo.
// transacao: step_up. As contribuições dizem quantos pontos de risco cada sinal que
// disparou somou. Os números deste exemplo são ilustrativos.
{ "module": "transacao", "passed": null, "outcome": "pending", "score": 40,
"data": { "transaction": {
"verdict": "step_up", // allow | step_up | deny
"risk_score": 70, // 0..100 (100 é o PIOR) = a SOMA das contributions, com teto em 100
"reasons": ["amount_above_profile", "new_counterparty", "night_window"],
// ordenadas por pontos desc; empate desempata pelo nome do sinal, em ordem alfabética
"contributions": [ { "signal": "amount_above_profile", "points": 31 },
{ "signal": "new_counterparty", "points": 22 },
{ "signal": "night_window", "points": 17 } ],
"confidence": 0.3, // 0..1, e NÃO é probabilidade de fraude
"feature_set_version": "tx-fs_90adbec7ef08",
"calibration_version": "tx-cal_9f2c1a4b7e03" } } }
// deny: é PORTÃO determinístico, não soma de pesos. Por isso "contributions" vem vazio
// e o risk_score 100 é carimbo, não cálculo.
{ "module": "transacao", "passed": false, "outcome": "failed", "score": 0,
"data": { "transaction": { "verdict": "deny", "risk_score": 100,
"reasons": ["blocklist_hit"], "contributions": [],
"confidence": 0.3,
"feature_set_version": "tx-fs_90adbec7ef08",
"calibration_version": "tx-cal_9f2c1a4b7e03" } } }
// allow, perfil maduro e payload completo -> confidence alta
{ "module": "transacao", "passed": true, "outcome": "approved", "score": 100,
"data": { "transaction": { "verdict": "allow", "risk_score": 0,
"reasons": [], "contributions": [], "confidence": 1,
"feature_set_version": "tx-fs_90adbec7ef08",
"calibration_version": "tx-cal_9f2c1a4b7e03" } } }A resposta síncrona sempre vem, e para a mesma pergunta ela é sempre a mesma. Num flow com o gate, todo evento avaliável recebe verdict na própria chamada. Isso inclui o seu retry: repetir o mesmo external_id devolve o veredito que já foi dado na primeira vez, sem reavaliar e sem uma segunda cobrança. É o comportamento que um caminho de pagamento precisa, porque o retry de rede ali é rotina e não exceção. O external_id é único por organização e ambiente, e o veredito dele também: se o mesmo external_id for enviado por outro flow seu, a resposta é o veredito que já foi dado, sem nova avaliação e sem nova cobrança.
No sandbox o gate também decide, com a mesma forma de resposta da produção e sem gravar nada. O veredito de sandbox é determinístico e você escolhe o ramo pelos dois últimos dígitos de amount_cents. Ele sai só do valor: no sandbox o gate não consulta perfil nem lista de bloqueio, porque ali ele existe para você exercitar os três ramos do seu código, e não para julgar titular.
// SANDBOX: o veredito sai do valor, para você exercitar os três ramos do seu código.
// ...13 -> deny (ex.: amount_cents 10013, 4513, 13)
// ...07 -> step_up (ex.: amount_cents 10007, 4507, 7)
// resto -> allow
POST /v1/verification-sessions (chave sk_test_)
{ "flow_id": "flow_...", "reference_id": "cliente-4821",
"transaction": { "type": "payment", "amount_cents": 10007, "external_id": "pay-1" } }
201
{ "id": "ing_...", "status": "consumed",
"ingest": { "batch_id": "ing_...", "accepted": 1, "duplicated": 0, "batch_size": 1,
"first_seen_at": "2026-09-20T12:00:00.000Z" },
"transacao": { "verdict": "step_up" } }Trate step_up como o caminho normal, não como o raro. Ele é a categoria que pede uma prova a mais antes de liberar o dinheiro, e é sempre a resposta quando o gate prefere não afirmar. Uma integração que só trata allow e deny está com um ramo em aberto.
Step-up com prova de humano (em breve). Quando o flow do gate tem um flow de prova configurado (step_up_flow_id, com um flow de reserva opcional em step_up_fallback_flow_id) e o veredito é step_up, o bloco transacao passa a trazer step_up. Com available: true, session_id é a sessão de PROVA, válida por 600 segundos: monte o widget com ela, como na criação de sessão, e o titular aprova o pagamento com a passkey da conta dele ou com a reautenticação facial, conforme o fator que a conta tem (factor). O resultado chega no webhook da sessão de prova, com data.step_up_of: source é o gate que pediu a prova, verification_id é a verificação do pagamento e event_type é o tipo do evento. É por ele que você liga o desfecho da prova ao pagamento que estava esperando. A verificação do pagamento não muda por causa da prova. Com available: false, reason diz por quê (por exemplo no_factor_enrolled ou insufficient_credit) e você aplica o seu próprio desafio. Repetir o mesmo external_id devolve o mesmo step-up enquanto o pedido estiver aberto, e step_up_closed depois. No sandbox a ingestão não abre sessão de prova: o bloco vem com step_up_sandbox, na mesma forma da produção. Os módulos de prova estão com a venda pausada, então a configuração só fica disponível quando eles abrirem.
A sua lista de bloqueio vale aqui, em produção. Quando o reference_id da transação está na lista, o veredito é deny, com blocklist_hit em reasons. Envie sempre o reference_id do titular junto da transação: é por ele que a sua lista é consultada. No sandbox, como o veredito vem do valor, use o gatilho de amount_cents para exercitar o ramo de deny do seu código.
O reference_id e o counterparty_ref são o sujeito e o destino, e nos dois a maiúscula não muda quem é quem. User_42, user_42 e USER_42 são a mesma pessoa, e o mesmo vale para o destino: o histórico, a janela de análise e a sua lista de bloqueio enxergam um titular só. Isso importa porque um id que muda de grafia entre dois caminhos do seu backend (um derivado de e-mail, outro digitado num formulário) partiria o histórico em dois pedaços pequenos, e histórico curto derruba a confidence do veredito. O external_id é o oposto e de propósito: ele é o token do evento, comparado byte a byte, então TX-1 e tx-1 são duas transações diferentes.
A janela de análise é contada pelo relógio do nosso servidor. O occurred_at que você manda continua valendo, e é ele que descreve quando o pagamento aconteceu no seu sistema; quem decide em que janela o evento entra é o momento em que ele chega aqui. Consequência prática para a sua integração: se você faz carga retroativa, mande-a de uma vez e não espere que ela reescreva a análise de semanas passadas, porque a análise é do que chegou.
reasons pode conter razão que não pontua, e isso é de propósito. Uma razão pode declarar contexto sobre a avaliação sem somar risco, e nesse caso ela aparece em reasons e não em contributions: nada é tratado como "limpo por omissão". Quem somar o tamanho de reasons como se fosse risco erra. A conta de verdade é contributions[].points: os pontos de risco de cada sinal que disparou, e a soma deles, com teto em 100, é o risk_score.
confidence não é probabilidade de fraude. É o quanto o veredito merece crédito, composto de maturidade do perfil daquele titular, completude do payload que você mandou e nitidez da contraparte. Perfil novo derruba a confiança mesmo com veredito allow. Use os dois juntos: step_up com confidence baixa é "desconfio, mas sei pouco", e merece tratamento diferente de step_up com confiança alta.
O que não sai daqui, e é deliberado: o limiar que separa as bandas, e o valor, a contraparte e o documento da transação. Publicar o limiar vigente transformaria o payload num oráculo contra o próprio gate. Pela mesma razão, a resposta síncrona da ingestão devolve o verdict e nada mais: as razões e as contribuições viajam só aqui, no webhook, que é superfície assinada e auditável. E calibration_version é o rastro dessa calibração: tx-cal_ mais doze caracteres em produção, e a string literal "sandbox" no ambiente de testes.
feature_set_version e calibration_version são strings opacas: guarde, nunca interprete. Elas servem para uma coisa só, e é uma coisa valiosa: reler um veredito de meses atrás sabendo que ele saiu sob exatamente aquela política. O prefixo é estável (tx-fs_ e tx-cal_); o que vem depois muda sempre que a política muda, e não carrega significado que você possa decodificar. Os valores que aparecem nos exemplos acima são ilustrativos: não compare o seu payload com eles, não os fixe em teste e não condicione comportamento a um valor específico. O que você deve fazer é guardá-los junto do desfecho, e comparar um veredito com outro veredito.
O contrato do evento: o ciclo de vida. Um movimento que você nos manda com status confirmed (o padrão) é um movimento que aconteceu. settlement liquida um evento seu que chegou pending, e quem decide a liquidação é o banco de dados, nunca a ordem de chegada: a segunda liquidação do mesmo alvo responde 409 already_settled, e uma liquidação cujo settles não aponta um evento seu, no mesmo ambiente, responde 422 unknown_settles_reference. Liquidação e reversão nunca chegam pendentes: status: pending em uma delas é 422 settlement_status_invalid. reversal nunca altera o evento revertido, que continua contando como o movimento que de fato aconteceu; a reversão é um evento próprio, e ela conta na próxima avaliação daquele titular. Em qualquer um dos dois, settles é o external_id do alvo. E status: pending só entra em flow cujo módulo aceite o ciclo pendente; nos demais ele responde 422 pending_lifecycle_not_enabled. Hoje nenhum dos módulos que consomem transação aceita o ciclo pendente: mande o evento já confirmado e, se ele for desfeito, mande a reversão.
// Reversão: um evento próprio, que aponta o alvo por settles (o external_id do alvo).
// O alvo não muda; a reversão conta na próxima avaliação deste titular.
POST /v1/verification-sessions
{ "flow_id": "flow_...", "reference_id": "cliente-4821",
"transaction": { "type": "payment", "amount_cents": 64000, "external_id": "pay-77",
"counterparty_ref": "loja-903" } }
{ "flow_id": "flow_...", "reference_id": "cliente-4821",
"transaction": { "type": "reversal", "amount_cents": 64000, "external_id": "pay-77-rev",
"settles": "pay-77" } }
// Liquidação: só de um alvo que chegou pending, em flow cujo módulo aceite o ciclo pendente.
{ "flow_id": "flow_...", "reference_id": "cliente-4821",
"transaction": { "type": "settlement", "amount_cents": 64000, "external_id": "dep-12-liq",
"settles": "dep-12" } }
// segunda liquidação ou reversão do mesmo alvo -> 409 already_settled
// settles sem evento seu no mesmo ambiente -> 422 unknown_settles_reference
// liquidação ou reversão com status pending -> 422 settlement_status_invalidOs três ids são opacos, e cada um tem um papel. reference_id é o titular, e precisa ser estável por pessoa: é por ele que o histórico, o perfil e a sua lista de bloqueio são lidos. external_id é o token de idempotência do evento, comparado byte a byte: é ele que faz o seu retry devolver o mesmo veredito sem uma segunda cobrança, e ele é obrigatório (sem ele, 422 external_id_required). counterparty_ref é o destino. Nenhum dos três aceita dado pessoal: um caractere fora do conjunto aceito é 422 reference_id_charset (a mensagem nomeia o campo recusado), e um valor com forma de dado pessoal é 422 pii_shaped_value. Número de pessoa (CPF, telefone) nunca é referência, nem escrito sem máscara: use um id do seu sistema. O corpo do erro nunca devolve o valor recusado, só o campo e a posição no lote.
device é minimizado antes de ser guardado. Do IP fica só o prefixo de rede (/24 em IPv4, /48 em IPv6) e um derivado comparável; do fingerprint, só o derivado. Nem o IP nem o fingerprint ficam em claro, e a telemetria de aparelho tem prazo próprio, mais curto que o do evento. Mande os dois quando tiver: eles alimentam os sinais de aparelho da avaliação e nada além disso.
direction diz para que lado o dinheiro foi, do ponto de vista do titular. in entrou na conta dele, out saiu. O campo é opcional e, ausente, fica registrado como não informado: nós nunca deduzimos a direção do type, porque uma transfer pode ser as duas coisas. Qualquer outro valor é 400 validation_error. No monitoramento de PLD/FT, uma transfer ou um payment sem direction conta como saída: mande in quando o titular recebe.
instrument identifica o meio de pagamento do titular, e nunca envie o número do cartão. O bloco tem kind (card, account, wallet ou pix_key) e ref, um valor opaco seu: o token que o seu PSP já devolve, ou um HMAC que você calcula. Guardamos só o tipo e um derivado comparável do ref, nunca o valor que chegou. Um ref com forma de número de cartão (13 a 19 dígitos que fecham o dígito verificador, com ou sem separador), de CPF, CNPJ, e-mail, telefone ou chave Pix é 422 pii_shaped_value, sem o valor no corpo do erro. Não existe campo de código de segurança nem de validade, e mandar um é 400 validation_error. Se o seu token do PSP for só de dígitos, prefira mandar um HMAC dele: uma sequência numérica pode fechar o dígito verificador por acaso e ser recusada.
cash diz que a operação foi em espécie, dinheiro vivo. É booleano, o padrão é false, e não existe outro jeito de marcar espécie: o method continua sendo o meio eletrônico. Mande true só quando a operação foi mesmo em espécie, porque é ele que as regras de espécie da PLD/FT pela regra da norma leem.
{ "flow_id": "flow_...", "reference_id": "cliente-4821",
"transaction": { "type": "transfer", "amount_cents": 64000, "external_id": "tr-91",
"direction": "out", "cash": false,
"instrument": { "kind": "card", "ref": "tok_1Nv0aB2eZvKYlo2C" } } }Os tetos, cada um com o seu código. amount_cents acima do teto de fábrica é 422 amount_too_large; moeda diferente de BRL é 422 currency_not_supported; occurred_at além da janela de importação é 422 event_too_old (dentro dela, o evento antigo entra normalmente e não recebe veredito, como dito acima); e o teto diário de eventos por organização é 429 daily_ingest_cap_reached, sem Retry-After porque o balde é o dia: pause, retome no dia seguinte, e saiba que o lote recusado não gravou nada. Num flow que contenha o gate, a chamada sem o bloco transaction responde 422 transaction_required, tanto na criação de sessão quanto na emissão de link hospedado: esse flow só recebe transação, e uma sessão de widget não teria o que perguntar a ele.
O flow do gate é um flow só dele. POST /v1/flows e PATCH /v1/flows/{id} recusam com 422 sync_gate_module_exclusive uma composição que junte o gate a qualquer outro módulo. A razão é a mesma que faz o gate decidir na própria chamada: a requisição que carrega o bloco transaction responde na hora e não abre jornada de captura, então um módulo de documento ou de biometria nesse flow nunca teria por onde rodar. Mantenha o seu KYC no flow que você já tem e crie um flow separado só com o gate. Os dois flows convivem no mesmo reference_id: é por ele que o gate encontra a identidade aprovada do titular.
// Os doze códigos da ingestão de transação, além dos da criação de sessão: // 422 transaction_required flow com o gate e chamada sem o bloco (sessão e link hospedado) // 422 external_id_required evento sem o token de idempotência // 422 amount_too_large acima do teto de fábrica // 422 currency_not_supported só BRL // 422 event_too_old occurred_at além da janela de importação // 422 reference_id_charset id opaco com caractere fora do conjunto aceito (a mensagem nomeia o campo) // 422 pii_shaped_value id opaco com forma de dado pessoal // 422 pending_lifecycle_not_enabled status pending em flow cujo módulo não aceita o ciclo pendente // 422 settlement_status_invalid liquidação ou reversão com status pending // 422 unknown_settles_reference settles sem evento seu no mesmo ambiente // 409 already_settled segunda liquidação ou reversão do mesmo alvo // 429 daily_ingest_cap_reached teto diário por organização; sem Retry-After, retome no dia seguinte
Pronto para integrar? A chave de sandbox sai no painel, logo depois do cadastro. Criar conta grátis