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.

Ir al panel admin

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/v1

Autenticacion (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 activo

Proceso 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.