Recebendo Resultados
O desfecho de uma verificação chega até você por um de três canais. Todos os três são assinados com seu webhook_secret, e GET /v1/sessions/{session_id} é sempre o registro oficial por trás deles.
| Canal | Quando se aplica |
|---|---|
| Webhook | delivery.mode é webhook (o padrão) |
| Redirect assinado | delivery.mode é redirect |
| Notificação de resultado | O resultado de uma sessão de redirect, um resultado server-to-server que muda depois, ou qualquer decisão tomada a posteriori |
Modo Webhook
A Inyo faz um POST do resultado normalizado para o seu webhook_url configurado:
POST /kyc-result HTTP/1.1 Content-Type: application/json X-Inyo-Signature: 4c1f9a2e8b7d…
Verifique a Assinatura
X-Inyo-Signature é um HMAC-SHA256 codificado em hexadecimal sobre os bytes brutos do corpo da requisição, com a chave sendo seu webhook_secret.
Verifique contra os bytes brutos, antes de fazer o parse do JSON. Fazer parse e reserializar altera espaços em branco e ordem de chaves, o que altera os bytes, o que quebra a assinatura. Esta é a falha de integração mais comum de todas. Compare em tempo constante — nunca com
==em strings.
Node.js (Express):
import express from "express";
import crypto from "node:crypto";
const app = express();
// corpo bruto, não express.json()
app.post(
"/kyc-result",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.get("X-Inyo-Signature") ?? "";
const expected = crypto
.createHmac("sha256", process.env.KYC_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const expectedBuf = Buffer.from(expected, "utf8");
const signatureBuf = Buffer.from(signature, "utf8");
if (
expectedBuf.length !== signatureBuf.length ||
!crypto.timingSafeEqual(expectedBuf, signatureBuf)
) {
return res.sendStatus(401);
}
const result = JSON.parse(req.body.toString("utf8"));
handleResult(result);
res.sendStatus(200);
}
);
Python (FastAPI):
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
@app.post("/kyc-result")
async def kyc_result(request: Request, x_inyo_signature: str = Header(default="")):
raw = await request.body()
expected = hmac.new(
os.environ["KYC_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, x_inyo_signature.strip()):
raise HTTPException(status_code=401, detail="invalid signature")
handle_result(json.loads(raw))
return {"ok": True}
crypto.timingSafeEqual lança exceção quando os comprimentos diferem, então o exemplo em Node verifica o comprimento primeiro; hmac.compare_digest trata isso internamente.
Garantias de Entrega
| Propriedade | Comportamento |
|---|---|
| Confirmação | Qualquer resposta 2xx. Qualquer outra coisa conta como falha |
| Retries | Até 3 tentativas com backoff (aproximadamente 1s, 4s, 10s) |
| Após 3 falhas | A entrega é descartada e não é tentada novamente depois |
| Timeout | 15 segundos por tentativa |
A entrega é best-effort. Responda 2xx rapidamente e processe de forma assíncrona — um handler lento consome o timeout e transforma uma verificação bem-sucedida em uma entrega descartada. Se você precisa de uma garantia em vez de um push, faça polling em GET /v1/sessions/{session_id}.
Torne seu handler idempotente. O resultado da mesma sessão pode legitimamente chegar mais de uma vez: um retry depois que seu 2xx se perdeu no caminho, ou um desfecho genuinamente mais novo (veja ordenação).
Modo Redirect
Quando a sessão foi criada com delivery.mode: "redirect", o cliente é devolvido à sua redirect_url com o desfecho na query string:
https://you.example/kyc-done?session_id=9f1c8e42…&status=approved&sig=7b3e1d…
| Parâmetro | Descrição |
|---|---|
session_id | A sessão que foi concluída |
status | approved, declined, expired ou in_review |
sig | Hex do HMAC-SHA256 da string "<session_id>.<status>", com a chave sendo seu webhook_secret |
Verifique a Assinatura do Redirect
import hashlib
import hmac
def valid_redirect(session_id: str, status: str, sig: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), f"{session_id}.{status}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
Como status faz parte da assinatura, um cliente não pode reescrever declined como approved, e uma sessão retida não pode ser reproduzida como aprovação. Uma assinatura que não valida significa que os parâmetros foram adulterados — rejeite o acesso por completo.
O status em um Redirect Pode Ser in_review
Uma sessão retida ainda devolve seu cliente a você em vez de abandoná-lo em uma tela de "em análise". Sua página de destino deve tratar in_review como um estado pendente — nem como falha, nem como aprovação. A decisão final do analista não estará no redirect com o qual o cliente chegou; ela chega até você por uma notificação de resultado ou por polling.
Notificações de Resultado
Um redirect viaja de volta no navegador do seu cliente, e uma verificação server-to-server responde em um corpo de resposta. Nenhum dos dois sobrevive a uma mudança posterior — e desfechos mudam, quando um analista decide uma sessão enfileirada ou uma decisão de controle de qualidade reverte uma sessão concluída.
Então, quando você tem um webhook_url configurado, os resultados também são enviados via POST para ele — assinados exatamente como na entrega em modo webhook — para sessões cuja entrega voltada ao cliente é outra:
| Situação | O que você recebe |
|---|---|
| Sessão em modo redirect é concluída | O resultado inicial, incluindo um in_review não terminal que o próprio redirect não consegue carregar |
| Qualquer mudança posterior | O resultado atualizado, carregando result.manual_review |
A resposta original de POST /v1/verifications | Nada — o 201 foi a entrega |
Sessões em modo webhook não são afetadas: ali o webhook é a entrega, não uma notificação sobre ela.
Ordenando Notificações Concorrentes
Toda notificação carrega um timestamp notified_at:
{
"session_id": "9f1c8e42…",
"status": "approved",
"auto_status": "declined",
"manual_review": { "action": "review", "reviewer": "analyst-7", "reason": "…", "auto_status": "declined" },
"notified_at": "2026-07-31T14:41:09.882Z"
}
Uma sessão pode ter duas entregas em trânsito ao mesmo tempo — uma ainda em retry enquanto uma decisão mais nova é despachada — então elas podem chegar fora de ordem.
Aplique o maior
notified_ate ignore qualquer coisa mais antiga. Não confie na ordem de chegada.
notified_at carimba a entrega, não o resultado armazenado, então ele está ausente do resultado que você lê via GET /v1/sessions/{session_id} — esse endpoint sempre retorna o estado atual, então não há nada a ordenar.
Desativando
As notificações de resultado vêm ativadas por padrão. Elas podem ser desativadas para o seu tenant, deixando o redirect do navegador ou a resposta síncrona como seu único canal — veja Configuração do Tenant. Ativar notificações sem um webhook_url é rejeitado em vez de ignorado silenciosamente.
Escolhendo uma Estratégia
| Sua situação | Abordagem recomendada |
|---|---|
| Fluxo de onboarding server-side | Modo webhook, com verificação de assinatura e um handler idempotente |
| Fluxo web em que o cliente deve continuar imediatamente | Modo redirect para o cliente, mais notificações para o registro de verdade |
| UI de captura própria | Server-to-server, mais um webhook_url para que decisões posteriores cheguem até você |
| Crítico para conformidade, não pode perder um desfecho | Qualquer uma das opções acima mais um job de reconciliação fazendo polling em GET /v1/sessions/{session_id} para sessões sem status terminal |
Próximos Passos
- Verificações e Decisões — interpretando o payload que você acabou de verificar
- Revisão Manual — o que
in_reviewemanual_reviewsignificam
