API de anclaje KYC en EVM
Tu exchange envia solo referencias opacas y hashes. El sistema nunca recibe ni almacena datos personales: la cadena guarda unicamente subject_ref y payload_hash.
Base URL
La API vive en esta aplicacion (TanStack server routes), no en la URL de Supabase. Supabase se usa internamente como base de datos.
Base URL: https://kyc.twinbot.io/api/public/v1Autenticacion (HMAC SHA-256)
Cada peticion lleva la key publica y una firma calculada con el secreto entregado una sola vez al crear la API key en el panel.
Cabeceras:
X-Api-Key: kyc_xxxxxxxxx
X-Timestamp: 1717171717 (epoch segundos, tolerancia +/- 300s)
X-Signature: <hex hmac-sha256>
base = `${timestamp}.${METHOD}.${path}.${sha256_hex(body)}`
signature = hmac_sha256(api_secret, base) -> hex
// Ejemplo Node.js
import { createHash, createHmac } from "node:crypto";
const body = JSON.stringify({ subject_ref, payload_hash, kyc_level: 2 });
const ts = Math.floor(Date.now() / 1000).toString();
const path = "/api/public/v1/kyc/stamp";
const base = `${ts}.POST.${path}.${createHash("sha256").update(body).digest("hex")}`;
const sig = createHmac("sha256", API_SECRET).update(base).digest("hex");Como calcular los hashes (lado exchange)
subject_ref = keccak256/sha256( user_id_interno + PEPPER_SECRETO ) -> bytes32 0x...
payload_hash = sha256( JSON canonico del expediente KYC ) -> bytes32 0x...
Nunca envies nombre, documento, foto, direccion ni email.Endpoints
POST /api/public/v1/kyc/stamp
scope requerido: stamp
{
"subject_ref": "0x…64 hex",
"payload_hash": "0x…64 hex",
"kyc_level": 2,
"country_code": "AR", // opcional, ISO-3166 alfa-2
"provider": "sumsub", // opcional
"verified_at": "2026-08-02T12:00:00Z",
"idempotency_key": "kyc-9981" // opcional
}
202 -> { "anchor_id": "uuid", "status": "queued" }POST /api/public/v1/kyc/revoke
scope requerido: stamp
{ "subject_ref": "0x…", "reason_hash": "0x…" }
202 -> { "anchor_id": "uuid", "status": "revoke_queued" }GET /api/public/v1/kyc/{subject_ref}
scope requerido: read
200 -> { "subject_ref": "0x…", "anchors": [ { status, tx_hash, block_number, batch_id, … } ] }GET /api/public/v1/kyc/{subject_ref}/proof
scope requerido: read
200 -> { leaf_hash, leaf_index, proof: ["0x…"], batch: { merkle_root, tx_hash, … } }POST /api/public/v1/kyc/verify
scope requerido: read
{ "subject_ref": "0x…", "payload_hash": "0x…" }
200 -> { "valid": true, "stamped_at": 1717171717, "contract": "0x…", "chain_id": 1337 }GET /api/public/v1/batches/{batch_id}
scope requerido: read
200 -> { merkle_root, leaf_count, status, tx_hash, block_number, period_start, period_end }GET /api/public/v1/health
scope requerido: publico
200 -> { status: "ok", pending_anchors: 3, chain: { connected: true, chain_id, block } }Webhooks hacia tu exchange
Se envian con reintentos exponenciales. Verifica la firma antes de procesar.
Cabeceras salientes:
X-Webhook-Timestamp: 1717171717
X-Webhook-Signature: hex hmac_sha256(webhook_secret, `${timestamp}.${body}`)
X-Webhook-Event: kyc.anchor.confirmed
Eventos:
kyc.anchor.confirmed { subject_ref, payload_hash, tx_hash, block_number, chain_id, contract }
kyc.anchor.failed { subject_ref, error }
kyc.anchor.revoked { subject_ref, tx_hash }
kyc.batch.committed { batch_id, merkle_root, leaf_count, tx_hash }
Responde 2xx para confirmar la entrega.Codigos de error
401 firma invalida / key inexistente o revocada
403 scope insuficiente
409 idempotency_key repetida con distinto payload
422 payload invalido (hashes deben ser 0x + 64 hex)
429 limite de peticiones por minuto excedido
503 sin red o contrato activoProceso periodico
Un cron llama cada minuto a /api/public/v1/cron/tick (protegido con secreto interno) para enviar transacciones, confirmar recibos y reintentar webhooks; cada hora cierra y ancla el lote Merkle.