Inyo

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.

CanalQuando se aplica
Webhookdelivery.mode é webhook (o padrão)
Redirect assinadodelivery.mode é redirect
Notificação de resultadoO 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

PropriedadeComportamento
ConfirmaçãoQualquer resposta 2xx. Qualquer outra coisa conta como falha
RetriesAté 3 tentativas com backoff (aproximadamente 1s, 4s, 10s)
Após 3 falhasA entrega é descartada e não é tentada novamente depois
Timeout15 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âmetroDescrição
session_idA sessão que foi concluída
statusapproved, declined, expired ou in_review
sigHex 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çãoO que você recebe
Sessão em modo redirect é concluídaO resultado inicial, incluindo um in_review não terminal que o próprio redirect não consegue carregar
Qualquer mudança posteriorO resultado atualizado, carregando result.manual_review
A resposta original de POST /v1/verificationsNada — 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_at e 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çãoAbordagem recomendada
Fluxo de onboarding server-sideModo webhook, com verificação de assinatura e um handler idempotente
Fluxo web em que o cliente deve continuar imediatamenteModo redirect para o cliente, mais notificações para o registro de verdade
UI de captura própriaServer-to-server, mais um webhook_url para que decisões posteriores cheguem até você
Crítico para conformidade, não pode perder um desfechoQualquer 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