Saltar al contenido
STAGING
Code Contract

API · v1

Documentación de la API

Integra Code Contract en tus sistemas. Autentica con un Bearer token generado en Ajustes → Llaves de API.

Spec OpenAPI disponible en /api/openapi

Autenticación

Casi todas las peticiones requieren un header Authorization: Bearer cc_live_.... Dos excepciones: la verificación pública, que no pide nada, y los endpoints de RGPD, que van por sesión de navegador y rechazan la llave. Cada llamada lo indica en su alcance.

curl https://app.codecontract.io/api/v1/me \
  -H "Authorization: Bearer cc_live_..."

Cada llave lleva los alcances que le marques al crearla: trackline, consigne y smartcheck, cada uno con :read o :write. El :write incluye el :read. Abajo, cada llamada indica el que necesita.

Cuenta y llave

  • GET

    /api/v1/me

    Comprueba que tu llave funciona, sin tocar datos. Es la primera llamada que conviene hacer.

    Alcance: ninguno — sirve para validar la llave

    Response: { ok, org: { id, slug, name, plan, creditsBalance }, scopes: [...], apiKey: { id, prefix, name, lastUsedAt, createdAt, expiresAt }, rateLimit: { remaining, resetAt, limit }, serverTime } — el 200 trae además las cabeceras X-RateLimit-Limit/Remaining/Reset.

Contactos

  • GET

    /api/v1/contacts

    Lista contactos de la organización.

    Alcance: trackline:read

    Query: q, limit

  • POST

    /api/v1/contacts

    Crea un contacto.

    Alcance: trackline:write

    Body: name, email, phone, company, position, preferredChannel, language

Trackline (Expedientes)

  • GET

    /api/v1/processes

    Lista expedientes. Con include=documents cada expediente trae ya sus solicitudes, documentos y datos extraídos: una llamada en vez de N.

    Alcance: trackline:read

    Query: status, templateId, q, names, nameExact, startedAfter, startedBefore, completedAfter, completedBefore, include=documents, limit, cursor

    Response: { data: [{ id, name, status, template, startedAt, completedAt, requestsCount, detailUrl }], count, limitApplied, nextCursor, warnings? }

  • POST

    /api/v1/processes

    Crea un expediente. Nace siempre ACTIVE. Con templateId + contactIds crea las solicitudes en DRAFT (contacto × requisito), que no se envían hasta lanzarlas.

    Alcance: trackline:write

    Body: name, templateId?, contactIds?, assignedToUserId?, teamId?

  • GET

    /api/v1/processes/{id}

    Detalle de un expediente: plantilla y fases, solicitudes con su documento y datos descifrados, y las últimas 50 trazas.

    Alcance: trackline:read

    Response: { id, name, status, template: { phases }, startedAt, completedAt, requests: [...], traces: [...] }

  • POST

    /api/v1/processes/{id}

    Añade una solicitud a un expediente existente. Nace en DRAFT.

    Alcance: trackline:write

    Body: contactId, title, channel?, subject?, body?, dataFields?, dueAt?, scheduledFor?

Trackline (Solicitudes)

  • GET

    /api/v1/requests

    Lista solicitudes. Pagina por cursor: recorre hasta que nextCursor sea null.

    Alcance: trackline:read

    Query: status, createdAfter, createdBefore, limit, cursor

  • GET

    /api/v1/requests/{id}

    Detalle de una solicitud: contacto, documento y datos extraídos.

    Alcance: trackline:read

  • POST

    /api/v1/requests

    Crea solicitud.

    Alcance: trackline:write

    Body: contactId, title, channel, subject, body, dueAt, folderId

Documentos

  • GET

    /api/v1/documents

    Lista documentos.

    Alcance: trackline:read

    Query: verified=true|false, docType, folderId, sha256, q, createdAfter, createdBefore, updatedAfter, updatedBefore, limit, cursor

  • POST

    /api/v1/documents

    Sube un documento. Multipart para ≤4,5 MB; JSON con el key de una presigned URL para archivos grandes.

    Alcance: trackline:write

    Body: Multipart: file, title?, docType?, folderId? · JSON: key, fileName, title?, docType?, folderId?

  • GET

    /api/v1/documents/{id}

    Detalle: metadatos, datos extraídos por IA, versiones y enlaces (descarga, verificación pública).

    Alcance: trackline:read

  • PATCH

    /api/v1/documents/{id}

    Corrige o añade datos extraídos. Cada campo se mergea y queda con confidence = 1. Escritura optimista OPCIONAL: manda el updatedAt que leíste en la cabecera If-Match (o como baseUpdatedAt en el body) y si otra escritura llegó antes responde 409 version_conflict en vez de pisarla; un token ilegible es 400 invalid_if_match. Sin él gana la última escritura, como siempre.

    Alcance: trackline:write

    Body: { fields: { numero_factura: "ABC123", total: 150.50 } }

  • GET

    /api/v1/documents/{id}/download

    Devuelve el BINARIO del documento. Exige la cabecera Authorization (no es una URL firmada). Una solicitud respondida con datos en vez de con un archivo no tiene binario: responde 404 no_file.

    Alcance: trackline:read

Carpetas

  • GET

    /api/v1/folders

    Lista carpetas de la organización.

    Alcance: trackline:read

    Query: module=consigne|smartcheck|trackline|general

  • POST

    /api/v1/folders

    Crea una carpeta.

    Alcance: trackline:write

    Body: name, module=general, parentId?, color?

Subidas de archivos grandes

  • POST

    /api/v1/uploads/presign

    Pide una URL de subida directa a S3. Necesario por encima de 4,5 MB, que es el máximo que admite un multipart. Flujo: presign → PUT del archivo a la url devuelta → finalizar con POST a /documents (campo key) o a /smartcheck (campo fileKeys). Límite propio: 200 presigns por hora y llave; al superarlo responde 429 con Retry-After.

    Alcance: cualquier alcance de escritura

    Body: fileName, mimeType, sizeBytes

    Response: { url, key, headers, expiresInSec, fileName, mimeType, sizeBytes } — manda el PUT con las headers que te devuelve, tal cual, o S3 rechaza la firma. expiresInSec dice cuánto vive la URL.

SmartCheck (Certificación)

  • POST

    /api/v1/smartcheck

    Certificar uno o varios documentos (multipart) o campos/valores (JSON). Soporta batch.

    Alcance: smartcheck:write

    Body: Multipart: projectName, file/files, summary?, folderId? · JSON de archivos ya subidos: projectName, fileKeys: [{key, fileName?}] · JSON de campos: projectName, field+value o fields[], summary?, folderId?

    Response: { id, sha256, verifyUrl, certified } o { count, results[] } si batch

  • GET

    /api/v1/smartcheck

    Listar certificados con filtros.

    Alcance: smartcheck:read

    Query: createdAfter, createdBefore, field, sha256, projectName, download, proof, limit · from/to siguen aceptándose (deprecados, y con el fin INCLUSIVO que siempre tuvieron)

    Response: { data: [...], count }

  • GET

    /api/v1/smartcheck/{id}

    Detalle de un certificado por ID de evidencia o documento.

    Alcance: smartcheck:read

    Query: download=true (descargar archivo), proof=true (obtener JSON de prueba)

    Response: Detalle completo con verifyUrl, downloadUrl, proofUrl

  • GET

    /api/v1/smartcheck/verify

    Verificar si un dato ya fue certificado previamente.

    Alcance: smartcheck:read — verificar es leer, así que el POST también pide :read, no :write

    Query: sha256, field+value, o data (texto plano)

    Response: { certified: true/false, evidence? }

  • POST

    /api/v1/smartcheck/verify

    Verificar si un dato ya fue certificado (body JSON).

    Alcance: smartcheck:read

    Body: sha256?, field+value?, data?

    Response: { certified: true/false, evidence? }

Consigne (Firmas)

  • POST

    /api/v1/signatures

    Crear solicitud de firma con uno o varios firmantes.

    Alcance: consigne:write

    Body: documentId (o documentIds[] para firmar varios), processName?, level (SES/AES), deadline?, folderId?, signers: [{name, email, phone?, channel?}], formFields?, signaturePosition?, signaturePage?

    Response: { id, processName, signers: [{id, name, signingUrl}] } — signingUrl se devuelve SOLO aquí, al crear: es la única vez que el enlace del firmante existe fuera del email. GET /api/v1/signatures/{id} lo devuelve siempre null; para obtener uno nuevo hay que reenviar la invitación (rota el enlace anterior).

  • GET

    /api/v1/signatures

    Listar solicitudes de firma.

    Alcance: consigne:read

    Query: status, processName, createdAfter, createdBefore, limit · from/to siguen aceptándose (deprecados, y con el fin INCLUSIVO que siempre tuvieron)

    Response: { data: [...], count }

  • GET

    /api/v1/signatures/{id}

    Detalle de una solicitud + firmantes + progreso.

    Alcance: consigne:read

    Query: evidence=true (trazabilidad), pdf=true (documento firmado)

  • DELETE

    /api/v1/signatures/{id}

    Cancelar solicitud de firma (no se puede si ya está completada).

    Alcance: consigne:write

Verificación pública

  • GET

    /api/v1/verify/{hash}

    Verifica un hash SHA-256 públicamente. No requiere autenticación.

    Alcance: público

  • GET

    /api/v1/dpp/{id}

    Pasaporte de Producto Digital de un expediente cerrado o público. Devuelve JSON-LD (Schema.org Product + GS1 Digital Link) si mandas Accept: application/ld+json o ?format=jsonld — vale cualquiera de los dos. Sin ninguno, JSON normal.

    Alcance: público

    Query: format=jsonld

  • GET

    /api/v1/seal-batches/{id}/verify

    Datos para comprobar de forma independiente un lote sellado. Devuelve { batch, events[], integrity, verification }: el merkleRoot y los datos del lote, un evento por hoja con su hash, el resultado de recalcular el árbol, y las instrucciones de verificación. No expone los payloads de los eventos, solo sus hashes.

    Alcance: público

RGPD (sesión de navegador, no API key)

  • GET

    /api/v1/export/full

    Dump JSON completo de la organización (portabilidad, RGPD art. 20). Metadatos + sha256, sin binarios. Solo OWNER o ADMIN.

    Alcance: sesión de navegador — con Bearer responde 401

  • POST

    /api/v1/personal-data/delete

    Supresión de datos personales de un titular por email (RGPD art. 17). Solo OWNER o ADMIN.

    Alcance: sesión de navegador — con Bearer responde 401

    Body: email, reason?

  • GET

    /api/v1/personal-data/delete

    Previsualiza qué se borraría para ese email, sin borrar nada. Solo OWNER o ADMIN.

    Alcance: sesión de navegador — con Bearer responde 401

    Query: email (obligatorio; sin él responde 400 email_required)

MCP (Model Context Protocol)

  • POST

    /api/mcp

    Endpoint MCP para Claude Desktop, Cursor, etc. Tools: query_contacts, create_contact, list_requests, create_request, list_documents, list_processes, credits_balance, create_signature_request.

    Alcance: los de tu llave — cada tool exige el suyo, igual que la ruta v1 equivalente

Portal del participante (token en la ruta, no API key)

Los cuatro endpoints que usa el portal donde el destinatario sube su documento. Puedes conducirlos tú, con tres avisos: (1) autorizan con el token del portal EN LA RUTA — el referenceCode de la solicitud, que es una credencial: no lo loguees ni lo reenvíes—, sin Authorization ni alcances; (2) NO son contrato v1 versionado: pueden cambiar de forma sin aviso de versión; (3) tienen cupo propio por token + IP, y la lectura con IA se cobra a tu organización —con tope por documento, y exigiendo saldo aunque tengas descubierto permitido, porque la dispara un tercero—. Dar de baja una solicitud cierra upload y extract con 404; upload-url y confirm todavía no lo comprueban. La secuencia completa, con curl, en docs/api-v1-external.md § Anexo.

  • POST

    /api/external/{token}/upload-url

    Pide la URL firmada para subir el documento. El fichero va del cliente al almacenamiento, no por aquí. Tope 30 MB. Cupo 20/h.

    Alcance: token del portal en la ruta — no acepta API key

    Body: fileName, mimeType, sizeBytes

    Response: { url, key, headers, expiresInSec, fileName, mimeType, sizeBytes }

  • POST

    /api/external/{token}/upload

    Finaliza la subida con la key. Valida el fichero por su firma, lo registra y marca la solicitud RECEIVED. Cobra 1 crédito. Cupo 20/h.

    Alcance: token del portal en la ruta — no acepta API key

    Body: key, fileName, mimeType?, sizeBytes?, source?

    Response: { ok, documentId, sha256, sizeBytes } · 409 already_received · 409 already_fulfilled_by_other

  • POST

    /api/external/{token}/extract

    Lee el documento con IA y devuelve solo los datos que pidió el remitente. 💰 SE COBRA a la organización que envió la solicitud, al precio de la lectura interna; no cobra si no hay datos pedidos, si ya estaba leído, si el formato no lo lee ningún modelo, si es CSV/JSON o si pides un preset, y devuelve lo cobrado por los proveedores que no llegaron a leer. La segunda llamada sobre el mismo documento no vuelve a cobrar. Cupo 10/h.

    Alcance: token del portal en la ruta — no acepta API key

    Body: preset? (por defecto: solo los campos que pidió el remitente; una lectura por preset no se cobra)

    Response: { schema, fields, confidence } · 402 { code: out_of_credits | license_expired | over_credit_cap } si no se puede cobrar, con el mismo message neutro en los tres

  • POST

    /api/external/{token}/confirm

    Guarda los datos y cierra la solicitud. Si ya estaba RECEIVED (el caso normal de una integración) fija los datos solo si el documento no tenía ninguno. Cupo 20/h.

    Alcance: token del portal en la ruta — no acepta API key

    Body: fields, mode (ai | manual), schemaName?

    Response: { ok } · { ok, alreadyReceived, fieldsStored }

Cómo sacarle partido a los listados

Buscar expedientes por nombre. nameExact casa por igualdad (distingue mayúsculas y espacios); names casa por contenido y admite varios a la vez, en CSV o repitiendo el parámetro, hasta 50. Si el nombre lleva una coma, repite el parámetro en vez de usar CSV. Con q buscas un solo texto y se exigen todos sus términos.

Traer los datos en una sola llamada. include=documents anida en cada expediente sus solicitudes con el contacto, el documento y los datos ya descifrados. Al expandir baja el techo: limit por defecto 10 y máximo 20. La respuesta trae limitApplied para que sepas con cuál se te sirvió.

Paginar. Pasa el nextCursor de la página anterior tal cual en cursor y repite hasta que llegue null. Es un token opaco: no lo construyas a mano. Solo paginan por cursor /processes, /requests y /documents. Los demás listados devuelven como mucho limit filas y no traen nextCursor: si esperas más de ese tope, acota con filtros.

Filtrar por fecha. Sufijo ...After inclusivo, ...Before exclusivo. Admiten YYYY-MM-DD o ISO completo, y una fecha ilegible devuelve 400 en vez de ignorarse. Ojo: una fecha sin hora se ancla a medianoche UTC, así que para incluir el día 7 entero pide ...Before=2026-08-08.

Saber qué tipo de documento pide cada solicitud. Usa requirementDocType, no el title: el título es texto libre, lo edita el usuario y se traduce. Y no lo confundas con document.docType, que es lo que se infirió del fichero y puede llegar null.

Documentos sin fichero. Una solicitud respondida con datos en vez de con un archivo llega con kind: "data" y downloadUrl: null. No es un error: no hay binario que descargar.

Quién subió cada documento. Cada documento trae uploadedBy con type (user, participant, api_key o unknown) más name y email. Un type conocido con nombre y correo a null significa «no podemos decirte quién» (se dio de baja, o fue una llave de API, de la que no publicamos nada) y NO es lo mismo que unknown, que es «nunca hubo actor registrado». En GET /api/v1/signatures/{id} el mismo dato se llama documentUploadedBy, porque esa respuesta aplana el documento con prefijo. Campo aditivo y opcional: la clave no aparece si tu organización no lo tiene activado, si no hemos podido leer la identidad de ese actor concreto —preferimos omitirla antes que afirmar algo sin comprobar— o si el documento está en la papelera. Lo que su ausencia nunca significa es «no consta»: para eso viaja unknown explícito.

Recorre extractedData, no lo enumeres. Tu organización puede declarar en qué formato quiere recibir cada campo (Ajustes → API). Se aplica solo a la entrega: lo guardado no cambia. Cuando un formato declarado no se puede aplicar, el valor llega entero y sin formatear —nunca a medias— y el motivo va en formatErrors; si no pudimos leer tu configuración, llega formatUnavailable: true. Los motivos son texto opaco para diagnóstico: se añaden valores nuevos sin previo aviso, así que no los uses como discriminador. Y un campo puede producir más claves de las que tenía: si declaras que es un rango de fechas (29-05-2026 al 06-07-2026) lo recibes además en dos campos nuevos con los nombres que elijas, mientras el original se conserva verbatim y las dos claves nuevas heredan su confidence. O las dos fechas o ninguna: si una mitad no se puede leer —o si el fin resulta anterior al inicio— no se parte nada. Para las fechas de día primero hace falta que tu organización haya declarado el orden de fecha; las que empiezan por el año no lo necesitan. Un año de dos cifras (3/4/26) sólo se lee si la regla de ese campo declara su ventana ("ventanaAnio": 2000 ⇒ de 2000 a 2099): no lo suponemos, porque 26 es 2026 en una vigencia y puede ser 1926 en una fecha de nacimiento.

Valores que ninguna regla sabe leer. Si un campo tiene formato declarado y su valor está escrito de una forma que ninguna regla reconoce (24th of September 2026), se lee con IA y llega en el formato declarado, sin marca. La IA sólo lee: el formato lo componen las mismas reglas, y la unidad no se convierte. Lo ambiguo (08/09/2026 sin orden de fecha declarado, 1.234 sin separador) no se lee, tampoco con IA, y la IA sólo puede devolver lo que está escrito en el valor. Cuesta 1 crédito por valor leído por primera vez; repetir no cuesta y un fallo se devuelve (y no se reintenta hasta 24 h después, 15 min si fue pasajero). Como mucho 20 lecturas nuevas por respuesta: el resto llega como hoy y se lee en las siguientes llamadas. Es estable: mismo documento, clave, valor guardado y declaraciones ⇒ exactamente la misma salida, porque guardamos la primera lectura y la repetimos. La exportación completa no lee con IA.

Lee siempre warnings[]. Es como la API te avisa de que te está devolviendo menos de lo que crees: un parámetro cuyo nombre no reconocemos, un nombre de búsqueda que se descartó, o expedientes aún pendientes de indexar. Una respuesta vacía con warnings no significa «no existe». Lo emiten los listados de expedientes, solicitudes, documentos, firmas y certificados; los de contactos y carpetas todavía no.

Ejemplos rápidos

Buscar un expediente por su nombre, con sus documentos y datos

curl -G https://app.codecontract.io/api/v1/processes \
  --data-urlencode "nameExact=PA33181 1" \
  --data-urlencode "include=documents" \
  -H "Authorization: Bearer cc_live_..."

Subir un archivo grande (más de 4,5 MB)

KEY_API="cc_live_..."
ARCHIVO="informe.pdf"

# 1. Pide la URL de subida y guarda la respuesta
RESP=$(curl -sf -X POST https://app.codecontract.io/api/v1/uploads/presign \
  -H "Authorization: Bearer $KEY_API" \
  -H "Content-Type: application/json" \
  -d "{\"fileName\":\"$ARCHIVO\",\"mimeType\":\"application/pdf\",\"sizeBytes\":$(wc -c < "$ARCHIVO")}")
URL=$(echo "$RESP" | jq -r .url)
KEY=$(echo "$RESP" | jq -r .key)

# 2. Sube el archivo a esa URL con las cabeceras que te devolvió el paso 1.
#    Van firmadas: si cambias o te saltas alguna, S3 responde SignatureDoesNotMatch.
curl -sf -X PUT "$URL" -H "Content-Type: application/pdf" --upload-file "$ARCHIVO"

# 3. Finaliza con el key
curl -sf -X POST https://app.codecontract.io/api/v1/documents \
  -H "Authorization: Bearer $KEY_API" \
  -H "Content-Type: application/json" \
  -d "{\"key\":\"$KEY\",\"fileName\":\"$ARCHIVO\"}"

Descargar el archivo de un documento

curl -f https://app.codecontract.io/api/v1/documents/DOCUMENT_ID/download \
  -H "Authorization: Bearer cc_live_..." -o documento.pdf

# El -f importa: sin él, un 404 se guarda DENTRO de documento.pdf y curl sale con éxito.

Comprobar que tu llave funciona

curl https://app.codecontract.io/api/v1/me \
  -H "Authorization: Bearer cc_live_..."

# Respuesta: { "ok": true, "org": {...}, "scopes": [...], "rateLimit": {...} }

Certificar múltiples archivos

curl -X POST https://app.codecontract.io/api/v1/smartcheck \
  -H "Authorization: Bearer cc_live_..." \
  -F "projectName=Lote 42" \
  -F "files=@factura.pdf" \
  -F "files=@albaran.pdf"

Certificar campos/valores en batch

curl -X POST https://app.codecontract.io/api/v1/smartcheck \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectName": "Lote 42",
    "fields": [
      {"field": "peso_neto", "value": "1250 kg"},
      {"field": "origen", "value": "Namibia"},
      {"field": "temperatura", "value": "-18°C"}
    ]
  }'

Verificar si un dato ya fue certificado

curl "https://app.codecontract.io/api/v1/smartcheck/verify?field=peso_neto&value=1250%20kg" \
  -H "Authorization: Bearer cc_live_..."

# Respuesta: { "certified": true, "sha256": "abc...", "evidence": { ... } }

Descargar documento certificado

curl https://app.codecontract.io/api/v1/smartcheck/EVIDENCE_ID?download=true \
  -H "Authorization: Bearer cc_live_..." -o documento.pdf

Consultar certificados por fecha

curl "https://app.codecontract.io/api/v1/smartcheck?from=2026-01-01&to=2026-03-31" \
  -H "Authorization: Bearer cc_live_..."

Crear solicitud Trackline

curl -X POST https://app.codecontract.io/api/v1/requests \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"contactId": "...", "title": "Factura marzo", "channel": "EMAIL",
       "subject": "Necesitamos la factura", "body": "Hola, ¿podrías enviarnos...?"}'

Límites y errores

Rate limit: hay DOS limitadores, y el techo práctico son 100 req/min, no 120. El primero está en el borde y muerde antes de llegar al endpoint: 100 peticiones/minuto por (últimos 8 caracteres de tu Authorization + tu IP). Detrás hay un segundo de 120/minuto por llave, compartido por toda la v1, que solo alcanzas si reparte el tráfico entre varias IPs. Los dos responden 429 con Retry-After en segundos, y en los dos la llave sigue siendo válida: espera y reintenta, no generes otra. Lo que cambia es el cuerpo — el del borde es { error: "Rate limit exceeded", retryAfter } y el de la llave { error: "rate_limited", retryAfterSeconds, hint }—, así que si discriminas, mira Retry-After, que está en ambos. Además, /api/v1/uploads/presign tiene un TERCER techo propio de 200 presigns por hora. Las cabeceras X-RateLimit-* viajan en el 200 de /api/v1/me (cupo de la llave) y en el 429 del borde.

Excepción: /api/mcp (más abajo) comparte el mismo cupo pero sigue devolviendo 401 al agotarlo, no 429. Si integras por MCP, un 401 repentino con una llave que sabes buena sigue siendo, probablemente, el límite.

Cambio de comportamiento (08/09/2026). Hasta esa fecha solo /api/v1/me y /api/v1/uploads/presign devolvían 429; en el resto el cupo agotado salía como 401, indistinguible de una llave revocada — y regenerar la llave no arreglaba nada, porque el cubo va por llave y seguía lleno. Si tu integración trata el 401 como «credencial mala», sigue siendo correcta: simplemente dejará de verlo por este motivo.

Tamaño máximo: 30 MB por archivo. Pero un multipart no puede pasar de 4,5 MB (tope de la plataforma para el cuerpo de una petición): por encima de eso, el único camino es /api/v1/uploads/presign. Pasarse devuelve 400 file_too_large (o 400 too_big en el presign), con el máximo en la respuesta.

Batch: puedes certificar varios archivos o campos en una petición. No hay un tope numérico impuesto, pero cada elemento consume su crédito y se procesan en serie: en lotes muy grandes te arriesgas a un timeout con parte del lote ya certificada. Ve por tandas.

Errores: Todas las respuestas de error siguen el formato {"error": "mensaje"}. Los más habituales:

  • 400 — parámetro mal formado: fecha ilegible, invalid_cursor, invalid_include, too_many_names (máximo 50), invalid_if_match, file_too_large, too_big / unsupported_type en el presign.
  • 401 — llave ausente, inválida, revocada o caducada. En /api/v1 ya no significa «límite agotado»: eso es un 429. En /api/mcp todavía puede serlo.
  • 429 — cupo de la llave agotado. Trae Retry-After; la llave es válida.
  • 402 insufficient_credits — no quedan créditos para certificar. Es el fallo más probable de un lote grande en SmartCheck.
  • 403 insufficient_scope — la llave es válida pero le falta el alcance. La respuesta dice cuál necesitas.
  • 404 — no existe en la organización de tu llave. También no_file al descargar algo que no tiene binario.
  • 409 — dos casos distintos, no los trates igual: version_conflict al hacer PATCH con If-Match (recarga y reintenta) y email_conflict al dar de alta un contacto que ya existe (reintentar no sirve de nada).
  • 410 document_deleted — el documento estaba y se borró.
  • 503 exact_match_unavailable — la búsqueda exacta no está disponible en este entorno; usa names.
  • 5xx — algunos traen un shortCode tipo E-260828-A1B2; pásaselo a soporte y localizamos el fallo exacto. Todavía no lo emiten todos los endpoints, así que si no viene, dinos la hora y la ruta.