📦 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.
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ón | Tipo | Descripción |
|---|---|---|
algorithm | string | anchorum-tree-v3 (párrafos, recomendado) · sha256 (solo hash físico) · anchorum-text-v2 (legacy) |
batchId | string | Agrupa documentos en un lote para sello Bitcoin conjunto |
metadata | object | Datos 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
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
| Algoritmo | Descripción | Mejor para |
|---|---|---|
anchorum-tree-v3 | Merkle Tree de párrafos detectados por espaciado vertical. Cada cláusula, un hash. | Contratos, actas, documentos con cláusulas bien definidas |
sha256 | SHA-256 byte a byte del archivo original. | Archivos binarios, imágenes, código fuente |
anchorum-text-v2 | Legacy. 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ámetro | Valor |
|---|---|
| Base URL | https://api.anchorum.co/api/v1 |
| Protocolo | HTTPS — TLS 1.2+ |
| Formato | JSON (request y response), UTF-8 |
| Versión | v1 |
| Autenticación | Header X-API-Key: anc_<tu_key> |
| Zona horaria | UTC en todos los timestamps |
| Latencia | <200ms en P99 para operaciones de escritura |
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.
.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
- Crea una cuenta en anchorum.co/registro.html
- La key se muestra una sola vez al crearla — guárdala en un lugar seguro
- Si la pierdes, usa el botón 🔄 Rotar Key en api.anchorum.co/app o llama a
POST /auth/rotatecon tu key actual — recibirás una nueva por email y la anterior quedará invalidada de inmediato
Endpoints públicos (sin API Key)
Estos endpoints son accesibles por auditores externos sin cuenta:
GET /verify— verificar integridad de un documentoGET /certificate/{document_id}— descargar certificado PDFGET /ots/{batch_id}/status— estado del timestamp Bitcoin
Respuestas de autenticación
Quickstart API — 5 minutos
Para integración backend (Python, Node, PHP). Si tienes frontend, ve a SDK Quickstart — es más sencillo.
/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 .
/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.
# 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.
Certifica un solo documento. Registra el hash SHA-256 con timestamp inmutable y lo incluye en el siguiente ciclo de anclaje Bitcoin.
certifyFile(). Solo necesitas llamarlo directamente desde backend (Python/Node/PHP). Ver SDK →Headers
anc_xxxxapplication/jsonBody
"contrato_compraventa.pdf"[a-zA-Z0-9_-], máx 100 chars. Ej: "CONTRATOS-2026-04"{"firmante":"García","tipo":"compraventa"}CONTENT_ONLY cuando el archivo cambia de formato pero el texto legal es idéntico..docx original — el sistema detecta que el contenido es idéntico aunque sean archivos distintos. Obtenido de POST /normalize.anchorum-tree-v3 (recomendado, Merkle de párrafos) · anchorum-text-v2 (legacy) · anchorum-text-v1 (legacy).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
INVALID_HASH — content no tiene 64 caracteres hexadecimales.DUPLICATE_DOCUMENT — ese hash_sha256 ya existe en este batch_id.BATCH_SEALED — el lote ya fue sellado con /merkle/generate. Usa un batch_id diferente.QUOTA_EXCEEDED — límite mensual alcanzado o saldo insuficiente.Certifica hasta 1,000 documentos en un solo request HTTP. Todos comparten el mismo batch_id, Merkle Tree y prueba Bitcoin.
Body
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"
}
]
}
failed con su motivo. El procesamiento es parcial — los documentos válidos siempre se certifican aunque otros fallen.Lista los documentos certificados de tu cuenta con paginación y filtros.
Query parameters
certified | pending | confirmed | revokedRespuesta — 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"
}
]
}
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..."
}
]
}
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.
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' }).
anchorum-tree-v3 se recomienda incluir la API Key (mayor límite de rate).
Headers
multipart/form-data (envío de archivo).anchorum-tree-v3. Sin key: límite 30 req/min. Con key: límite del plan.Query parameters
anchorum-tree-v3 (recomendado, default) · anchorum-text-v2 (legacy) · anchorum-text-v1 (legacy).Body (multipart/form-data)
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:
- Generás un PDF desde Word (Guardar como PDF) y lo sellás
- Más tarde verificás con el
.docxoriginal - El extractor de PDF usa
\n\nentre párrafos; el extractor de DOCX usa\n— loshash_robustodifieren aunque el texto sea idéntico hash_planoiguala ambos → resultadoCONTENT_ONLY ✅
.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', }) });
hashFisico — SHA-256 de los bytes exactos del archivo
· hashRobusto — SHA-256 del texto normalizado (null si no hay texto extraíble)
· scanned — true 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
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';"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)
- NFKC — convierte ligaduras tipográficas (
fi→fi,ff→ff) y compatibilidad Unicode (²→2,™→TM) - Eliminar invisibles — zero-width spaces, ZWNJ, ZWJ, soft hyphens, BOM, word-joiner
- Eliminar C0 + C1 —
[ ---](excepto LF) - Eliminar bidi —
U+200E,U+200F,U+202A–202E,U+2066–2069 - Espacios Unicode → espacio normal — NBSP (
U+00A0), em/en/thin space (U+2000–200A), etc. - Lowercase — todo a minúsculas
- NFD → eliminar diacríticos → NFC —
é→e,ñ→n,ü→u,ç→c - Tabs y espacios múltiples → 1 espacio
- Eliminar números de página — líneas que contienen solo dígitos (
^\s*\d+\s*$) - Trim por línea — elimina espacios al inicio y al final de cada línea
- 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
hash_robusto coincide — formato modificado, texto legal idéntico.hash_robusto calculado con algoritmo diferente.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 items → NOT_FOUND
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.
Verifica la integridad criptográfica de un documento. Siempre devuelve HTTP 200 — revisar match_type para el resultado.
Body (JSON)
CONTENT_ONLY.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)
anchorum-tree-v3.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
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.
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
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
original | file | ✓ | Documento original (PDF, DOCX, TXT, HTML, Markdown). Máx. 20 MB. |
candidato | file | ✓ | Documento con posibles cambios. Mismo límite de tamaño. |
seal_hash | string | Obligatorio vía API | SHA-256 del sello Anchorum del documento original. Autentica el original contra Bitcoin antes de comparar. |
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
integrity_pct | float | Porcentaje de integridad del documento (0–100). |
intact | int | Párrafos sin cambios. |
total | int | Total de párrafos comparados. |
changes_count | int | Número de cambios detectados. |
original_authenticated | bool | null | true si el hash coincide con un sello en Bitcoin. false si no coincide. null si no se envió hash. |
changes | array | Lista de cambios. Ver estructura abajo. |
doc_format_original | string | Formato detectado del documento original (pdf, docx, txt…). |
doc_format_candidato | string | Formato detectado del documento candidato. |
Estructura de cada cambio en changes[]
| Campo | Valores posibles | Descripción |
|---|---|---|
status | modified added removed moved moved_modified | Tipo de cambio detectado. |
seccion | string | Nombre de la cláusula o sección donde ocurrió el cambio. |
antes | string | null | Texto original del párrafo. |
despues | string | null | Texto 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ódigo | Error | Causa |
|---|---|---|
400 | SEAL_HASH_REQUIRED | Se envió API Key pero no se incluyó seal_hash. |
400 | FILE_TOO_LARGE | Alguno de los archivos supera 20 MB. |
400 | EXTRACTION_FAILED | No se pudo extraer texto del archivo (formato no soportado o corrupto). |
400 | EMPTY_DOCUMENT | El archivo no contiene texto extraíble. |
429 | RATE_LIMIT | Límite de 10 comparaciones por minuto excedido. |
Descarga el certificado PDF. Devuelve application/pdf. No requiere autenticación — comparte la URL con auditores, tribunales o reguladores.
El certificado PDF incluye
- Nombre del archivo y hash SHA-256 completo
- Fecha y hora UTC de certificación
- Batch ID y posición en el Merkle Tree
- Merkle Root y estado del timestamp Bitcoin
- QR code con enlace de verificación pública
- Metadatos adicionales (si fueron incluidos en el ingest)
- Sello Dual — hash de contenido normalizado (
anchorum-text-v2), cuando el documento fue certificado conhash_robusto
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
}
| Estado | Significado |
|---|---|
not_sealed | El lote tiene documentos pero aún no se llamó a /merkle/generate |
pending | Merkle Root enviada a OTS, esperando confirmación de bloque Bitcoin |
confirmed | Bloque Bitcoin confirmado. Prueba .ots disponible y verificable offline |
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.
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
/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
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")
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 }
]
}
Inicia o renueva una suscripción. Devuelve URL de pago en criptomonedas (Cryptomus).
Body
"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"
}
Recarga créditos prepagados para el plan Pay-as-you-go.
Body
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.
Request
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.
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.
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);
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.
| Plan | req/min (API) | req/min (Nginx) | Burst |
|---|---|---|---|
| Free | 20 | 60 | 10 |
| Starter | 60 | 60 | 20 |
| Pro | 300 | 120 | 50 |
| Pay as you go | 1,000 | 120 | 100 |
| Enterprise | Personalizado | — | — |
Los endpoints de solo lectura (/verify, /certificate/, /ots/) tienen un rate limit más alto independiente de tu plan.
/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
INVALID_HASH — El hash_sha256 no tiene 64 caracteres hexadecimales válidos.INVALID_BATCH_ID — El batch_id contiene caracteres no permitidos o supera 100 caracteres.INVALID_API_KEY — API Key inválida, malformada o revocada.QUOTA_EXCEEDED — Límite mensual alcanzado. Actualiza tu plan.INSUFFICIENT_CREDITS — Plan Pay-as-you-go sin saldo disponible.PLAN_EXPIRED — Tu suscripción ha vencido. Renueva desde el dashboard.DUPLICATE_DOCUMENT — Ese hash ya existe en el batch_id especificado.BATCH_SEALED — El lote ya fue sellado. Usa un nuevo batch_id para agregar documentos.Retry-After.5xx — Errores del servidor
X-Request-ID.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({...}) } );
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
- API Key en variables de entorno — nunca hardcodeada en el código fuente ni en el repositorio.
- API Key solo en backend — nunca expuesta en JavaScript del navegador, apps móviles o logs.
- Webhook HMAC verificado — rechazar eventos con firma inválida (HTTP 401).
- HTTPS siempre — todas las llamadas a api.anchorum.co usan TLS 1.2+.
Confiabilidad
- Timeout configurado — máximo 15s para operaciones de escritura, 30s para merkle/generate.
- Retry con backoff — maneja 429 y 5xx con reintentos y backoff exponencial.
- document_id guardado — persiste el document_id de Anchorum junto con tu registro en base de datos.
- batch_id predecible — usa un esquema consistente (ej:
ENTIDAD-YYYY-MM) para facilitar auditorías. - merkle/generate llamado — siempre sella el lote después de certificar los documentos.
Operaciones
- Monitorear cuota — llama a
GET /usageperiódicamente y alerta antes de agotar el límite. - Webhook endpoint activo — verifica que tu endpoint responde antes de sellar el lote.
- Certificado PDF accesible — guarda la
certificate_urly ofrécela a tus usuarios. - Prueba de verificación — verifica al menos un documento desde el endpoint público antes de ir a producción.
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 |
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.
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.
- Descarga el
.otsdel documento:GET /documents/{document_id}/proof - Ve a opentimestamps.org
- 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 }