Constaia

Modo test

Cómo funcionan las claves ck_test_ de Constaia, qué devuelve cada fichero de prueba y cómo escribir tests automatizados con Vitest, PHPUnit o pytest.

Las claves ck_test_... llaman a la misma API, con las mismas rutas, validaciones de opciones y formato de respuesta, pero en vez de analizar el documento con IA devuelven un resultado determinista elegido por el nombre del fichero. Sirven para desarrollar, para CI y para enseñar el producto sin gastar créditos.

Test frente a live

Modo test (ck_test_...)Modo live (ck_live_...)
ResultadoSimulado y determinista, según el nombre del ficheroAnálisis real con IA
CréditosNo reserva ni consume (usage.credits: 0)Reserva al empezar y cobra al terminar
livemode en la respuestafalsetrue
Validación de opciones, errores, idempotenciaIgual que en live—
LímitesLas mismas peticiones por segundo que tu nivel (2 req/s gratis, 10 de pago), sin límite de páginas ni de concurrenciaPeticiones/s, análisis simultáneos y páginas/min de tu nivel
Checks deterministas (nif_check_digit, MRZ, IBAN…)Se ejecutan de verdad sobre los datos simuladosSe ejecutan sobre los datos extraídos
Análisis, listados y usoSolo ves los de testSolo ves los de live
WebhooksSolo reciben los endpoints creados con clave testSolo reciben los endpoints creados con clave live
Firma y metadatos de PDFSe verifican de verdad sobre el fichero que envíasIgual
RequisitoNingunoEmail verificado

Los checks que pides (holder, min_age_years, max_age_days, expected_amount…) se evalúan sobre los datos simulados igual que en live, así que puedes probar también los casos de error.

Cómo se elige el resultado

  1. Se toma el nombre del fichero sin extensión y en minúsculas.
  2. Si coincide exactamente con un escenario (dni_valid, dni_expired, nie, passport, medical_certificate, sexual_offences_certificate, payment_receipt, invoice, blurry), se usa ese.
  3. Si no, se buscan palabras clave en este orden: blur/borros, expired/caducad, nie/tie, passport/pasaporte, medical/medico/médico, sexual/delitos/penales, receipt/justificante/transfer, invoice/factura, dni. La primera que aparezca gana: dni_borroso.jpg da el escenario blurry, y dni_caducado.png, dni_expired.
  4. Si no hay ninguna, el documento es generic.

El contenido tiene que ser un fichero real JPEG, PNG, WEBP, HEIC o PDF: el tipo se detecta por los primeros bytes, no por la extensión. Cualquier foto o PDF sirve. No necesitas un fichero por escenario: el nombre que cuenta es el que envías en la petición (el filename del multipart), así que puedes reutilizar el mismo fichero con nombres distintos.

Escenarios

NombreTambién coincide conexpect típicoResultado
dni_validdnies_dniVálido MARÍA GARCÍA LÓPEZ, 12345678Z, nacida el 1990-05-14, caduca el 2031-03-12.
dni_expiredexpired, caducades_dniNo válido JUAN PÉREZ SÁNCHEZ, 87654321X, caducado el 2020-06-15: not_expired con severidad error, "Caducado el 15/06/2020."
nienie, tiees_nieVálido ANNA KOWALSKA, X1234567L, caduca el 2029-11-30.
passportpassport, pasaportepassportVálido Pasaporte (TD3) MARIA GARCIA LOPEZ, PAA123456, caduca el 2032-06-01.
medical_certificatemedical, medico, médicomedical_certificate_sportVálido Apto, firmado y sellado, emitido el 2026-09-01.
sexual_offences_certificatesexual, delitos, penaleses_sexual_offences_certificateVálido Sin antecedentes, CSV MJU4-7K2P-9QXA-3ZTR, emitido el 2026-09-15.
payment_receiptreceipt, justificante, transferpayment_receiptVálido 45 EUR, IBAN beneficiario ES7921000813610123456789, referencia INSCRIPCION 123.
invoiceinvoice, facturainvoiceVálido Factura 20260042, base 100, IVA 21 %, total 121 EUR; invoice_totals superado.
blurryblur, borroses_dniRevisar El mismo DNI que dni_valid con warnings: ["blurry", "low_quality"] y el motivo low_quality con severidad warning.
cualquier otro——Tipo generic sin campos. Sin expect, verdict es null. Con expect, motivo type_unknown (warning) y Revisar.

No hay escenarios para los tipos de EE. UU. ni para el resto del catálogo: un permiso de conducir o un W-9 en modo test da generic y, con expect, review. Para ver esos tipos, usa una clave live (los 150 créditos gratis del mes sirven).

Los PDF se analizan de verdad

La firma electrónica y los metadatos de un PDF no se simulan: se leen del fichero que envías. Por eso:

  • Un PDF sin firma con expect: "es_sexual_offences_certificate" (un tipo que se descarga firmado) añade el motivo signature_missing con warning y el veredicto pasa a review. Con el PDF original descargado de la sede verás signature_valid.
  • Un PDF generado con Word, LibreOffice, Canva, iLovePDF u otro editor conocido añade edited_suspected (warning) y también lleva a review.

Para obtener exactamente los veredictos de esta tabla, usa una imagen (JPEG o PNG) con el nombre del escenario, o un PDF exportado por una herramienta que no sea un editor. Ver Firmas digitales en PDF.

Los mensajes con fechas relativas, como "Emitido hace 28 días (máximo 365).", dependen del día en que ejecutes la prueba.

Ejemplos de respuesta

Forzar casos de error

Además de dni_expired y blurry, puedes provocar otros resultados combinando un fichero con opciones:

Qué quieres probarFicheroOpcionesResultado
Tipo equivocadopassport.jpg{"expect":"es_dni"}invalid, motivo type_mismatch con severidad error ("Se esperaba DNI (España), pero el documento parece Pasaporte.")
Titular distintodni_valid.jpg{"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"}}}invalid, motivo holder con severidad error
Menor de edaddni_valid.jpg{"expect":"es_dni","checks":{"min_age_years":40}}invalid, motivo min_age_years con severidad error (la titular nació en 1990)
Certificado antiguomedical_certificate.pdf{"expect":"medical_certificate_sport","checks":{"max_age_days":7}}invalid, motivo max_age_days con severidad error (emitido el 2026-09-01)
Importe distintopayment_receipt.pdf{"expect":"payment_receipt","checks":{"expected_amount":50}}invalid, motivo expected_amount con severidad error
Documento no reconocidootro.jpg{"expect":"es_dni"}review, motivo type_unknown con severidad warning

Mensajes en otro idioma

language cambia el idioma de reasons[].message, de los mensajes de checks y de document.label. Por ejemplo, dni_valid.jpg con {"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"},"min_age_years":18},"language":"en"}:

verdict
{
  "expected": ["es_dni"],
  "match": true,
  "status": "invalid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." },
    { "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." },
    { "code": "age", "severity": "info", "message": "The holder is 36 years old." },
    { "code": "holder", "severity": "error", "message": "Holder mismatch: full_name is “MARÍA GARCÍA LÓPEZ”, expected “Juan Pérez”." }
  ]
}

Los códigos no cambian con el idioma: programa siempre contra code y severity.

Tests automatizados

Recomendaciones para que tus tests no se rompan:

  • Usa una clave ck_test_ dedicada a CI, en una variable de entorno propia (por ejemplo CONSTAIA_TEST_API_KEY), y comprueba que empieza por ck_test_ antes de lanzar nada.
  • Afirma solo sobre verdict.status, los code y severity de verdict.reasons y warnings. No compares mensajes (dependen del idioma y, en algunos casos, de la fecha), ni id, fechas o size_bytes.
  • Guarda en el repositorio un único JPEG y un único PDF pequeños y envíalos con distintos nombres.
test/constaia.test.ts
import { beforeAll, describe, expect, it } from "vitest";
import { Constaia, type Analysis } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const apiKey = process.env.CONSTAIA_TEST_API_KEY ?? "";
const constaia = new Constaia({ apiKey });

// Mismo fichero real, distinto nombre: el nombre elige el escenario.
const jpeg = (name: string) => fromPath("test/fixtures/sample.jpg", name);

const codes = (a: Analysis, severity?: string): string[] =>
  (a.verdict?.reasons ?? []).filter((r) => !severity || r.severity === severity).map((r) => r.code);

describe("validación de DNI", () => {
  beforeAll(() => {
    if (!apiKey.startsWith("ck_test_")) throw new Error("CONSTAIA_TEST_API_KEY debe ser una clave ck_test_");
  });

  it("acepta un DNI vigente", async () => {
    const analysis = await constaia.analyze(await jpeg("dni_valid.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("valid");
    expect(codes(analysis)).toContain("type_match");
    expect(analysis.warnings).toEqual([]);
  });

  it("rechaza un DNI caducado", async () => {
    const analysis = await constaia.analyze(await jpeg("dni_expired.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("invalid");
    expect(codes(analysis, "error")).toContain("not_expired");
  });

  it("manda a revisión una foto borrosa", async () => {
    const analysis = await constaia.analyze(await jpeg("blurry.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("review");
    expect(analysis.warnings).toEqual(expect.arrayContaining(["blurry", "low_quality"]));
    expect(codes(analysis, "warning")).toContain("low_quality");
  });

  it("rechaza un pasaporte cuando se espera un DNI", async () => {
    const analysis = await constaia.analyze(await jpeg("passport.jpg"), { expect: "es_dni" });
    expect(analysis.verdict?.status).toBe("invalid");
    expect(codes(analysis, "error")).toContain("type_mismatch");
  });
});

Si lanzas muchos tests en paralelo, recuerda que el modo test tiene el mismo límite de peticiones por segundo que tu nivel (2 req/s en el plan gratuito): los SDKs reintentan los 429 respetando Retry-After, pero conviene limitar la concurrencia. Ver Límites de uso.

Webhooks en modo test

Cada endpoint de webhook tiene el modo de la clave con que se creó (mode: "test" o "live") y solo recibe eventos de ese modo. Para probar webhooks en desarrollo:

  1. Crea el endpoint con tu clave ck_test_ (POST /v1/webhook-endpoints) apuntando a una URL HTTPS pública (por ejemplo, un túnel hacia tu máquina).
  2. Lanza análisis con async: true o lotes con la clave test: recibirás analysis.completed, analysis.review_required (con blurry.jpg), batch.completed…
  3. Desde el panel también puedes enviar un evento de prueba (type: "test") y ver las últimas 100 entregas.

credits.low solo existe en modo live. Para tests unitarios del receptor sin red, genera cabeceras firmadas con signWebhook (SDK de JavaScript), Webhook::headers (SDK de PHP) o constaia.webhooks.sign (SDK de Python). Ver Webhooks.

Cuando pases a live

Las claves live nunca reciben resultados simulados. Si el entorno no tiene proveedores de IA reales disponibles, las llamadas con ck_live_ fallan con 503 live_mode_unavailable (en el SDK de JavaScript, APIError). Sigue usando la clave test mientras tanto. Los pasos para pasar a producción están en el inicio rápido.

Siguientes pasos

En esta página