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_...) | |
|---|---|---|
| Resultado | Simulado y determinista, según el nombre del fichero | Análisis real con IA |
| Créditos | No reserva ni consume (usage.credits: 0) | Reserva al empezar y cobra al terminar |
livemode en la respuesta | false | true |
| Validación de opciones, errores, idempotencia | Igual que en live | — |
| Límites | Las mismas peticiones por segundo que tu nivel (2 req/s gratis, 10 de pago), sin límite de páginas ni de concurrencia | Peticiones/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 simulados | Se ejecutan sobre los datos extraídos |
| Análisis, listados y uso | Solo ves los de test | Solo ves los de live |
| Webhooks | Solo reciben los endpoints creados con clave test | Solo reciben los endpoints creados con clave live |
| Firma y metadatos de PDF | Se verifican de verdad sobre el fichero que envías | Igual |
| Requisito | Ninguno | Email 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
- Se toma el nombre del fichero sin extensión y en minúsculas.
- Si coincide exactamente con un escenario (
dni_valid,dni_expired,nie,passport,medical_certificate,sexual_offences_certificate,payment_receipt,invoice,blurry), se usa ese. - 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.jpgda el escenarioblurry, ydni_caducado.png,dni_expired. - 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
| Nombre | También coincide con | expect típico | Resultado |
|---|---|---|---|
dni_valid | dni | es_dni | Válido MARÍA GARCÍA LÓPEZ, 12345678Z, nacida el 1990-05-14, caduca el 2031-03-12. |
dni_expired | expired, caducad | es_dni | No válido JUAN PÉREZ SÁNCHEZ, 87654321X, caducado el 2020-06-15: not_expired con severidad error, "Caducado el 15/06/2020." |
nie | nie, tie | es_nie | Válido ANNA KOWALSKA, X1234567L, caduca el 2029-11-30. |
passport | passport, pasaporte | passport | Válido Pasaporte (TD3) MARIA GARCIA LOPEZ, PAA123456, caduca el 2032-06-01. |
medical_certificate | medical, medico, médico | medical_certificate_sport | Válido Apto, firmado y sellado, emitido el 2026-09-01. |
sexual_offences_certificate | sexual, delitos, penales | es_sexual_offences_certificate | Válido Sin antecedentes, CSV MJU4-7K2P-9QXA-3ZTR, emitido el 2026-09-15. |
payment_receipt | receipt, justificante, transfer | payment_receipt | Válido 45 EUR, IBAN beneficiario ES7921000813610123456789, referencia INSCRIPCION 123. |
invoice | invoice, factura | invoice | Válido Factura 20260042, base 100, IVA 21 %, total 121 EUR; invoice_totals superado. |
blurry | blur, borros | es_dni | Revisar 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 motivosignature_missingconwarningy el veredicto pasa areview. Con el PDF original descargado de la sede verássignature_valid. - Un PDF generado con Word, LibreOffice, Canva, iLovePDF u otro editor conocido añade
edited_suspected(warning) y también lleva areview.
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 probar | Fichero | Opciones | Resultado |
|---|---|---|---|
| Tipo equivocado | passport.jpg | {"expect":"es_dni"} | invalid, motivo type_mismatch con severidad error ("Se esperaba DNI (España), pero el documento parece Pasaporte.") |
| Titular distinto | dni_valid.jpg | {"expect":"es_dni","checks":{"holder":{"full_name":"Juan Pérez"}}} | invalid, motivo holder con severidad error |
| Menor de edad | dni_valid.jpg | {"expect":"es_dni","checks":{"min_age_years":40}} | invalid, motivo min_age_years con severidad error (la titular nació en 1990) |
| Certificado antiguo | medical_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 distinto | payment_receipt.pdf | {"expect":"payment_receipt","checks":{"expected_amount":50}} | invalid, motivo expected_amount con severidad error |
| Documento no reconocido | otro.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"}:
{
"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 ejemploCONSTAIA_TEST_API_KEY), y comprueba que empieza porck_test_antes de lanzar nada. - Afirma solo sobre
verdict.status, loscodeyseveritydeverdict.reasonsywarnings. No compares mensajes (dependen del idioma y, en algunos casos, de la fecha), niid, fechas osize_bytes. - Guarda en el repositorio un único JPEG y un único PDF pequeños y envíalos con distintos nombres.
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:
- 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). - Lanza análisis con
async: trueo lotes con la clave test: recibirásanalysis.completed,analysis.review_required(conblurry.jpg),batch.completed… - 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
Qué usar
Cuándo usar analyze, classify, lotes, el widget, el servidor MCP o una herramienta no-code en Constaia, con costes, latencia y límites de cada opción.
Casos de uso
Guías por caso de uso con DNI en inscripciones, certificados médicos y LOPIVI, justificantes, facturas a Excel, lotes, revisión humana y EE. UU.