Base URL: https://api.anchorum.co/api/v1

📦 Quickstart SDK — 30 segundos

El Anchorum SDK encapsula todo el flujo de certificación y verificación directamente en el navegador. Sin npm, sin bundler, sin configurar nada. Un <script> y listo.

💡
¿Cuándo usar el SDK vs. la API directa?
Usa el SDK si tienes un frontend (HTML/JS): el archivo se hashea en el navegador del usuario, la API Key vive en tu servidor, el archivo nunca transita por la red.
Usa la API directa si certificas desde un backend (Python, Node, PHP) donde no hay navegador.

Importar

<!-- Sin npm, sin bundler, compatible con cualquier stack -->
<script src="https://api.anchorum.co/assets/anchorum-sdk.js"></script>

Instanciar

// Sin API Key en el frontend — la key vive en tu servidor backend
const anchorum = new AnchorumClient({
  apiBase: 'https://api.anchorum.co/api/v1'
});

// O con API Key si el frontend es solo tuyo (no público)
const anchorum = new AnchorumClient({
  apiKey: 'anc_tu_api_key'
});

Flujo completo: normalizar → enviar hashes → sello Bitcoin

// 1. El usuario selecciona un archivo
const file = document.querySelector('#file-input').files[0];

// 2. El SDK extrae y hashea el documento LOCALMENTE (sin subir el archivo)
const norm = await anchorum.normalizeFile(file, 'anchorum-tree-v3');
// norm = { hash_fisico, hash_robusto, paragraphs: [...], normalize_algo }

// 3. Envía SOLO los hashes a tu backend (archivo nunca sale del navegador)
const r = await fetch('/api/certify', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filename:         file.name,
    content:          norm.hash_fisico,
    hash_robusto:     norm.hash_robusto,
    normalize_algo:   norm.normalize_algo,
    paragraph_hashes: norm.paragraphs,
  })
});
// Tu backend llama a Anchorum API con la API Key — Zero-Knowledge garantizado

certifyFile(file, options)

Certifica un archivo directamente desde el navegador. Requiere apiKey en el constructor (para usar cuando el frontend es propio y no público).

const seal = await anchorum.certifyFile(file, {
  algorithm: 'anchorum-tree-v3',  // Árbol de Párrafos — detecta cambios por cláusula
  batchId:   'CONTRATOS-ABRIL',     // opcional
  metadata:  { cliente: 'Empresa SA' }  // opcional
});

console.log(seal.document_id);    // UUID del documento
console.log(seal.seal_summary);   // "28 párrafos certificados"
console.log(seal.hash_sha256);    // hash físico
console.log(seal.hash_robusto);   // hash semántico (v2/v3)
OpciónTipoDescripción
algorithmstringanchorum-tree-v3 (párrafos, recomendado) · sha256 (solo hash físico) · anchorum-text-v2 (legacy)
batchIdstringAgrupa documentos en un lote para sello Bitcoin conjunto
metadataobjectDatos adicionales que aparecen en el certificado PDF

verifyFile(file, options)

Verifica la integridad de un archivo comparándolo contra el sello registrado. Sin API Key — endpoint público.

const result = await anchorum.verifyFile(file, {
  documentId: '164a0691-5d60-4823-a680-3f8c47b12a91'
});

console.log(result.match);           // true / false
console.log(result.integrity_pct);   // 96.4 (con árbol v3)
console.log(result.algorithm);       // "anchorum-tree-v3"
console.log(result.certified_at);    // "2026-04-19T14:32:00Z"

diffParagraphs(candidateFile, documentId)

Compara un archivo candidato cláusula por cláusula contra un documento previamente certificado con anchorum-tree-v3. Detecta exactamente qué párrafos fueron modificados.

const diff = await anchorum.diffParagraphs(candidateFile, documentId);

console.log(diff.integrity_pct);        // 88.5
console.log(diff.total_paragraphs);      // 28
console.log(diff.modified_paragraphs);   // 3 (índices de párrafos distintos)
console.log(diff.details[0].index);      // 7 — párrafo #8 modificado
console.log(diff.details[0].match);      // false
⚠️
Solo funciona con documentos certificados con anchorum-tree-v3. Para documentos sellados con algoritmos anteriores usa verifyFile().

normalizeFile(file, algo, onProgress)

Extrae y normaliza un archivo retornando los hashes sin certificar. Útil cuando quieres enviar los hashes a tu propio backend antes de llamar a Anchorum.

const norm = await anchorum.normalizeFile(
  file,
  'anchorum-tree-v3',
  pct => console.log(`Procesando: ${pct}%`)
);

// Retorna:
// norm.hash_fisico      — SHA-256 del archivo binario
// norm.hash_robusto     — hash semántico normalizado
// norm.paragraphs       — array de { index, hash } (v3)
// norm.normalize_algo   — "anchorum-tree-v3"
// norm.paragraph_count  — número de cláusulas detectadas
AlgoritmoDescripciónMejor para
anchorum-tree-v3Merkle Tree de párrafos detectados por espaciado vertical. Cada cláusula, un hash.Contratos, actas, documentos con cláusulas bien definidas
sha256SHA-256 byte a byte del archivo original.Archivos binarios, imágenes, código fuente
anchorum-text-v2Legacy. SHA-256 del texto completo, NFD, sin tildes, minúsculas. Soportado para verificar documentos sellados con versiones anteriores.Verificación de sellos históricos

Introducción

Anchorum es un servicio de certificación criptográfica de documentos. Envías el hash SHA-256 de un archivo; Anchorum lo registra con timestamp inmutable en PostgreSQL y lo ancla en la blockchain de Bitcoin via OpenTimestamps, generando un certificado PDF verificable por cualquier tercero sin depender de Anchorum.

Datos de la API

ParámetroValor
Base URLhttps://api.anchorum.co/api/v1
ProtocoloHTTPS — TLS 1.2+
FormatoJSON (request y response), UTF-8
Versiónv1
AutenticaciónHeader X-API-Key: anc_<tu_key>
Zona horariaUTC en todos los timestamps
Latencia<200ms en P99 para operaciones de escritura
💡
Privacidad por diseño: Anchorum nunca recibe ni almacena tus documentos originales. Solo el hash SHA-256 — matemáticamente irreversible — se envía a la API. El archivo permanece siempre en tu infraestructura.

Nota sobre el árbol Merkle: El archivo .ots individual de cada documento contiene los hashes intermedios del árbol OTS (nodos hermanos), no los SHA-256 de los demás PDFs. Esto significa que ningún cliente puede derivar los hashes originales de otros documentos del mismo lote.

¿Cuánto ahorras vs. notaría?

Mueve el slider para ver el coste de Anchorum frente a alternativas tradicionales de certificación.

Anchorum
/ mes
Notaría tradicional
~$25 / doc (precio mínimo Colombia/LATAM)
DocuSign Enterprise / eIDAS
~$4 / doc (plan enterprise + audit trail)
💡
Verificación offline: A diferencia de cualquier proveedor de firma electrónica, los certificados Anchorum son verificables sin conexión a internet y sin depender de nuestra existencia. El archivo .ots contiene la prueba completa en la cadena Bitcoin.

Autenticación

Todos los endpoints protegidos requieren el header X-API-Key. Las claves tienen el prefijo anc_ seguido de un token aleatorio de 40+ bytes en hex.

X-API-Key: anc_TuClaveDe48CaracteresAqui

Obtener tu API Key

  1. Crea una cuenta en anchorum.co/registro.html
  2. La key se muestra una sola vez al crearla — guárdala en un lugar seguro
  3. Si la pierdes, usa el botón 🔄 Rotar Key en api.anchorum.co/app o llama a POST /auth/rotate con tu key actual — recibirás una nueva por email y la anterior quedará invalidada de inmediato
⚠️
Seguridad: Nunca incluyas tu API Key en código del lado cliente (JavaScript del navegador, apps móviles). Usa variables de entorno en tu servidor backend.
🧪
Entorno de pruebas (Sandbox): Al registrarte, tu plan Free incluye 50 sellos gratuitos mensuales. Úsalos para validar tu integración antes de pasar a un plan de pago.

Endpoints públicos (sin API Key)

Estos endpoints son accesibles por auditores externos sin cuenta:

Respuestas de autenticación

401
API Key inválida, malformada o desactivada.
402
Plan expirado o saldo insuficiente (plan Pay-as-you-go sin créditos).

Quickstart API — 5 minutos

Para integración backend (Python, Node, PHP). Si tienes frontend, ve a SDK Quickstart — es más sencillo.

📦
Los pasos siguientes son para servidores sin navegador. El Anchorum SDK hace exactamente esto automáticamente desde el navegador del usuario: extrae el hash localmente y llama a /ingest en tu nombre. Ver SDK →

Tres pasos para certificar tu primer documento y anclarlo en Bitcoin.

Paso 1 — Calcular el SHA-256 localmente

El archivo nunca sale de tu servidor. Solo calculas su huella digital.

import hashlib

with open('contrato.pdf', 'rb') as f:
    sha256 = hashlib.sha256(f.read()).hexdigest()

print(sha256)
# a252a71c9d2e8f5c3b1d4e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b
// Node.js — módulo nativo crypto
const crypto = require('crypto');
const fs     = require('fs');

const sha256 = crypto
  .createHash('sha256')
  .update(fs.readFileSync('contrato.pdf'))
  .digest('hex');
<?php
// hash_file es nativo en PHP 5.1.2+
$sha256 = hash_file('sha256', 'contrato.pdf');
// a252a71c9d2e8f5c3b1d4e6f7a8b9c0d...
# Linux / macOS
sha256sum contrato.pdf

# macOS alternativa
shasum -a 256 contrato.pdf

# Windows PowerShell
Get-FileHash contrato.pdf -Algorithm SHA256 | Select-Object Hash

Paso 2 — Certificar el documento

import requests

r = requests.post(
    'https://api.anchorum.co/api/v1/ingest',
    headers={
        'X-API-Key':    'anc_tu_api_key',
        'Content-Type': 'application/json',
    },
    json={
        'filename':    'contrato_compraventa.pdf',
        'content':    sha256,
        'batch_id':    'CONTRATOS-ABRIL-2026',
        'metadata':    {'firmante': 'Juan García', 'tipo': 'compraventa'},
    }
)
r.raise_for_status()
doc = r.json()
print(doc['document_id'])
# → "164a0691-5d60-4823-a680-3f8c47b12a91"
const res = await fetch('https://api.anchorum.co/api/v1/ingest', {
  method:  'POST',
  headers: {
    'X-API-Key':    'anc_tu_api_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filename:    'contrato_compraventa.pdf',
    content:     sha256,
    batch_id:    'CONTRATOS-ABRIL-2026',
    metadata:    { firmante: 'Juan García' },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const doc = await res.json();
console.log(doc.document_id);
// → "164a0691-5d60-4823-a680-3f8c47b12a91"
<?php
$payload = json_encode([
    'filename'    => 'contrato_compraventa.pdf',
    'content'    => $sha256,
    'batch_id'    => 'CONTRATOS-ABRIL-2026',
    'metadata'    => ['firmante' => 'Juan García'],
]);

$ch = curl_init('https://api.anchorum.co/api/v1/ingest');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: anc_tu_api_key',
        'Content-Type: application/json',
    ],
]);

$res = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 201) {
    throw new RuntimeException("Anchorum error: $status — $res");
}
$doc = json_decode($res, true);
echo $doc['document_id'];
// → "164a0691-5d60-4823-a680-3f8c47b12a91"
curl -s -X POST https://api.anchorum.co/api/v1/ingest \
  -H "X-API-Key: anc_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "filename":   "contrato_compraventa.pdf",
    "content":    "a252a71c9d2e...",
    "batch_id":   "CONTRATOS-ABRIL-2026",
    "metadata":   { "firmante": "Juan García" }
  }' | jq .

Paso 3 — Sellar el lote en Bitcoin

Cuando hayas terminado de certificar todos los documentos del lote, genera el Merkle Tree y ancla en Bitcoin:

r = requests.post(
    'https://api.anchorum.co/api/v1/merkle/generate',
    headers={'X-API-Key': 'anc_tu_api_key'},
    json={'batch_id': 'CONTRATOS-ABRIL-2026'},
)
r.raise_for_status()
result = r.json()
print(result['ots_status'])
# → "pending" (confirmación Bitcoin en ~1-3h)
await fetch('https://api.anchorum.co/api/v1/merkle/generate', {
  method:  'POST',
  headers: {
    'X-API-Key':    'anc_tu_api_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ batch_id: 'CONTRATOS-ABRIL-2026' }),
});
// Recibe confirmación via webhook en ~1-3h
<?php
$ch = curl_init('https://api.anchorum.co/api/v1/merkle/generate');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(['batch_id' => 'CONTRATOS-ABRIL-2026']),
    CURLOPT_HTTPHEADER     => [
        'X-API-Key: anc_tu_api_key',
        'Content-Type: application/json',
    ],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
// $result['ots_status'] === 'pending'
curl -s -X POST https://api.anchorum.co/api/v1/merkle/generate \
  -H "X-API-Key: anc_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"batch_id": "CONTRATOS-ABRIL-2026"}' | jq .
ℹ️
El timestamp Bitcoin tarda 1-3 horas. El certificado PDF ya está disponible inmediatamente después del /ingest. La confirmación Bitcoin añade la prueba .ots irrefutable.

Patrones de integración

Cómo encaja Anchorum en diferentes arquitecturas empresariales.

Patrón A — Certificación en el momento de firma

El más común para sistemas de gestión documental. Cuando el usuario firma o aprueba un documento, tu backend calcula el hash y llama a Anchorum antes de confirmar la operación.

# En tu evento de firma
def on_document_signed(document_id, file_path, signer):
    # 1. Hash local — el archivo no sale de tu servidor
    with open(file_path, 'rb') as f:
        sha256 = hashlib.sha256(f.read()).hexdigest()

    # 2. Certificar en Anchorum
    r = requests.post('https://api.anchorum.co/api/v1/ingest',
        headers={'X-API-Key': API_KEY},
        json={
            'filename':    os.path.basename(file_path),
            'content':    sha256,
            'batch_id':    f'DOCS-{datetime.today().strftime("%Y-%m")}',
            'metadata':    {'signer': signer, 'doc_id': document_id},
        }, timeout=10)
    r.raise_for_status()

    # 3. Guardar el document_id de Anchorum junto con tu registro
    anchorum_id = r.json()['document_id']
    db.execute(
        'UPDATE documents SET anchorum_id = ? WHERE id = ?',
        (anchorum_id, document_id)
    )

Patrón B — Batch de fin de día (lotes masivos)

Para empresas que procesan muchos documentos: acumula durante el día y certifica al cierre. Más eficiente para planes con cuota mensual.

# Tarea cron: cada noche a las 23:00
def nightly_batch_certification():
    today = datetime.today().strftime('%Y-%m-%d')
    batch_id = f'NIGHTLY-{today}'

    # Documentos creados hoy sin certificar
    docs = db.fetch_uncertified_today()

    documents = []
    for doc in docs:
        with open(doc.path, 'rb') as f:
            sha256 = hashlib.sha256(f.read()).hexdigest()
        documents.append({
            'filename':    doc.name,
            'content':    sha256,
            'metadata':    {'internal_id': doc.id},
        })

    # Un solo request certifica hasta 1,000 docs
    r = requests.post('https://api.anchorum.co/api/v1/ingest/batch',
        headers={'X-API-Key': API_KEY},
        json={'batch_id': batch_id, 'documents': documents},
        timeout=30)
    r.raise_for_status()

    # Guardar document_ids y sellar el lote en Bitcoin
    for certified in r.json()['documents']:
        internal_id = certified['metadata']['internal_id']
        db.update_anchorum_id(internal_id, certified['document_id'])

    requests.post('https://api.anchorum.co/api/v1/merkle/generate',
        headers={'X-API-Key': API_KEY},
        json={'batch_id': batch_id})

Patrón C — Verificación en recepción de documentos

Para sistemas donde recibes documentos de terceros y necesitas verificar que no fueron alterados en tránsito.

# Cuando recibes un documento con su document_id de Anchorum
def verify_received_document(file_path, anchorum_document_id):
    with open(file_path, 'rb') as f:
        sha256 = hashlib.sha256(f.read()).hexdigest()

    # Endpoint público — no requiere API Key
    r = requests.get(
        'https://api.anchorum.co/api/v1/verify',
        params={'document_id': anchorum_document_id, 'hash_to_verify': sha256}
    )
    result = r.json()

    if not result['is_valid']:
        raise SecurityError('Documento alterado — rechazando recepción')

    return result['certified_at'], result['ots_verified']

Patrón D — Integración desde el navegador sin SDK (proxy server-side)

Si consumes la API directamente desde JavaScript en el navegador sin usar el SDK, las llamadas serán bloqueadas por CORS — el navegador rechaza peticiones cross-origin con credenciales (X-API-Key) a dominios externos. La solución es un proxy server-side: tu servidor recibe la petición del navegador, añade la API Key y la reenvía a Anchorum. La API Key nunca viaja al cliente.

⚠️ Nunca pongas la API Key en el frontend. Cualquier visitante puede verla en el código fuente. Úsala únicamente en tu servidor o usa el SDK de Anchorum, que gestiona esto automáticamente.
# server.py — proxy mínimo (Python stdlib, sin dependencias)
import http.server, urllib.request, urllib.parse, os

API_KEY  = os.environ['ANCHORUM_API_KEY']   # ← de variable de entorno, nunca hardcoded
API_BASE = 'https://api.anchorum.co/api/v1'

ROUTES = {
    '/proxy/ingest':     '/ingest',
    '/proxy/verify':     '/verify',
    '/proxy/normalize':  '/normalize',
    '/proxy/verify/diff': '/verify/diff',
}

class ProxyHandler(http.server.SimpleHTTPRequestHandler):
    def do_POST(self):
        route = ROUTES.get(urllib.parse.urlparse(self.path).path)
        if not route:
            return self.send_error(404)
        length = int(self.headers.get('Content-Length', 0))
        body   = self.rfile.read(length)
        req    = urllib.request.Request(API_BASE + route, data=body, method='POST')
        req.add_header('Content-Type', self.headers.get('Content-Type', 'application/json'))
        req.add_header('X-API-Key', API_KEY)
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                data, status = r.read(), r.status
        except urllib.error.HTTPError as e:
            data, status = e.read(), e.code
        self.send_response(status)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.end_headers()
        self.wfile.write(data)

# Arrancar: ANCHORUM_API_KEY=anc_... python server.py
import socketserver
with socketserver.TCPServer(('', 8080), ProxyHandler) as s:
    s.serve_forever()

En producción sustituye el proxy por un endpoint en tu propio backend (Node.js, Django, Laravel, etc.) que haga lo mismo. El navegador habla con tu servidor — Anchorum nunca queda expuesto al cliente.

POST /ingest

Certifica un solo documento. Registra el hash SHA-256 con timestamp inmutable y lo incluye en el siguiente ciclo de anclaje Bitcoin.

📦
El SDK llama a este endpoint automáticamente cuando usas certifyFile(). Solo necesitas llamarlo directamente desde backend (Python/Node/PHP). Ver SDK →

Headers

X-API-Keystringrequerido
Tu clave de API. Formato: anc_xxxx
Content-Typestringrequerido
Debe ser application/json

Body

filenamestringrequerido
Nombre descriptivo del archivo. Aparece en el certificado PDF. Ej: "contrato_compraventa.pdf"
contentstringrequerido
Hash SHA-256 del archivo en hexadecimal lowercase (64 chars [0-9a-f]). Calcula el hash localmente — el archivo nunca sale de tu servidor.
batch_idstringrequerido
Identificador del lote/proyecto. Agrupa documentos para el Merkle Tree. Solo caracteres [a-zA-Z0-9_-], máx 100 chars. Ej: "CONTRATOS-2026-04"
content_size_bytesintegeropcional
Tamaño del archivo original en bytes. Se muestra en el certificado PDF.
metadataobjectopcional
Objeto JSON libre. Máximo 2KB. Se almacena y aparece en el certificado. Ej: {"firmante":"García","tipo":"compraventa"}
hash_robustostringopcional
SHA-256 del texto normalizado (Sello Dual). Permite verificación CONTENT_ONLY cuando el archivo cambia de formato pero el texto legal es idéntico.
hash_planostringopcional
SHA-256 del texto con whitespace completamente colapsado. Permite detección cross-formato: sellás un PDF generado desde Word y verificás con el .docx original — el sistema detecta que el contenido es idéntico aunque sean archivos distintos. Obtenido de POST /normalize.
normalize_algostringopcional
Algoritmo de normalización: anchorum-tree-v3 (recomendado, Merkle de párrafos) · anchorum-text-v2 (legacy) · anchorum-text-v1 (legacy).
paragraph_hashesarrayopcional
Hashes individuales por párrafo. Solo con normalize_algo=anchorum-tree-v3. Array de objetos {index, hash, anchor_start, anchor_end, char_count} obtenidos de POST /normalize?algo=anchorum-tree-v3.

Respuesta — 201 Created

{
  "document_id":    "164a0691-5d60-4823-a680-3f8c47b12a91",
  "filename":       "contrato_compraventa.pdf",
  "hash_sha256":    "a252a71c9d2e8f5c3b1d4e6f7a8b9c0d...",
  "hash_robusto":   "0da3ab97aa4b36174b373a1b785e8180...",
  "seal_type":      "dual",
  "certified_at":   "2026-04-09T14:32:11.847Z",
  "batch_id":       "CONTRATOS-2026-04",
  "status":         "certified",
  "certificate_url":"https://api.anchorum.co/api/v1/certificate/164a0691..."
}

Errores

400
INVALID_HASH — content no tiene 64 caracteres hexadecimales.
409
DUPLICATE_DOCUMENT — ese hash_sha256 ya existe en este batch_id.
409
BATCH_SEALED — el lote ya fue sellado con /merkle/generate. Usa un batch_id diferente.
402
QUOTA_EXCEEDED — límite mensual alcanzado o saldo insuficiente.
POST /ingest/batch

Certifica hasta 1,000 documentos en un solo request HTTP. Todos comparten el mismo batch_id, Merkle Tree y prueba Bitcoin.

Body

batch_idstringrequerido
Identificador del lote para todos los documentos.
documentsarrayrequerido
Array de hasta 1,000 objetos. Cada objeto: content (req — SHA-256), filename (req), metadata (opt), hash_robusto (opt — Sello Dual v2), hash_plano (opt — detección cross-formato PDF↔DOCX), normalize_algo (opt — anchorum-text-v1 | anchorum-text-v2 | anchorum-tree-v3), paragraph_hashes (opt — requerido con v3, array de {index, hash, anchor_start}).

Respuesta — 201 Created

{
  "batch_id":  "AUDITORIA-Q1-2026",
  "certified": 2,
  "failed":    0,
  "documents": [
    {
      "document_id": "uuid-1...",
      "filename":    "reporte_financiero.pdf",
      "certified_at":"2026-04-09T14:32:11Z",
      "status":      "certified"
    }
  ]
}
💡
Los items que fallen (hash inválido, duplicado) se incluyen en failed con su motivo. El procesamiento es parcial — los documentos válidos siempre se certifican aunque otros fallen.
GET/documents

Lista los documentos certificados de tu cuenta con paginación y filtros.

Query parameters

batch_idstringopcional
Filtra por lote.
statusstringopcional
certified | pending | confirmed | revoked
limitintegeropcional
1-100. Default: 50.
offsetintegeropcional
Para paginación. Default: 0.

Respuesta — 200 OK

{
  "total":  127,
  "limit":  50,
  "offset": 0,
  "items": [
    {
      "document_id":  "164a0691-...",
      "filename":     "contrato.pdf",
      "hash_sha256":  "a252a71c...",
      "certified_at": "2026-04-09T14:32:11Z",
      "batch_id":     "CONTRATOS-2026-04",
      "status":       "confirmed"
    }
  ]
}
GET/documents/batches

Lista todos tus lotes con estadísticas.

Respuesta — 200 OK

{
  "batches": [
    {
      "batch_id":         "CONTRATOS-2026-04",
      "doc_count":        23,
      "latest_at":        "2026-04-09T14:32:11Z",
      "ots_status":       "confirmed",
      "merkle_root":      "9f8e7d6c..."
    }
  ]
}
POST/merkle/generate

Genera el Merkle Tree de todos los documentos del lote y ancla la raíz en Bitcoin via OpenTimestamps. Llama este endpoint cuando hayas terminado de certificar todos los documentos del lote.

⚠️
Solo puedes generar el Merkle Root una vez por batch_id. El lote queda sellado — no se pueden agregar documentos después.

Respuesta — 200 OK

{
  "batch_id":            "CONTRATOS-2026-04",
  "root_hash":           "9f8e7d6c5b4a3f2e...",
  "document_count":      23,
  "ots_status":          "pending",
  "estimated_confirmation": "1-3 horas"
}

POST /normalize

Extrae texto de un archivo y calcula los hashes para Sello Dual — server-side. El archivo no se almacena. Se destruye de RAM inmediatamente tras el cálculo.

Usado internamente por el SDK cuando llamas a normalizeFile() o certifyFile(file, { algorithm: 'anchorum-tree-v3' }).

Sin API Key para v1/v2 — cualquier cliente puede usar este endpoint. Para anchorum-tree-v3 se recomienda incluir la API Key (mayor límite de rate).

Headers

Content-Typestringrequerido
Debe ser multipart/form-data (envío de archivo).
X-API-Keystringopcional
Recomendado para anchorum-tree-v3. Sin key: límite 30 req/min. Con key: límite del plan.

Query parameters

algostringopcional
Algoritmo: anchorum-tree-v3 (recomendado, default) · anchorum-text-v2 (legacy) · anchorum-text-v1 (legacy).

Body (multipart/form-data)

filefilerequerido
Archivo a procesar. Formatos: PDF, DOCX, TXT, HTML, Markdown. Máximo 20 MB.

Respuesta — 200 OK

{
  "hash_fisico":       "a252a71c9d2e8f5c...",  // SHA-256 del archivo binario
  "hash_robusto":      "0da3ab97aa4b3617...",  // Merkle Root (v3) o SHA-256 normalizado (v2)
  "hash_plano":        "722089ab354404b0...",  // SHA-256 con whitespace colapsado — cross-format
  "normalize_algo":    "anchorum-tree-v3",
  "doc_format":        "pdf",
  "text_length":       18432,
  "normalized_length": 17901,
  "paragraph_count":   28,              // solo v3
  "tree_depth":        5,               // solo v3
  "paragraphs": [                          // solo v3
    {
      "index":        0,
      "hash":         "e3b0c44298fc1c14...",
      "char_count":   412,
      "anchor_start": "CONTRATO DE PRESTACION DE SERVICIOS",
      "anchor_end":   "las partes suscriben el presente",
      "section":      "CLÁUSULA PRIMERA"
    }
  ],
  "warning": null                          // "PDF escaneado" si no hay texto extraíble
}

Ejemplo — curl v3

curl -s -X POST "https://api.anchorum.co/api/v1/normalize?algo=anchorum-tree-v3" \
  -H "X-API-Key: anc_tu_api_key" \
  -F "file=@contrato.pdf" | jq .paragraphs

Flujo v3 completo (backend directo sin SDK)

import hashlib, requests

# 1. Hash físico local
with open("contrato.pdf", "rb") as f:
    file_bytes = f.read()
hash_fisico = hashlib.sha256(file_bytes).hexdigest()

# 2. Extraer árbol de párrafos (server-side)
norm = requests.post(
    "https://api.anchorum.co/api/v1/normalize",
    params={"algo": "anchorum-tree-v3"},
    headers={"X-API-Key": "anc_tu_api_key"},
    files={"file": ("contrato.pdf", file_bytes, "application/pdf")},
).json()

# 3. Certificar con ambos hashes + árbol de párrafos
requests.post(
    "https://api.anchorum.co/api/v1/ingest",
    headers={"X-API-Key": "anc_tu_api_key", "Content-Type": "application/json"},
    json={
        "content":          hash_fisico,
        "filename":         "contrato.pdf",
        "batch_id":         "CONTRATOS-2026-04",
        "hash_robusto":     norm["hash_robusto"],    # Merkle Root
        "normalize_algo":   "anchorum-tree-v3",
        "paragraph_hashes": norm["paragraphs"],     # array de {index, hash, ...}
    },
)

⚡ Sello Dual — Verificación de Contenido

Además del hash SHA-256 físico, Anchorum almacena un hash de contenido normalizado (hash_robusto). Esto permite verificar que el texto legal de un documento no fue modificado, incluso si el archivo fue re-exportado, cambió de formato, se le añadieron espacios o se modificaron mayúsculas.

Detección cross-formato — PDF ↔ DOCX

Anchorum incluye un tercer hash, hash_plano, que colapsa todo el whitespace (saltos de línea, espacios múltiples) a un único espacio antes de calcular el SHA-256. Esto resuelve el caso donde:

⚠️
Para que la detección cross-formato funcione, el PDF y el DOCX deben tener exactamente el mismo texto. La forma más segura es sellar el PDF generado directamente desde el Word, y verificar contra ese mismo .docx. El copy-paste de PDF a Word puede perder encabezados, pies de página o caracteres especiales.

📦 SDK oficial — anchorum-sdk.js

Incluye un solo script y tendrás todo lo necesario para calcular ambos hashes (hashFisico y hashRobusto) directamente en el navegador. No necesitas instalar dependencias ni implementar el algoritmo de normalización.

<!-- 1. Incluir el SDK -->
<script src="https://api.anchorum.co/assets/anchorum-sdk.js"></script>

// 2. Computar hashes (File viene de un <input type="file">)
const { hashFisico, hashRobusto, scanned } = await AnchorumSeal.compute(file);

// 3. Certificar
await fetch('https://api.anchorum.co/api/v1/ingest', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-API-Key': 'TU_API_KEY' },
  body: JSON.stringify({
    filename: file.name,
    content:       hashFisico,
    hash_robusto:   hashRobusto,   // null si PDF escaneado
    normalize_algo: 'anchorum-tree-v3',
  })
});
Retorna:  · hashFisico — SHA-256 de los bytes exactos del archivo  · hashRobusto — SHA-256 del texto normalizado (null si no hay texto extraíble)  · scannedtrue si el PDF no tiene texto extraíble (imagen / escaneado)
Soporta: PDF, DOCX, TXT, CSV, JSON, XML, MD y otros formatos de texto.
Sin bloqueo de UI: usa Web Workers internamente; fallback inline automático.

Dos huellas independientes

hash_sha256Sello Físico
SHA-256 de los bytes exactos del archivo. Cambia si el archivo es re-exportado, re-comprimido o tiene metadatos nuevos.
hash_robustoSello Dual
Para v3: raíz del Merkle Tree de párrafos. Para v2 (legacy): SHA-256 del texto extraído y normalizado. Resiste re-exportaciones, cambios de mayúsculas, tildes y espacios.

Algoritmo anchorum-text-v2 (legacy — soportado para verificación)

Aplicar en este orden exacto. El resultado debe ser idéntico en certificación y verificación — cualquier diferencia produce NOT_FOUND.

Paso 1 — Extracción de texto del PDF

Usar pdf.js ≥ 3.x con getTextContent({ includeMarkedContent: false }). Al recorrer los text items, insertar un espacio entre dos items consecutivos cuando ninguno de los dos termina/comienza con un carácter de espacio:

// Crítico: garantiza separación de palabras cuando el PDF no incluye espacio explícito
if (pageText.length > 0 && item.str.length > 0) {
  const lastChar  = pageText[pageText.length - 1];
  const firstChar = item.str[0];
  if (!/\s/.test(lastChar) && !/\s/.test(firstChar)) pageText += ' ';
}
pageText += item.str;
if (item.hasEOL) pageText += '\n';
⚠️ Error frecuente: Omitir esta lógica de separación produce que palabras de distintos text items se fusionen (ej. "contrato"+"firmado""contratofirmado"), generando un hash diferente al almacenado → NOT_FOUND.

Paso 2 — Pipeline de normalización v2.1 (11 pasos, aplicar sobre el texto extraído)

  1. NFKC — convierte ligaduras tipográficas (fi→fi, ff→ff) y compatibilidad Unicode (²→2, ™→TM)
  2. Eliminar invisibles — zero-width spaces, ZWNJ, ZWJ, soft hyphens, BOM, word-joiner
  3. Eliminar C0 + C1[- --Ÿ] (excepto LF )
  4. Eliminar bidiU+200E, U+200F, U+202A–202E, U+2066–2069
  5. Espacios Unicode → espacio normal — NBSP (U+00A0), em/en/thin space (U+2000–200A), etc.
  6. Lowercase — todo a minúsculas
  7. NFD → eliminar diacríticos → NFCé→e, ñ→n, ü→u, ç→c
  8. Tabs y espacios múltiples → 1 espacio
  9. Eliminar números de página — líneas que contienen solo dígitos (^\s*\d+\s*$)
  10. Trim por línea — elimina espacios al inicio y al final de cada línea
  11. 3 o más saltos de línea → 2 · Trim global — máximo un párrafo vacío; elimina espacios al inicio y fin del documento

El resultado final se codifica en UTF-8 y se le aplica SHA-256 → ese es el hash_robusto.

Implementación de referencia (JavaScript)

// Spec v2.1 — debe ser idéntico al pipeline Python de sello_dual_service.py
function _normalizeBase(text) {
  return text
    .normalize('NFKC')                                              // paso 1: ligaduras + compatibilidad
    .replace(/[​‌‍­⁠�]/g, '') // paso 2: invisibles
    .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')       // paso 3: C0+C1
    .replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '')      // paso 4: bidi
    .replace(/[  -    ]/g, ' ')      // paso 5: espacios Unicode
    .replace(/[ \t]+/g, ' ')                                        // colapsar espacios
    .replace(/^ +| +$/gm, '')                                       // trim por línea
    .replace(/\n{3,}/g, '\n\n')                                     // colapsar newlines
    .trim();
}

function normalizeV2(text) {
  let t = _normalizeBase(text);
  t = t.toLowerCase();                                              // paso 6: lowercase
  t = t.normalize('NFD')
       .replace(/[\u0300-\u036f]/g, '')                            // paso 7: diacríticos
       .normalize('NFC');
  t = t.replace(/^\s*\d+\s*$/gm, '');                             // paso 9: números de página
  return t.trim();
}

async function computeHashRobusto(text) {
  const encoded = new TextEncoder().encode(normalizeV2(text));
  const hashBuf = await crypto.subtle.digest('SHA-256', encoded);
  return Array.from(new Uint8Array(hashBuf))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}

La implementación de referencia completa (incluyendo extracción de PDF, DOCX y texto plano) está disponible en el SDK oficial: api.anchorum.co/assets/anchorum-sdk.js. Úsalo directamente en tu integración para garantizar hashes idénticos.

Resultados posibles en verificación

EXACTVálido
Hash físico coincide — archivo 100% original, sin ningún cambio.
CONTENT_ONLYVálido
Hash físico difiere pero hash_robusto coincide — formato modificado, texto legal idéntico.
NOT_FOUNDInválido
Ningún hash coincide — no certificado, alterado, o hash_robusto calculado con algoritmo diferente.
💡
✅ PDF certificado → mismo PDF re-guardado con nuevos metadatos → CONTENT_ONLY
✅ PDF con espacio añadido al final de párrafo → CONTENT_ONLY
✅ Texto con mayúsculas cambiadas ("Contrato""CONTRATO") → CONTENT_ONLY
✅ TXT certificado → mismo TXT con CRLF→LF o espacios diferentes → CONTENT_ONLY
❌ PDF → texto copiado y pegado manualmente en TXT → no compatible (extractores distintos)
hash_robusto calculado sin el paso de separación de text itemsNOT_FOUND
⚠️ Regla crítica de integración: El hash_robusto que envías en POST /ingest y el que envías en POST /verify deben ser calculados con exactamente el mismo pipeline. Cualquier diferencia — incluso omitir un solo paso — produce un hash distinto y el resultado siempre será NOT_FOUND. Usa anchorum-sdk.js en ambos lados para garantizar consistencia.
POST/verifySIN API KEY

Verifica la integridad criptográfica de un documento. Siempre devuelve HTTP 200 — revisar match_type para el resultado.

Body (JSON)

hash_to_verifystringrequerido
SHA-256 del archivo a verificar. 64 caracteres hexadecimales.
document_idUUIDopcional
UUID del documento. Si se omite, búsqueda global en toda la base de datos.
hash_robustostringopcional
Hash de contenido normalizado. Si el hash físico no coincide, Anchorum intenta coincidir por contenido → CONTENT_ONLY.
hash_planostringopcional
SHA-256 con whitespace colapsado. Paso 3 de verificación — detecta contenido idéntico entre formatos distintos (PDF ↔ DOCX). Obtenido de POST /normalize.

Respuesta — 200 OK

{
  "is_valid":       true,
  "match_type":     "CONTENT_ONLY",
  "document_id":    "164a0691-5d60-4823-a680-3f8c47b12a91",
  "filename":       "contrato_compraventa.pdf",
  "certified_at":   "2026-04-09T14:32:11Z",
  "batch_id":       "CONTRATOS-2026-04",
  "status":         "merkle_anchored",
  "hash_provided":  "e865e288...",
  "hash_stored":    "a252a71c...",
  "verified_at":    "2026-04-15T05:32:04Z",
  "failure_reason": null
}
ℹ️
match_type: EXACT · CONTENT_ONLY · NOT_FOUND. HTTP siempre 200 — solo 4xx si el payload es inválido.

POST /verify/diff

Analiza párrafo a párrafo un documento candidato contra uno certificado con anchorum-tree-v3. Detecta exactamente qué cláusulas fueron modificadas, añadidas o eliminadas.

Solo funciona con documentos sellados con anchorum-tree-v3. Para documentos v1/v2 usar POST /verify.

Body (JSON)

document_idUUIDrequerido
UUID del documento original certificado con anchorum-tree-v3.
candidate_paragraphsarrayrequerido
Array de párrafos del documento candidato. Obtenerlos via POST /normalize?algo=anchorum-tree-v3. Cada objeto: {index, hash, anchor_start, char_count}.

Respuesta — 200 OK

{
  "document_id":          "164a0691-5d60-4823-...",
  "is_valid":             false,
  "integrity_pct":        92.8,
  "total_paragraphs":     28,
  "intact_paragraphs":    26,
  "modified_paragraphs":  2,
  "details": [
    {
      "index":        3,
      "status":       "modified",    // "intact" | "modified" | "added" | "removed"
      "anchor_start": "CLÁUSULA CUARTA"
    }
  ],
  "summary": "⚠️ Integridad 92.8% — 26/28 párrafos intactos. Cambios en párrafos #4, #17."
}

Ejemplo — curl

# 1. Obtener párrafos del candidato
NORM=$(curl -s -X POST "https://api.anchorum.co/api/v1/normalize?algo=anchorum-tree-v3" \
  -F "file=@contrato_v2.pdf" | jq .paragraphs)

# 2. Comparar contra el documento certificado
curl -s -X POST https://api.anchorum.co/api/v1/verify/diff \
  -H "Content-Type: application/json" \
  -d "{
    "document_id": "164a0691-5d60-4823-a680-3f8c47b12a91",
    "candidate_paragraphs": $NORM
  }" | jq .summary
POST /compare SIN API KEY (demo) · CON API KEY (producción)

Compara dos documentos párrafo a párrafo y devuelve exactamente qué cambió. Detecta modificaciones de palabras, párrafos eliminados, insertados y movidos. Genera un comprobante forense descargable cuando el original está autenticado en Bitcoin.

⚠️ Modo API vs Demo pública

Sin X-API-Key: el campo seal_hash es opcional — modo demo público sin comprobante.
Con X-API-Key válida: el campo seal_hash es obligatorio. Solo documentos previamente sellados en Anchorum pueden ser comparados. Sin hash → 400 SEAL_HASH_REQUIRED.

Request — multipart/form-data

CampoTipoRequeridoDescripción
originalfileDocumento original (PDF, DOCX, TXT, HTML, Markdown). Máx. 20 MB.
candidatofileDocumento con posibles cambios. Mismo límite de tamaño.
seal_hashstringObligatorio vía APISHA-256 del sello Anchorum del documento original. Autentica el original contra Bitcoin antes de comparar.

Respuesta

CampoTipoDescripción
integrity_pctfloatPorcentaje de integridad del documento (0–100).
intactintPárrafos sin cambios.
totalintTotal de párrafos comparados.
changes_countintNúmero de cambios detectados.
original_authenticatedbool | nulltrue si el hash coincide con un sello en Bitcoin. false si no coincide. null si no se envió hash.
changesarrayLista de cambios. Ver estructura abajo.
doc_format_originalstringFormato detectado del documento original (pdf, docx, txt…).
doc_format_candidatostringFormato detectado del documento candidato.

Estructura de cada cambio en changes[]

CampoValores posiblesDescripción
statusmodified added removed moved moved_modifiedTipo de cambio detectado.
seccionstringNombre de la cláusula o sección donde ocurrió el cambio.
antesstring | nullTexto original del párrafo.
despuesstring | nullTexto modificado del párrafo.

Ejemplo — con API Key (producción)

# Con X-API-Key — seal_hash obligatorio
curl -s -X POST https://api.anchorum.co/api/v1/compare \
  -H "X-API-Key: tu_api_key_aqui" \
  -F "original=@contrato_original.pdf" \
  -F "candidato=@contrato_sospechoso.pdf" \
  -F "seal_hash=b81550cd36cf5b851236a0844d1ecba4179deb7a2559c8dff8e91371ebdc052a" \
  | jq '{integridad: .integrity_pct, cambios: .changes_count, autenticado: .original_authenticated}'

Ejemplo — sin API Key (demo pública)

# Sin X-API-Key — seal_hash opcional, sin comprobante forense
curl -s -X POST https://api.anchorum.co/api/v1/compare \
  -F "original=@contrato_v1.pdf" \
  -F "candidato=@contrato_v2.pdf" \
  | jq '{integridad: .integrity_pct, cambios: .changes_count}'

Errores específicos

CódigoErrorCausa
400SEAL_HASH_REQUIREDSe envió API Key pero no se incluyó seal_hash.
400FILE_TOO_LARGEAlguno de los archivos supera 20 MB.
400EXTRACTION_FAILEDNo se pudo extraer texto del archivo (formato no soportado o corrupto).
400EMPTY_DOCUMENTEl archivo no contiene texto extraíble.
429RATE_LIMITLímite de 10 comparaciones por minuto excedido.
Minimal-Knowledge — Los archivos se procesan en RAM y se descartan al terminar la petición. Ningún documento se almacena en disco ni en base de datos.
GET/certificate/{document_id}SIN API KEY

Descarga el certificado PDF. Devuelve application/pdf. No requiere autenticación — comparte la URL con auditores, tribunales o reguladores.

El certificado PDF incluye

GET/ots/{batch_id}/statusSIN API KEY

Estado del timestamp Bitcoin del lote.

Respuesta — 200 OK

{
  "batch_id":          "CONTRATOS-2026-04",
  "ots_status":        "confirmed",
  "root_hash":         "9f8e7d6c...",
  "bitcoin_block":     890123,
  "bitcoin_block_time":"2026-04-09T15:00:00Z",
  "document_count":    23
}
EstadoSignificado
not_sealedEl lote tiene documentos pero aún no se llamó a /merkle/generate
pendingMerkle Root enviada a OTS, esperando confirmación de bloque Bitcoin
confirmedBloque Bitcoin confirmado. Prueba .ots disponible y verificable offline
GET/documents/{document_id}/proof

Descarga el archivo .ots individual para un documento específico. El archivo vincula directamente el SHA-256 del documento con el bloque Bitcoin — verificable offline sin ninguna dependencia de Anchorum.

💡
¿Cómo funciona? Anchorum construye un árbol Merkle OTS a nivel de bytes al sellar el lote. Para cada documento almacena las operaciones binarias (Prepend/Append + SHA256) que van desde su hash hasta la raíz del árbol. Al descargar el proof individual, el servidor ensambla al vuelo esas ops + la prueba de la raíz a Bitcoin, generando un .ots estándar.

El archivo es verificable directamente en opentimestamps.org subiendo el .ots junto al PDF original — sin ninguna dependencia de Anchorum.

Parámetros de ruta

document_idUUIDrequerido
UUID del documento devuelto por /ingest o /ingest/batch. Lo guardas en tu base de datos al momento de certificar.

Respuesta exitosa — 200 OK

Content-Type: application/octet-stream · Nombre del archivo: {filename}.ots

# Descargar el .ots individual del documento
curl -OJ "https://api.anchorum.co/api/v1/documents/164a0691-5d60-4823-a680-3f8c47b12a91/proof"
# Descarga: contrato_compraventa.ots

# Verificar contra Bitcoin (opentimestamps-client)
ots verify contrato_compraventa.ots contrato_compraventa.pdf
# Success! Bitcoin block 890123 attests data existed as of 2026-04-09T15:00:00Z

Errores posibles

404
Documento no encontrado o no pertenece a tu cuenta.
425
OTS_NOT_READY — El lote aún no ha sido sellado con /merkle/generate, o la confirmación Bitcoin está pendiente.

Flujo completo: desde certificación hasta proof individual

import hashlib, requests, time

API_KEY = "anc_tu_api_key"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# 1. Calcular hash localmente
with open("contrato.pdf", "rb") as f:
    sha256 = hashlib.sha256(f.read()).hexdigest()

# 2. Certificar
r = requests.post("https://api.anchorum.co/api/v1/ingest", headers=HEADERS, json={
    "filename": "contrato.pdf", "content": sha256, "batch_id": "Q2-2026"
})
doc_id = r.json()["document_id"]  # guardar en tu BD

# 3. Sellar el lote en Bitcoin
requests.post("https://api.anchorum.co/api/v1/merkle/generate", headers=HEADERS,
             json={"batch_id": "Q2-2026"})

# 4. Esperar confirmación (webhook recomendado, o polling)
# En producción usa webhooks. Este es solo un ejemplo de polling:
for _ in range(20):
    status = requests.get(
        f"https://api.anchorum.co/api/v1/ots/Q2-2026/status", headers=HEADERS
    ).json()["ots_status"]
    if status == "confirmed": break
    time.sleep(300)  # esperar 5 minutos entre intentos

# Nota: Si llamas a este endpoint antes de que Bitcoin confirme el lote,
# recibirás HTTP 425 OTS_NOT_READY. Por eso usamos el webhook o polling antes.
# 5. Descargar .ots individual del documento
proof = requests.get(
    f"https://api.anchorum.co/api/v1/documents/{doc_id}/proof", headers=HEADERS
)
with open("contrato.ots", "wb") as f:
    f.write(proof.content)

print("✅ contrato.ots listo para verificar en opentimestamps.org")
GET/usage

Consumo mensual y saldo disponible.

Respuesta — 200 OK

{
  "plan":                  "starter",
  "billing_model":         "quota",
  "seals_used":            127,
  "seals_limit":           500,
  "plan_expires_at":       "2026-05-09T23:59:59Z",
  "credits_balance":       null,
  "monthly_history": [
    { "period": "2026-04", "seals": 127 }
  ]
}
POST/billing/subscribe

Inicia o renueva una suscripción. Devuelve URL de pago en criptomonedas (Cryptomus).

Body

planstringrequerido
"starter" ($49/mes · 500 sellos) | "pro" ($149/mes · 5,000 sellos)

Respuesta — 200 OK

{
  "checkout_url": "https://pay.cryptomus.com/pay/uuid...",
  "amount_usd":   20,
  "plan":         "starter"
}
POST/billing/cryptomus-topup

Recarga créditos prepagados para el plan Pay-as-you-go.

Body

amount_usdnumberrequerido
Cantidad a recargar en USD. Mínimo: 10. Los créditos no vencen.
POST/auth/rotate

Genera una nueva API Key e invalida la anterior de inmediato. Útil si sospechas que tu key fue comprometida. La nueva key también se envía por email.

⚠️
Acción irreversible: En cuanto ejecutas esta llamada, la key actual queda inválida. Guarda la nueva key de la respuesta antes de cerrar la sesión. También puedes usar el botón 🔄 Rotar Key en api.anchorum.co/app.

Request

X-API-Keyheaderrequerido
Tu key actual. Body vacío — no se requieren campos.

Respuesta — 200 OK

{
  "new_api_key":  "anc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "message":     "API Key rotada. La anterior ha sido invalidada de inmediato. Guarda esta key — no la volveremos a mostrar. También te la enviamos por email.",
  "client_name": "Tu Empresa"
}

Ejemplo

curl -X POST https://api.anchorum.co/api/v1/auth/rotate \
     -H "X-API-Key: anc_tu_key_actual"

Rate limit

Máximo 3 rotaciones por hora por key.

GET/stats PUB

Estadísticas agregadas de la plataforma. No requiere autenticación. Resultado cacheado 5 minutos para reducir carga en la base de datos.

Respuesta — 200 OK

{
  "total_documents":   4821,
  "confirmed_batches": 138,
  "active_clients":    47
}

Ejemplo

curl https://api.anchorum.co/api/v1/stats

Webhooks

Anchorum envía eventos HTTP POST a tu endpoint cuando Bitcoin confirma un lote. Ideal para notificar a tus usuarios automáticamente.

Registrar un webhook

Envía solo la URL y los eventos que deseas escuchar. El secreto HMAC (signing_secret) es generado automáticamente por el servidor y se retorna una sola vez en la respuesta — guárdalo.

// Request — NO incluyas ningún "secret"
POST /webhooks
{
  "url":    "https://tuservidor.com/webhooks/anchorum",
  "events": ["ots.confirmed"]
}

Respuesta — 201 Created

{
  "id":             "wh_a1b2c3d4...",
  "url":            "https://tuservidor.com/webhooks/anchorum",
  "events":         ["ots.confirmed"],
  "signing_secret": "ancs_xxxxxxxxxxxxxxxx",
  "batch_id":       null,
  "created_at":     "2026-04-15T12:00:00Z"
}
// ↑ Guarda signing_secret — solo se muestra aquí, nunca más.
⚠️
El signing_secret no se almacena en Anchorum. Guárdalo como variable de entorno (WEBHOOK_SECRET) en tu servidor inmediatamente. Si lo pierdes, debes registrar un nuevo webhook.

Evento: ots.confirmed

{
  "event":           "ots.confirmed",
  "batch_id":        "CONTRATOS-2026-04",
  "root_hash":       "9f8e7d6c...",
  "confirmed_at":    "2026-04-09T15:00:00Z",
  "verify_command":  "ots verify -d 9f8e7d6c... anchorum_CONTRATOS-2026-04_merkle.ots"
}

Verificar la firma HMAC

Cada request incluye X-Anchorum-Signature: sha256=<hmac>. Siempre verifica esta firma antes de procesar el evento.

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');
  // timingSafeEqual previene timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(signature)
  );
}

// Express handler (usa express.raw para acceder al body sin parsear)
app.post('/webhooks/anchorum', express.raw({type: '*/*'}), (req, res) => {
  const sig = req.headers['x-anchorum-signature'];
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Firma inválida');
  }
  const event = JSON.parse(req.body);
  // procesar event.batch_id...
  res.sendStatus(200);
});
import hmac, hashlib
from flask import request, abort

def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask handler
@app.route('/webhooks/anchorum', methods=['POST'])
def anchorum_webhook():
    sig = request.headers.get('X-Anchorum-Signature', '')
    if not verify_webhook(request.get_data(), sig, WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # procesar event['batch_id']...
    return '', 200
<?php
function verifyWebhook(string $rawBody, string $sig, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    // hash_equals previene timing attacks
    return hash_equals($expected, $sig);
}

$rawBody = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_ANCHORUM_SIGNATURE'] ?? '';

if (!verifyWebhook($rawBody, $sig, getenv('WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit('Firma inválida');
}

$event = json_decode($rawBody, true);
// procesar $event['batch_id']...
http_response_code(200);
⚠️
Importante: Tu endpoint debe responder con HTTP 200 en menos de 30 segundos. Si falla o tarda más, Anchorum reintentará el webhook hasta 3 veces con backoff exponencial (5min, 30min, 2h).

Rate Limits

Los rate limits se aplican por API Key. Cuando los superas, la API responde con 429 Too Many Requests y el header Retry-After indica cuándo puedes volver a intentar.

Planreq/min (API)req/min (Nginx)Burst
Free206010
Starter606020
Pro30012050
Pay as you go1,000120100
EnterprisePersonalizado

Los endpoints de solo lectura (/verify, /certificate/, /ots/) tienen un rate limit más alto independiente de tu plan.

💡
Nota sobre Lotes Masivos: El endpoint /ingest/batch consume 1 sola petición de tu límite de red por minuto, sin importar si envías 10 o 1,000 documentos en ese único payload.

Códigos de error

Anchorum devuelve errores en formato JSON: {"detail": {"error": "CODIGO", "message": "descripción"}}

4xx — Errores del cliente

400
INVALID_HASH — El hash_sha256 no tiene 64 caracteres hexadecimales válidos.
400
INVALID_BATCH_ID — El batch_id contiene caracteres no permitidos o supera 100 caracteres.
401
INVALID_API_KEY — API Key inválida, malformada o revocada.
402
QUOTA_EXCEEDED — Límite mensual alcanzado. Actualiza tu plan.
402
INSUFFICIENT_CREDITS — Plan Pay-as-you-go sin saldo disponible.
402
PLAN_EXPIRED — Tu suscripción ha vencido. Renueva desde el dashboard.
409
DUPLICATE_DOCUMENT — Ese hash ya existe en el batch_id especificado.
409
BATCH_SEALED — El lote ya fue sellado. Usa un nuevo batch_id para agregar documentos.
422
Validación de esquema JSON fallida. El body tiene campos faltantes o de tipo incorrecto.
429
Rate limit superado. Revisa el header Retry-After.

5xx — Errores del servidor

500
Error interno. Si persiste, contacta a soporte con el header X-Request-ID.
503
Servicio temporalmente no disponible. Reintenta con backoff exponencial.

Retry y resiliencia

Para integraciones en producción, implementa lógica de reintento con backoff exponencial.

import time, requests
from requests.adapters import HTTPAdapter, Retry

def anchorum_session() -> requests.Session:
    """Session con reintentos automáticos para 429, 503."""
    session = requests.Session()
    retry = Retry(
        total=4,
        backoff_factor=1,          # 1s, 2s, 4s, 8s
        status_forcelist=[429, 500, 502, 503, 504],
        respect_retry_after_header=True,  # respeta Retry-After
        allowed_methods=['POST', 'GET'],
    )
    session.mount('https://', HTTPAdapter(max_retries=retry))
    session.headers.update({
        'X-API-Key': API_KEY,
        'Content-Type': 'application/json',
    })
    return session

# Usar:
client = anchorum_session()
r = client.post('https://api.anchorum.co/api/v1/ingest', json={...}, timeout=15)
r.raise_for_status()
async function fetchWithRetry(url, options, retries = 4) {
  for (let i = 0; i < retries; i++) {
    const res = await fetch(url, options);
    if (res.ok) return res;

    const isRetryable = [429, 500, 502, 503, 504].includes(res.status);
    if (!isRetryable || i === retries - 1) throw new Error(`HTTP ${res.status}`);

    // Backoff: 1s, 2s, 4s...
    const retryAfter = res.headers.get('Retry-After');
    const delay = retryAfter ? parseInt(retryAfter) * 1000 : 1000 * 2 ** i;
    await new Promise(r => setTimeout(r, delay));
  }
}

// Usar:
const res = await fetchWithRetry(
  'https://api.anchorum.co/api/v1/ingest',
  { method: 'POST', headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({...}) }
);
💡
Idempotencia: Si certifica el mismo hash con el mismo batch_id dos veces, recibirás 409 DUPLICATE_DOCUMENT. Esto es seguro — el documento ya está certificado. Puedes usar el document_id guardado en tu base de datos.

Checklist de integración

Antes de pasar a producción, verifica que tu integración cumple con estos puntos:

Seguridad

Confiabilidad

Operaciones

Verificación independiente de Bitcoin

La prueba Anchorum es verificable offline sin ningún servidor de Anchorum. Esto garantiza su validez incluso si el servicio deja de existir.

Capas de verificación — qué prueba el .ots y qué requiere Anchorum

Anchorum genera múltiples hashes por documento. No todos son independientemente verificables via OTS: depende de qué información quedó anclada en la cadena de Bitcoin.

Capa Hash / dato Dónde vive Verificable sin Anchorum
1 — Física (OTS) hash_fisico — SHA-256 byte a byte del archivo original Cadena de Bitcoin (dentro del .ots) ✅ Sí — con ots verify o opentimestamps.org
2 — Semántica (Anchorum) hash_robusto — hash del texto limpio (ignorando formato) Base de datos de Anchorum ❌ No — requiere la API /verify de Anchorum
2 — Semántica (Anchorum) hash_plano — hash del texto sin normalización avanzada Base de datos de Anchorum ❌ No — requiere la API /verify de Anchorum
3 — Párrafos (Anchorum) Hash de cada párrafo individual (versión anchorum-tree-v3) Base de datos de Anchorum ❌ No — requiere la API /verify/paragraphs de Anchorum
⚠️
Verificación de cambio de formato (PDF ↔ DOCX)
Detectar que dos archivos de formatos distintos tienen el mismo contenido (ej. contrato.pdf y contrato.docx) es una función exclusiva de Anchorum: el hash_robusto es idéntico para ambos. El archivo .ots solo certifica el hash_fisico, que es diferente entre formatos aunque el texto sea el mismo.
💡
Si Anchorum deja de existir…
La Capa 1 sigue siendo válida indefinidamente: el archivo .ots + el documento original demuestran existencia en esa fecha en Bitcoin, verificable con cualquier cliente OTS. Las capas 2 y 3 requieren acceso a la base de datos de Anchorum y no podrían verificarse si el servicio desaparece.

Opción 1 — Proof individual por documento (recomendado)

Desde v2, cada documento tiene su propia prueba OTS. El auditor solo necesita el PDF original y el archivo .ots — sin depender de ningún servidor.

  1. Descarga el .ots del documento: GET /documents/{document_id}/proof
  2. Ve a opentimestamps.org
  3. Sube el .ots (derecha) y el PDF original (izquierda) → ✓ Check Verde

Opción 2 — CLI de OpenTimestamps (verificación local offline)

# Instalar cliente OTS
pip install opentimestamps-client

# Descargar el .ots individual del documento
curl -OJ "https://api.anchorum.co/api/v1/documents/164a0691-5d60-4823-a680-3f8c47b12a91/proof"
# Descarga: contrato.ots
ots verify contrato.ots contrato.pdf
# Success! Bitcoin block 890123 attests data existed as of 2026-04-09T15:00:00Z

Opción 3 — Endpoint público /verify (sin cuenta)

# Verificar sin API Key ni cuenta
SHA=$(sha256sum mi_contrato.pdf | awk '{print $1}')

curl "https://api.anchorum.co/api/v1/verify?\
document_id=164a0691-5d60-4823-a680-3f8c47b12a91&\
hash_to_verify=$SHA" | jq .

# Respuesta:
# { "is_valid": true, "ots_verified": true, "bitcoin_block": 890123 }