Constaia
Guías por caso

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:

NecesitasDónde estáQuién decide
¿Es un ACORD 25, está vigente, es el asegurado correcto?verdictConstaia, con los checks que pidas
Pólizas, límites, asegurado adicional, titular del certificadofieldsTu código, con las reglas de tu contrato

Paso 1: veredicto con acord_25

El tipo acord_25 devuelve estos campos:

CampoContenido
certificate_date, certificate_number, revision_numberFecha, número y revisión del certificado.
producerProductor (agente o corredor): name, address, contact_name, phone, email.
insuredAsegurado: name, address.
insurersAseguradoras A–F: letter, name, naic.
policiesUna 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_dateEl vencimiento más próximo de todas las pólizas listadas.
description_of_operationsDescripción de operaciones, ubicaciones o vehículos.
certificate_holderTitular del certificado: name, address.
authorized_representativeRepresentante 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"}'
coi-verdict.ts
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.

coi-rules.ts
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

En esta página