Constaia
Guías por caso

Documentos del formulario I-9

Verifica los documentos de las listas A, B y C que presenta un nuevo empleado para el formulario I-9, etiquétalos por lista y prerrellena la sección 2 para RR. HH., con Node o Python.

Cuando contratas a alguien en Estados Unidos, el formulario I-9 deja constancia de que has examinado documentos que acreditan la identidad del empleado y su autorización para trabajar. El empleado presenta un documento de la lista A, o uno de la lista B más uno de la lista C, y el empleador anota en la sección 2 el título, la autoridad emisora, el número y la fecha de caducidad de cada documento.

Constaia puede quitarte el trabajo mecánico de ese paso: leer cada documento con su tipo del catálogo, decirte a qué lista pertenece, validar el formato de sus identificadores y prerrellenar un borrador de la sección 2 para la persona que lo revisa. No decide si alguien puede trabajar.

Las listas y sus tipos en Constaia

ListaAcreditaTipos del catálogo
AIdentidad y autorización de trabajous_passport_book, us_passport_card, us_permanent_resident_card, us_foreign_passport_i551, us_ead, us_i94
BSolo identidadus_driver_license, us_state_id, us_school_id, us_voter_registration_card, us_military_id, us_tribal_document, ca_driver_licence
CSolo autorización de trabajous_ssn_card, us_birth_certificate, us_consular_birth_report, us_tribal_document, us_citizen_id_card, us_resident_citizen_id_card

us_tribal_document aparece en las listas B y C. Cada tipo indica sus listas en el campo i9_lists de GET /v1/document-types, que es público y no necesita clave. El propio formulario cumplimentado también tiene tipo: us_i9_form.

Es un resumen simplificado. Las listas oficiales, sus excepciones (recibos, prórrogas automáticas, tarjetas de la Seguridad Social con restricciones, el I-94 junto con un pasaporte extranjero…) y cómo examinar los documentos están en el Handbook for Employers (M-274) de USCIS. Sigue ese manual, no esta página.

Lo que Constaia no hace

Constaia no determina la elegibilidad para trabajar, no se conecta a E-Verify ni a ninguna base de datos pública y no sustituye el examen de los documentos que USCIS exige al empleador. Etiqueta a qué lista pertenece cada documento, extrae datos y señala problemas; si la combinación de documentos está completa lo decide tu código, y tu equipo de RR. HH. completa y firma la sección 2. El empleador sigue siendo responsable del formulario I-9. Esta guía no es asesoramiento jurídico.

Deja que el empleado elija

El empleador no debe indicar qué documentos presenta el empleado, pedir más documentos de los necesarios ni rechazar un documento por su tipo o por la ciudadanía u origen nacional del empleado. En la práctica:

  • Muestra al empleado todas las opciones aceptables (un documento de la lista A, o uno de la lista B más uno de la lista C) y deja que elija.
  • Usa Constaia solo sobre los documentos que el empleado ha elegido. No construyas reglas que favorezcan un documento frente a otro.
  • Trata los avisos y los motivos del veredicto como motivo para que una persona vuelva a mirar, nunca como rechazo automático.

Las normas contra la discriminación en torno al I-9 son estrictas. Revisa tu flujo con tu asesoría jurídica.

El flujo

El empleado elige y sube

Tu app de incorporación guarda la elección del empleado (A, o B + C) y envía cada fichero a tu backend. La clave de API se queda en tu servidor.

Tu backend verifica cada documento

Para cada documento, llama a POST /v1/analyze con expect igual a los tipos de la lista elegida. Recibes el tipo detectado, los campos, las comprobaciones de formato y un verdict cuyos motivos incluyen la lista del I-9.

Una persona revisa y firma

Enseña al revisor el borrador prerrellenado, la confianza de cada campo y los motivos. Examina los documentos como exige el M-274, corrige lo necesario y completa la sección 2.

Qué devuelve Constaia

Cuando analizas un documento con expect, verdict.reasons incluye un motivo informativo con su lista:

verdict.reasons (extracto)
[
  { "code": "type_match", "severity": "info", "message": "El documento es Licencia de conducir (EE. UU.)." },
  { "code": "i9_list", "severity": "info", "message": "Documento aceptable para el formulario I-9: lista B." }
]

Además, según el tipo:

  • Números USCIS (A-Number de la Permanent Resident Card, del EAD o del sello I-551): formato validado con el esquema us_uscis, en checks[] como id_number_format.
  • Número de la Seguridad Social: formato validado con us_ssn. Solo el formato: Constaia no lo contrasta con la Social Security Administration.
  • Permisos de conducir e ID estatales: número validado según el estado (us_dl) y lectura del código PDF417 del reverso cruzada con el anverso (aamva_matches_visual). Detalles en Permiso de conducir de EE. UU..
  • Pasaportes de EE. UU.: dígitos de control de la MRZ y cruce de la MRZ con los datos impresos.
  • Caducidad: not_expired está activo por defecto en los tipos con caducidad. En el I-9 conviene desactivarlo (not_expired: false) y marcar la fecha para el revisor, porque hay documentos caducados que siguen siendo aceptables (por ejemplo, con prórroga automática).
  • Titular: con holder puedes comparar el nombre del documento con el de la sección 1.

Prerrellenar la sección 2

El ejemplo carga una vez las listas desde el catálogo público, analiza cada documento con los tipos de la lista que eligió el empleado y devuelve un borrador para el revisor. isComplete comprueba la combinación (A, o B más C en documentos distintos); tu código decide qué hacer con ese resultado.

npm i @constaia/sdk
i9-draft.ts
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const constaia = new Constaia(); // lee CONSTAIA_API_KEY

type ListId = "A" | "B" | "C";

// Catálogo público (sin clave): tipo → listas del I-9.
async function loadI9Lists(): Promise<Map<string, ListId[]>> {
  const res = await fetch("https://api.constaia.com/v1/document-types?limit=500");
  const { data } = (await res.json()) as { data: { type: string; i9_lists: ListId[] | null }[] };
  return new Map(data.filter((t) => t.i9_lists?.length).map((t) => [t.type, t.i9_lists!]));
}
const I9_LISTS = await loadI9Lists();
const typesFor = (list: ListId) => [...I9_LISTS].filter(([, lists]) => lists.includes(list)).map(([type]) => type);

// Campos que no se llaman document_number / expiry_date en algunos tipos.
const NUMBER_FIELD: Record<string, string> = {
  us_ssn_card: "ssn",
  us_i94: "admission_number",
  us_birth_certificate: "certificate_number",
  us_consular_birth_report: "certificate_number",
};
const EXPIRY_FIELD: Record<string, string> = {
  us_i94: "admit_until_date",
  us_foreign_passport_i551: "stamp_expiry_date",
};

export async function draftSection2(path: string, list: ListId, employeeId: string, section1Name: string) {
  const analysis = await constaia.analyze(await fromPath(path), {
    expect: typesFor(list),
    checks: { notExpired: false, holder: { fullName: section1Name } },
    storage: "none",
    keepResults: false,
    language: "en", // título del documento en inglés, como en el formulario
    metadata: { employee_id: employeeId, i9_list: list },
  });
  if (analysis.status !== "completed" || !analysis.verdict) return { status: "pending", analysisId: analysis.id };

  const type = analysis.document?.type ?? "generic";
  const value = (name: string) => {
    const v = analysis.fields[name]?.value;
    return typeof v === "string" && v.trim() ? v.trim() : null;
  };

  // Todo lo que no es informativo va al revisor: tipo distinto, titular distinto, formato de número, calidad…
  const flags = analysis.verdict.reasons
    .filter((r) => r.severity !== "info")
    .map((r) => `${r.code}: ${r.message}`);
  for (const [name, field] of Object.entries(analysis.fields)) {
    if (field.value != null && field.confidence < 0.8) flags.push(`low_confidence: ${name}`);
  }
  const expiration = value(EXPIRY_FIELD[type] ?? "expiry_date");
  if (expiration && expiration < new Date().toISOString().slice(0, 10)) flags.push("expiration_date_in_past");

  return {
    status: "draft",
    type,
    lists: I9_LISTS.get(type) ?? [], // igual que el motivo i9_list del veredicto
    documentTitle: analysis.document?.label ?? null,
    issuingAuthority: value("issuing_authority") ?? value("issuing_state") ?? value("tribe"),
    documentNumber: value(NUMBER_FIELD[type] ?? "document_number"),
    expirationDate: expiration ?? "N/A",
    flags, // para el revisor de RR. HH., nunca un rechazo automático
  };
}

// Un documento de la lista A, o uno de la B más otro distinto de la C.
export function isComplete(docs: { lists: ListId[] }[]): boolean {
  if (docs.some((d) => d.lists.includes("A"))) return true;
  return docs.some((b, i) => b.lists.includes("B") && docs.some((c, j) => j !== i && c.lists.includes("C")));
}

draftSection2(process.argv[2] ?? "./document.jpg", "A", "emp_123", "Jane Q Doe")
  .then((draft) => console.log(draft))
  .catch((err) => {
    if (err instanceof ConstaiaError) console.error(`Constaia error ${err.status} ${err.code} (request ${err.requestId})`);
    else console.error(err);
    process.exit(1);
  });

Cada marca es una indicación para el revisor, no un veredicto. Una fecha de caducidad pasada puede seguir siendo aceptable en los casos que describe el M-274 (como las prórrogas automáticas); una diferencia de nombre puede deberse a un apellido de casada; un type_mismatch puede significar que el empleado subió otro documento distinto del que eligió. Decide una persona.

Modo test

Con una clave ck_test_ el simulador no tiene escenarios específicos de EE. UU.: un documento estadounidense se responde con el escenario generic (salvo que el nombre del fichero contenga palabras como passport), así que el veredicto suele ser review con type_unknown y sin motivo i9_list. 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.

Cada documento cuesta 1 crédito (hasta 2 páginas). También puedes usar POST /v1/classify (0,2 créditos) para detectar subidas claramente erróneas, como una página en blanco, antes de analizar.

Si solo quieres unos campos

Si no necesitas veredicto ni etiquetado de listas, puedes pasar tu propio JSON Schema en extract sin expect (por ejemplo con solo título, autoridad, número y caducidad). En ese caso verdict es null, no hay motivo i9_list y las comprobaciones automáticas no se aplican a tus campos.

Privacidad

  • Usa storage: "none" y keep_results: false (sin retención por defecto) y guarda los datos en tu sistema de I-9, no en Constaia. No pongas números de documento en metadata.
  • Conservar o no copias de los documentos es una decisión de tu política; el M-274 espera la misma práctica con todos los empleados. Revisa la conservación con tu asesoría jurídica.
  • Constaia no hace reconocimiento facial ni trata datos biométricos. Actúa como proveedor de servicios según la CCPA.
  • Hoy todo se procesa y se guarda en la UE, también para clientes de EE. UU. Una región de EE. UU. está prevista, próximamente, sin fecha. Consulta Residencia de datos y cumplimiento.

Siguientes pasos

En esta página