Certificados de seguro (ACORD 25)
Valida los certificados de seguro de proveedores con el tipo acord_25, con vigencia y asegurado comprobados, y lee pólizas, límites y asegurado adicional para aplicar las reglas de tu contrato.
Antes de que un contratista, proveedor o inquilino empiece a trabajar, lo habitual en EE. UU. es pedirle un certificado de seguro (COI), casi siempre en el formulario ACORD 25. Después alguien comprueba que las pólizas están vigentes, que los límites cumplen tu contrato y que tu empresa figura como titular del certificado (certificate holder) o como asegurado adicional (additional insured).
Constaia se encarga de la lectura y de las comprobaciones mecánicas con el tipo de catálogo acord_25. Una sola
llamada te da las dos cosas:
| Necesitas | Dónde está | Quién decide |
|---|---|---|
| ¿Es un ACORD 25, está vigente, es el asegurado correcto? | verdict | Constaia, con los checks que pidas |
| Pólizas, límites, asegurado adicional, titular del certificado | fields | Tu código, con las reglas de tu contrato |
Paso 1: veredicto con acord_25
El tipo acord_25 devuelve estos campos:
| Campo | Contenido |
|---|---|
certificate_date, certificate_number, revision_number | Fecha, número y revisión del certificado. |
producer | Productor (agente o corredor): name, address, contact_name, phone, email. |
insured | Asegurado: name, address. |
insurers | Aseguradoras A–F: letter, name, naic. |
policies | Una entrada por póliza: insurer_letter, type, policy_number, effective_date, expiry_date, additional_insured, subrogation_waived y limits (lista de name y amount en USD). |
policy_expiry_date | El vencimiento más próximo de todas las pólizas listadas. |
description_of_operations | Descripción de operaciones, ubicaciones o vehículos. |
certificate_holder | Titular del certificado: name, address. |
authorized_representative | Representante autorizado. |
En este tipo la vigencia (not_expired) se comprueba por defecto sobre policy_expiry_date, y reference_date te
permite comprobarla en otra fecha, por ejemplo el día en que empieza el trabajo. holder compara el nombre del
asegurado (insured.name) con el que esperas (normalizado, sin tener en cuenta tildes ni orden y tolerando pequeñas
erratas). max_age_days se mide desde certificate_date y require_fields admite rutas con punto, como
certificate_holder.name. La lista completa está en
GET /v1/document-types/acord_25 y en el catálogo.
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@coi.pdf \
-F 'options={"expect":"acord_25","checks":{"reference_date":"2026-11-02","holder":{"full_name":"Acme Roofing LLC"},"require_fields":["certificate_holder.name"]},"storage":"none"}'import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia(); // lee CONSTAIA_API_KEY
export async function checkCoi(path: string, vendorName: string, jobStart: string) {
const analysis = await constaia.analyze(await fromPath(path), {
expect: "acord_25",
checks: {
referenceDate: jobStart,
holder: { fullName: vendorName },
requireFields: ["certificate_holder.name"],
},
storage: "none",
metadata: { vendor: vendorName },
});
return {
analysis,
status: analysis.verdict?.status ?? "pending", // "valid" | "invalid" | "review"
reasons: analysis.verdict?.reasons.map((r) => `${r.code} (${r.severity}): ${r.message}`) ?? [],
policyExpiryDate: analysis.fields.policy_expiry_date?.value ?? null,
};
}
checkCoi(process.argv[2] ?? "./coi.pdf", "Acme Roofing LLC", "2026-11-02")
.then(({ status, reasons, policyExpiryDate }) => console.log({ status, reasons, policyExpiryDate }))
.catch((err) => {
if (err instanceof ConstaiaError) console.error(`Constaia error ${err.status} ${err.code} (request ${err.requestId})`);
else console.error(err);
process.exit(1);
});Un certificado con alguna póliza vencida a esa fecha vuelve como invalid con un motivo not_expired de severidad
error; un asegurado distinto da un error holder, y un documento que no es un ACORD 25, un error type_mismatch.
Consulta Veredictos y Comprobaciones.
policy_expiry_date es el vencimiento más próximo de todas las pólizas listadas. Si el certificado incluye una
línea que tu contrato no exige y que vence antes, not_expired dará invalid igualmente. En ese caso manda
"not_expired": false y compara las fechas de cada póliza en tu código, como en el paso 2.
Paso 2: reglas del contrato en tu código
Los límites mínimos, las líneas de cobertura exigidas y el asegurado adicional dependen de cada contrato, así que se
comprueban en tu código con los fields de la misma respuesta. No hace falta una segunda llamada.
import type { Analysis } from "@constaia/sdk";
const minimumLimits: Record<string, Record<string, number>> = {
"general liability": { "each occurrence": 1_000_000, "general aggregate": 2_000_000 },
};
const requirements = {
holderMustContain: "YOUR COMPANY INC",
requiredLines: ["general liability", "workers compensation"],
additionalInsuredOn: "general liability",
minimumLimits,
};
type Limit = { name?: string; amount?: number };
type Policy = { type?: string; policy_number?: string; expiry_date?: string; additional_insured?: boolean; limits?: Limit[] };
const has = (text: string | undefined, needle: string) => (text ?? "").toLowerCase().includes(needle);
export function checkContractRules(analysis: Analysis, jobEnd: string) {
const issues: string[] = (analysis.verdict?.reasons ?? [])
.filter((r) => r.severity !== "info")
.map((r) => `${r.code}:${r.severity}`);
const f = analysis.fields;
const policies = (f.policies?.value as Policy[] | undefined) ?? [];
for (const line of requirements.requiredLines) {
const p = policies.find((x) => has(x.type, line));
if (!p) issues.push(`missing_line:${line}`);
else if (!p.expiry_date || p.expiry_date < jobEnd) issues.push(`expires_before_job_end:${line}`);
}
const ai = policies.find((x) => has(x.type, requirements.additionalInsuredOn));
if (ai && ai.additional_insured !== true) issues.push("additional_insured_not_marked");
for (const [line, mins] of Object.entries(requirements.minimumLimits)) {
const limits = policies.find((x) => has(x.type, line))?.limits ?? [];
for (const [name, min] of Object.entries(mins)) {
const amount = limits.find((l) => has(l.name, name))?.amount;
if (typeof amount !== "number" || amount < min) issues.push(`limit_below_minimum:${line}:${name}`);
}
}
const holder = (f.certificate_holder?.value as { name?: string } | null)?.name ?? "";
if (!holder.toUpperCase().includes(requirements.holderMustContain)) issues.push("certificate_holder_mismatch");
return { ok: analysis.verdict?.status === "valid" && issues.length === 0, issues };
}type y los nombres de los límites se devuelven tal como aparecen en el formulario (por ejemplo
commercial general liability o each occurrence), por eso se comparan por contenido. Las fechas se comparan como
cadenas YYYY-MM-DD, que se ordenan correctamente. Envía los certificados con incidencias a
revisión humana o de vuelta al corredor del proveedor.
Un COI es informativo
Un certificado ACORD 25 no modifica ni amplía la cobertura, y la marca de "additional insured" suele depender de un suplemento (endorsement) de la póliza. Constaia lee lo que dice el certificado; no contacta con aseguradoras ni corredores. Para trabajos de alto riesgo, pide los suplementos y revisa tus requisitos con tu equipo de riesgos o tu asesoría jurídica; esta guía no es asesoramiento jurídico.
Otros certificados
Para certificados que no siguen el formulario ACORD 25, el tipo genérico insurance_certificate (cualquier país)
extrae insurer, policy_number, holder, coverage, valid_from y valid_until, con not_expired por defecto.
Si necesitas un dato que ningún tipo del catálogo devuelve, puedes pasar tu propio JSON Schema en extract; sin
expect no habrá verdict y las reglas irán en tu código.
Modo test
El simulador del modo test (claves ck_test_) no tiene escenarios de documentos de EE. UU. Un ACORD 25 se responde
con el escenario generic, así que con expect: "acord_25" el veredicto es review con el motivo type_unknown
(los nombres de fichero que contienen invoice o receipt se responden con escenarios de factura y justificante de
pago españoles). Úsalo para probar tus rutas de error; para ver resultados reales usa una clave live. El plan
gratuito incluye 150 créditos al mes; los créditos live gratuitos requieren un email verificado. Consulta
Modo test.
Próximamente: región de EE. UU.
Hoy todo el procesamiento y el almacenamiento, también el de clientes de EE. UU., se hace en la UE. Está prevista una región de EE. UU., sin fecha todavía. Consulta Residencia de datos y cumplimiento.
Siguientes pasos
Documentos de EE. UU.
Todos los tipos de EE. UU. del catálogo y qué comprueba cada uno.
Comprobaciones
not_expired, reference_date, holder y el resto.
Revisión humana
Envía a una persona los certificados con incidencias.
Lotes masivos
Revisa toda una lista de proveedores en una petición.
Formulario W-9 para contratistas
Recoge los datos fiscales de esos mismos proveedores.
Verificación de ingresos con nóminas y extractos bancarios
Analiza en un lote nóminas y extractos bancarios de EE. UU. con los tipos us_paystub y us_bank_statement, con antigüedad y titular comprobados, y calcula el ingreso mensual en tu código.
Justificante de domicilio con facturas de suministros
Verifica una factura de suministros o un extracto bancario de EE. UU. como justificante de domicilio, con su antigüedad, el titular y el código ZIP, y compara la dirección con tu formulario.