Inyo

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.

CanalCuándo aplica
Webhookdelivery.mode es webhook (el predeterminado)
Redirección firmadadelivery.mode es redirect
Notificación de resultadoEl 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

PropiedadComportamiento
ConfirmaciónCualquier respuesta 2xx. Cualquier otra cosa cuenta como fallo
ReintentosHasta 3 intentos con backoff (aproximadamente 1 s, 4 s, 10 s)
Después de 3 fallosLa entrega se descarta y no se reintenta más adelante
Tiempo de espera15 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ámetroDescripción
session_idLa sesión que se completó
statusapproved, declined, expired o in_review
sigHMAC-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ónQué recibe
Se completa una sesión en modo redirecciónEl resultado inicial, incluyendo un in_review no terminal que la redirección misma no puede transportar
Cualquier cambio posteriorEl resultado actualizado, con result.manual_review
La respuesta original de POST /v1/verificationsNada — 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_at má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ónEnfoque recomendado
Flujo de onboarding del lado del servidorModo webhook, con verificación de firma y un handler idempotente
Flujo web donde el cliente debe continuar de inmediatoModo redirección para el cliente, más notificaciones como registro de la verdad
UI de captura propiaServidor a servidor, más un webhook_url para que las decisiones posteriores le lleguen
Crítico para cumplimiento, no puede perder un desenlaceCualquiera 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