Si tu despacho ya trabaja con un gestor, no hace falta cambiar de sitio: estos seis pasos bastan para sellar un documento y mandarlo como enlace verificable desde donde ya estás.
Está en Ajustes → API. Va en la cabecera X-API-Key de cada llamada. No la pongas en código que viaje al navegador.
curl -X POST https://www.aim-codex.ai/api/v1/documents/write \
-H "X-API-Key: TU_CLAVE" -H "Content-Type: application/json" \
-d '{"title":"Presupuesto reforma","markdown":"Total: 6.000 EUR"}'# /documents/write ya lo deja sellado (status sealed): este paso es solo para plantillas
curl -X POST https://www.aim-codex.ai/api/v1/documents \
-H "X-API-Key: TU_CLAVE" -H "Content-Type: application/json" \
-d '{"template_id":"presupuesto","data":{"emisor":"Reformas Paco","cliente":"Ana Ruiz"}}'
curl -X POST https://www.aim-codex.ai/api/v1/documents/DOC_ID/seal \
-H "X-API-Key: TU_CLAVE" -H "Content-Type: application/json" -d '{}'curl -X POST https://www.aim-codex.ai/api/v1/documents/DOC_ID/share \
-H "X-API-Key: TU_CLAVE" -H "Content-Type: application/json" \
-d '{"label":"Construcciones Lopez","allow_accept":true}'curl https://www.aim-codex.ai/api/v1/documents/DOC_ID/opens -H "X-API-Key: TU_CLAVE" curl https://www.aim-codex.ai/api/v1/acceptances -H "X-API-Key: TU_CLAVE"
curl https://www.aim-codex.ai/api/v1/documents/DOC_ID/dossier \ -H "X-API-Key: TU_CLAVE" -o expediente.pdf
curl https://www.aim-codex.ai/api/v1/verify/HUELLA_SHA256
Eso es todo. El enlace que devuelve url se abre sin cuenta, en el idioma del navegador de tu cliente, y se puede comprobar contra Bitcoin sin pasar por nosotros.
Si una llamada se corta y no sabes si llegó, repítela con la misma cabecera Idempotency-Key (cualquier texto único tuyo, hasta 200 caracteres): durante 24 horas devolvemos la respuesta de la primera vez y no se envía nada dos veces. La lista de documentos admite ?limit= (hasta 500) y &offset=, del más reciente al más antiguo. Los errores llegan en JSON, con error y code.
En Ajustes → API pones una dirección tuya y CODEX te llama cuando pasa algo. Sin sondear nada: tu gestor se entera en el acto de que el cliente ha firmado.
POST https://tu-gestor.example/codex
{
"event": "document.accepted",
"doc_id": "doc-XXXXXXXX",
"content_hash": "6ab026dc...",
"name": "Antonio Lopez",
"at": "2026-09-04T09:52:00Z"
}Eventos que se avisan:
document.first_opendocument.accepteddocument.signatures_completedocument.rejecteddocument.counterofferdocument.revokeddocument.unrevokedcredential.revokedcredential.unrevokeddocument.anchoreddocument.receivedCada llamada va firmada con tu secreto, para que sepas que viene de nosotros. Y como es una petición HTTP normal, sirve igual para Zapier, Make o cualquier cosa que sepa recibir un webhook.
Cada llamada lleva X-Codex-Event, X-Codex-Event-Id (el mismo en los reintentos: úsalo para no procesar dos veces), X-Codex-Timestamp y la firma X-Codex-Signature-256: t=…,v1=…: HMAC-SHA256 con tu secreto sobre t + "." + cuerpo. Rechaza las que tengan más de cinco minutos. (X-Codex-Signature firma solo el cuerpo y se mantiene por compatibilidad.) Si tu servidor responde 5xx o no responde, se reintenta dos veces más; las redirecciones no se siguen. El secreto está en Ajustes → API, y ahí mismo se puede cambiar.
import hmac, hashlib, time
def es_de_codex(cuerpo: bytes, cabecera: str, secreto: str) -> bool:
partes = dict(p.split('=', 1) for p in cabecera.split(','))
if abs(time.time() - int(partes['t'])) > 300:
return False
esperada = hmac.new(secreto.encode(), partes['t'].encode() + b'.' + cuerpo,
hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes['v1'])Si superas un límite recibes 429 con Retry-After: espera esos segundos y repite. La verificación pública admite 120 consultas por minuto y por IP; el servidor MCP, otras 120. Las respuestas de error llevan error (texto) y code (estable, para tu programa: mira la tabla de errores); con X-Lang: en el texto llega en inglés.
Todo error 4xx llega en JSON con error (texto para una persona) y code (estable, para tu programa). Compara siempre code, nunca el texto. Antes de crear o sellar se comprueban tipos y longitudes: lo sellado no se puede corregir, así que un dato mal formado se rechaza con 400 y no se sella nada.
| 400 | bad_json | el cuerpo no es JSON válido (cortado o mal formado), o no es un objeto |
| 400 | bad_types | un campo con el tipo equivocado (una lista donde va un texto); viene con field |
| 400 | too_long | un campo pasa de su máximo; viene con field y max_length (title: 200 caracteres) |
| 400 | no_title · no_content · no_template | falta el título, el contenido o la plantilla |
| 400 | invalid_data · no_signers · no_recipients | datos de plantilla no válidos, sin firmantes, sin destinatarios |
| 400 | bad_limit · bad_offset · webhook_not_public | paginación fuera de rango; webhook a una dirección interna |
| 401 | unauthorized | falta la clave o no es válida |
| 402 | plan | la función es de un plan superior (min_plan dice cuál) |
| 403 | forbidden · needs_login | tu rol no lo permite; o hace falta una persona (contraseña del propietario) |
| 404 | not_found | no existe, o no es de tu cuenta |
| 409 | conflict | el estado no lo permite (p. ej. pedir acuse de un borrador) |
| 413 | too_large | la petición pasa del tamaño máximo |
| 429 | rate | demasiadas peticiones: espera lo que diga Retry-After |
GET /api/v1/verify/HUELLA devuelve tres cosas distintas. authentic: los bytes son los que se sellaron (sigue siendo true en un documento revocado o caducado). state: valid, revoked, expired, superseded, hash_only, received, flagged o unknown. valid: true solo si state es valid. Si tu programa mira un solo campo, que sea valid. La verificación por fichero (/verify-file/SHA256_DEL_FICHERO) da también state: conflict cuando cuentas distintas sellaron el mismo fichero; ahí valid es false. El servidor MCP usa el mismo criterio con su campo vigente.
{"authentic": true, "revoked": true, "state": "revoked", "valid": false, ...}POST /api/v1/documents {template_id, data} -> borrador / draft
POST /api/v1/documents/DOC_ID/seal {} -> sellado / sealed (idempotente)
POST /api/v1/seal-hash {hash, label} -> solo la huella / fingerprint only
POST /api/v1/documents/DOC_ID/signature {signers:[{name,email,tipo}]} -> firma / signature flow
tipo: signer | approver | cc (sin tipo: signer / default: signer)
POST /d/SHARE_ID/delegate {email,to_name,to_email,reason[,code]} -> delegar / delegate
POST /api/v1/documents/DOC_ID/bulk-share {recipients:[{name,email}]}
POST /api/v1/acuses {doc_id, recipients:[{name,email}], deadline}
GET /api/v1/documents?limit=1..500&offset=0 -> documents, count, total, limit, offset, next_offsetCada elemento de signers admite tipo (también type o kind): signer (por defecto: firma en su turno), approver (aprueba o rechaza con motivo en su turno, sin firmar; su justificante dice «Aprobado por») o cc (copia de solo lectura: recibe el enlace al crear el flujo y la copia final; no firma ni bloquea). role sigue siendo el papel libre («Arrendador»); si vale approver o cc, se toma como tipo. Un tipo desconocido da 400 bad_role; un flujo solo de copias, 400 no_signers. Un firmante o un aprobador puede pasar su turno desde su enlace (POST /d/SHARE_ID/delegate, con su correo y, si el servidor envía correo, el código que recibe): el delegado tiene su propio enlace y su propio código. GET /api/v1/documents/DOC_ID/signatures devuelve tipo, delegated_to y delegated_from (quién, cuándo y por qué); los estados nuevos son approved y delegated. Webhook: document.signature_delegated. Las copias y quien delegó no cuentan en signed/total.
{"mode": "sequential", "signers": [
{"name": "Gestor", "email": "gestor@empresa.es", "tipo": "cc"},
{"name": "Socia", "email": "socia@empresa.es", "tipo": "approver"},
{"name": "Cliente", "email": "cliente@empresa.es", "role": "Arrendatario"}]}POST /api/v1/tenant/settings con {"webhook_url": "https://…"}. Adónde van los avisos lo decide una persona: con solo la clave, añade "password" (la del propietario) o recibirás 403 needs_login. El secreto se enseña una sola vez, en la respuesta que lo crea o lo rota ("rotate_webhook_secret": true, también con password); después sale como ••••••. Guárdalo entonces. Una dirección interna (localhost, red privada, metadatos de la nube) se rechaza con 400 webhook_not_public.
JSON-RPC 2.0 sobre HTTP (Streamable HTTP) en https://www.aim-codex.ai/mcp. Herramientas: verificar_documento, search y fetch. Un argumento mal formado devuelve un resultado con isError: true; el exceso de peticiones, HTTP 429 con un error JSON-RPC -32000 y Retry-After.
curl -X POST https://www.aim-codex.ai/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"HUELLA_SHA256"}}}'No hace falta que construyamos un conector para cada uno. Pon en el campo de webhook una dirección de Zapier o Make, y desde ahí conecta con lo que uses: Holded, hojas de cálculo, tu CRM o el correo del equipo. Diez minutos y sin programar.
Lo decimos así porque es lo que funciona hoy, no lo que nos gustaría anunciar. Si tu gestor no está en ninguno de los dos, la API de arriba sirve igual.
Si emites diplomas, cada uno se puede leer también como Verifiable Credential (W3C): el formato de datos del estándar. Público y sin clave, como toda nuestra verificación. Todavía no va firmado con la clave del emisor, que es lo que exigen las carteras: su prueba es el anclaje en Bitcoin y su estado se consulta aquí.
Cómo se calcula el sello, paso a paso →
curl https://www.aim-codex.ai/v/HUELLA_SHA256/w3c.json
Conecta https://www.aim-codex.ai/mcp como servidor MCP en Claude o ChatGPT y pídele que compruebe un documento. Sin cuenta y sin coste.