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
| Lista | Acredita | Tipos del catálogo |
|---|---|---|
| A | Identidad y autorización de trabajo | us_passport_book, us_passport_card, us_permanent_resident_card, us_foreign_passport_i551, us_ead, us_i94 |
| B | Solo identidad | us_driver_license, us_state_id, us_school_id, us_voter_registration_card, us_military_id, us_tribal_document, ca_driver_licence |
| C | Solo autorización de trabajo | us_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:
[
{ "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, enchecks[]comoid_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_expiredestá 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
holderpuedes 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/sdkimport { 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"ykeep_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 enmetadata. - 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
Documentos de EE. UU.
Todos los tipos de EE. UU. y Canadá del catálogo y sus comprobaciones.
Permiso de conducir de EE. UU.
Código PDF417, formato del número por estado, edad y caducidad.
Revisión humana
Monta la cola del revisor para los documentos marcados.
Almacenamiento y privacidad
Qué guarda Constaia y durante cuánto tiempo.
Verificar un permiso de conducir de EE. UU.
Verifica un permiso de conducir o una ID estatal de EE. UU. con el código PDF417 (AAMVA), el formato del número de cada estado, la caducidad y la edad mínima, con Node o Python.
Alta de contratistas con el formulario W-9
Valida el formulario W-9 con el tipo us_w9 sin guardarlo, obtén un veredicto con firma y titular, y recibe el nombre, la clasificación fiscal, el TIN y la dirección.