Recepción de Resultados
El desenlace de una verificación le llega por uno de tres canales. Los tres están firmados con su webhook_secret, y GET /v1/sessions/{session_id} es siempre el registro autoritativo detrás de ellos.
| Canal | Cuándo aplica |
|---|---|
| Webhook | delivery.mode es webhook (el predeterminado) |
| Redirección firmada | delivery.mode es redirect |
| Notificación de resultado | El resultado de una sesión de redirección, un resultado servidor a servidor que cambia después, o cualquier decisión tomada a posteriori |
Modo Webhook
Inyo envía por POST el resultado normalizado a su webhook_url configurada:
POST /kyc-result HTTP/1.1 Content-Type: application/json X-Inyo-Signature: 4c1f9a2e8b7d…
Verifique la Firma
X-Inyo-Signature es un HMAC-SHA256 sobre los bytes crudos del cuerpo de la solicitud, codificado en hexadecimal y con su webhook_secret como clave.
Verifique contra los bytes crudos, antes de parsear el JSON. Parsear y re-serializar cambia los espacios en blanco y el orden de las claves, lo que cambia los bytes, lo que rompe la firma. Este es el fallo de integración más común. Compare en tiempo constante — nunca con
==sobre cadenas.
Node.js (Express):
import express from "express";
import crypto from "node:crypto";
const app = express();
// cuerpo crudo, no 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 lanza una excepción si las longitudes no coinciden, por eso el ejemplo de Node comprueba la longitud primero; hmac.compare_digest lo maneja internamente.
Garantías de Entrega
| Propiedad | Comportamiento |
|---|---|
| Confirmación | Cualquier respuesta 2xx. Cualquier otra cosa cuenta como fallo |
| Reintentos | Hasta 3 intentos con backoff (aproximadamente 1 s, 4 s, 10 s) |
| Después de 3 fallos | La entrega se descarta y no se reintenta más adelante |
| Tiempo de espera | 15 segundos por intento |
La entrega es de mejor esfuerzo. Responda 2xx rápidamente y procese de forma asíncrona — un handler lento consume el tiempo de espera y convierte una verificación exitosa en una entrega descartada. Si necesita una garantía en lugar de un push, sondee GET /v1/sessions/{session_id}.
Haga su handler idempotente. El resultado de la misma sesión puede llegar legítimamente más de una vez: un reintento después de que su 2xx se perdió en tránsito, o un desenlace genuinamente más nuevo (vea ordenamiento).
Modo Redirección
Cuando la sesión se creó con delivery.mode: "redirect", el cliente es devuelto a su redirect_url con el desenlace en la cadena de consulta:
https://you.example/kyc-done?session_id=9f1c8e42…&status=approved&sig=7b3e1d…
| Parámetro | Descripción |
|---|---|
session_id | La sesión que se completó |
status | approved, declined, expired o in_review |
sig | HMAC-SHA256 en hexadecimal de la cadena "<session_id>.<status>", con su webhook_secret como clave |
Verifique la Firma de la Redirección
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 está dentro de la firma, un cliente no puede reescribir declined a approved, y una sesión retenida no puede reproducirse como una aprobación. Una firma que no se verifica significa que los parámetros fueron manipulados — rechace el aterrizaje por completo.
El status en una Redirección Puede Ser in_review
Una sesión retenida devuelve igualmente a su cliente a usted, en lugar de dejarlo varado en una pantalla de "en revisión". Su página de aterrizaje debe manejar in_review como un estado pendiente — no como un fallo, y no como una aprobación. La decisión final del analista no estará en la redirección con la que el cliente llegó; le llega a través de una notificación de resultado o mediante sondeo.
Notificaciones de Resultado
Una redirección viaja de vuelta en el navegador de su cliente, y una verificación servidor a servidor responde en un cuerpo de respuesta. Ninguna de las dos sobrevive a un cambio posterior — y los desenlaces cambian, cuando un analista decide una sesión en cola o una decisión de control de calidad revierte una completada.
Así que cuando tiene un webhook_url configurado, los resultados también se envían por POST a él — firmados exactamente igual que la entrega en modo webhook — para las sesiones cuya entrega de cara al cliente es otra:
| Situación | Qué recibe |
|---|---|
| Se completa una sesión en modo redirección | El resultado inicial, incluyendo un in_review no terminal que la redirección misma no puede transportar |
| Cualquier cambio posterior | El resultado actualizado, con result.manual_review |
La respuesta original de POST /v1/verifications | Nada — el 201 fue la entrega |
Las sesiones en modo webhook no se ven afectadas: ahí el webhook es la entrega, no una notificación sobre ella.
Ordenamiento de Notificaciones Concurrentes
Cada notificación lleva una marca de tiempo 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"
}
Una sesión puede tener dos entregas en vuelo a la vez — una todavía reintentándose mientras se despacha una decisión más nueva — así que pueden llegar fuera de orden.
Aplique el
notified_atmás alto e ignore cualquier cosa más antigua. No confíe en el orden de llegada.
notified_at sella la entrega, no el resultado almacenado, por lo que está ausente del resultado que lee vía GET /v1/sessions/{session_id} — ese endpoint siempre devuelve el estado actual, así que no hay nada que ordenar.
Cómo Desactivarlas
Las notificaciones de resultado están activadas por defecto. Pueden desactivarse para su tenant, dejando la redirección del navegador o la respuesta síncrona como su único canal — vea Configuración del Tenant. Habilitar las notificaciones sin un webhook_url es rechazado en lugar de ignorado silenciosamente.
Elección de una Estrategia
| Su situación | Enfoque recomendado |
|---|---|
| Flujo de onboarding del lado del servidor | Modo webhook, con verificación de firma y un handler idempotente |
| Flujo web donde el cliente debe continuar de inmediato | Modo redirección para el cliente, más notificaciones como registro de la verdad |
| UI de captura propia | Servidor a servidor, más un webhook_url para que las decisiones posteriores le lleguen |
| Crítico para cumplimiento, no puede perder un desenlace | Cualquiera de los anteriores más un trabajo de conciliación que sondee GET /v1/sessions/{session_id} para las sesiones sin un estado terminal |
Próximos Pasos
- Comprobaciones y Decisiones — lectura del payload que acaba de verificar
- Revisión Manual — qué significan
in_reviewymanual_review
