Constaia
Guías por caso

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.

Antes de pagar a un contratista (contractor) de EE. UU., recoges un formulario W-9 con su nombre legal, nombre comercial, clasificación fiscal federal, dirección y número de identificación fiscal (TIN: un SSN o un EIN). Leer esos PDF a mano es lento y propenso a errores; guardar copias por ahí es un riesgo.

Esta guía usa el tipo de catálogo us_w9 con expect: Constaia extrae los campos, comprueba el formato del TIN, la firma y el nombre, y devuelve un verdict, sin guardar nunca el documento. El cotejo del TIN con el IRS sigue siendo cosa tuya.

El flujo

El contratista sube el W-9 firmado

Tu portal de alta envía el PDF o la foto a tu backend. La clave de API nunca sale de tu servidor.

Tu backend analiza el W-9 sin guardarlo

Llama a POST /v1/analyze con expect: "us_w9", los checks que necesites, storage: "none" y keep_results: false. La respuesta es el único sitio donde aparece el TIN; en Constaia no queda nada.

Tu código decide y guarda

Según verdict.status (valid, review o invalid) das de alta al contratista, lo mandas a revisión o le pides otro formulario. Guardas el TIN cifrado en tu sistema y pasas el registro a tu equipo de pagos o al cotejo del TIN.

El tipo us_w9

El tipo us_w9 devuelve estos campos:

CampoContenido
nameLínea 1: nombre tal como figura en la declaración de impuestos.
business_nameLínea 2: nombre comercial o de la entidad no considerada (disregarded entity).
tax_classificationClasificación fiscal federal (individual, C corp, S corp, partnership, LLC…).
exempt_payee_codeCódigo de beneficiario exento, si figura.
address, postal_codeDirección y código ZIP.
ssn, einEl TIN, en la casilla en la que se escribió (solo una suele tener valor).
signature_present, signature_dateSi está firmado y la fecha de la firma.
form_revisionRevisión del formulario, por ejemplo Rev. March 2024.

Con cada análisis, Constaia valida el formato de ssn (esquema us_ssn), ein (us_ein) y postal_code (us_zip). El resultado aparece en checks[] con el código id_number_format. Estas validaciones son soft: si fallan, el motivo llega a verdict.reasons con severidad warning y el veredicto pasa a review, no a invalid.

Los checks que admite el tipo son holder, require_fields, require_signature, max_age_days (sobre signature_date) y reference_date. La lista completa, siempre al día, está en GET /v1/document-types/us_w9 y en el catálogo.

Analizar y decidir

Solo formato

id_number_format comprueba la estructura del número (nueve cifras, rangos no asignados como 000, 666 o 9xx en un SSN, prefijos de EIN válidos). No es una validación contra el IRS ni la Social Security Administration: un TIN con formato correcto puede seguir siendo erróneo o de otra persona.

w9.ts
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

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

type Contractor = { id: string; legalName: string; tin?: string }; // lo que escribió en tu formulario de alta

export async function processW9(path: string, contractor: Contractor) {
  const analysis = await constaia.analyze(await fromPath(path), {
    expect: "us_w9",
    checks: {
      requireSignature: true,
      requireFields: ["name", "tax_classification"],
      holder: { fullName: contractor.legalName },
    },
    storage: "none",
    keepResults: false,
    metadata: { contractor_id: contractor.id }, // nunca el TIN
  });
  if (analysis.status !== "completed") return { status: "pending", analysisId: analysis.id };

  const v = (name: string) => analysis.fields[name]?.value ?? null;
  const tinType = v("ein") ? "ein" : v("ssn") ? "ssn" : null;
  const tin = String(v("ein") ?? v("ssn") ?? "").replace(/\D/g, "");

  // Motivos que no son informativos: type_mismatch, holder, require_signature, id_number_format…
  const issues = (analysis.verdict?.reasons ?? [])
    .filter((r) => r.severity !== "info")
    .map((r) => `${r.code}:${r.severity}`);
  if (contractor.tin && contractor.tin.replace(/\D/g, "") !== tin) issues.push("tin_differs_from_form:error");

  const status =
    analysis.verdict?.status === "invalid" ? "rejected" : issues.length ? "needs_review" : "ok";

  return {
    status,
    issues,
    record: {
      name: v("name"),
      businessName: v("business_name"),
      taxClassification: v("tax_classification"),
      exemptPayeeCode: v("exempt_payee_code"),
      address: v("address"),
      postalCode: v("postal_code"),
      tinType,
      tin, // guárdalo cifrado; muestra y registra solo los 4 últimos
      tinLast4: tin.slice(-4),
      signatureDate: v("signature_date"),
      formRevision: v("form_revision"),
    },
  };
}

processW9(process.argv[2] ?? "./w9.pdf", { id: "ctr_42", legalName: "Specimen Consulting LLC" })
  .then(({ status, issues, record }) => console.log({ status, issues, tinLast4: record?.tinLast4 }))
  .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 W-9 con un SSN mal formado y todo lo demás correcto vuelve así (con language: "es"):

verdict
{
  "expected": ["us_w9"],
  "match": true,
  "status": "review",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "El documento es Formulario W-9 (EE. UU.)." },
    { "code": "holder", "severity": "info", "message": "Los datos del titular coinciden (full_name)." },
    { "code": "require_signature", "severity": "info", "message": "El documento está firmado." },
    { "code": "id_number_format", "severity": "warning", "message": "Validación fallida: ssn (us_ssn) no tiene un formato válido: 666123456." }
  ]
}

Qué significa cada resultado:

  • valid: es un W-9, está firmado, el nombre coincide y el TIN tiene un formato correcto.
  • review: algo merece una mirada: un TIN con formato inválido, un campo que no se pudo leer, baja calidad de imagen o baja confianza. Envíalo a revisión humana.
  • invalid: no es un W-9 (type_mismatch, por ejemplo un W-8BEN), falta la firma, falta un campo obligatorio o el nombre no coincide. Pide al contratista un formulario nuevo.

ITIN en la casilla del SSN

El campo ssn se valida con el esquema us_ssn, que rechaza los números que empiezan por 9. Un ITIN escrito en la casilla del SSN, algo habitual y correcto en un W-9, da por eso un aviso id_number_format y el veredicto review. Si aceptas ITIN, compruébalo en tu código (nueve cifras que empiezan por 9) antes de mandar el formulario a revisión.

Un W-9 completo con sus instrucciones tiene 6 páginas y cuesta 3 créditos (1 crédito hasta 2 páginas, +1 por cada 2 páginas más). Si pides solo la primera página, cuesta 1. Consulta Créditos y facturación.

Contratistas con EIN: la carta del IRS

Si el contratista es una empresa, puedes pedir también la carta en la que el IRS le asignó el EIN (CP 575) o se lo confirmó (147C). El tipo us_irs_ein_letter extrae business_name, ein, address, postal_code, notice_type e issue_date. En este tipo la validación del EIN no es soft: un EIN con formato inválido da invalid.

Para cruzarla con el W-9, manda el nombre y el EIN leídos del W-9 en holder:

ein-letter.ts
// w9: el registro (record) que devuelve processW9
const letter = await constaia.analyze(await fromPath("./cp575.pdf"), {
  expect: "us_irs_ein_letter",
  checks: { holder: { fullName: w9.businessName ?? w9.name, documentNumber: w9.tin } },
  storage: "none",
  keepResults: false,
});
console.log(letter.verdict?.status); // "valid" si nombre y EIN coinciden con el W-9

holder.document_number se compara sin espacios ni guiones. Que la carta coincida con el W-9 no sustituye al cotejo con el IRS: ambos documentos los aporta el propio contratista.

Tratamiento del TIN

  • Trátalo como dato muy sensible. Mantén storage: "none" y keep_results: false, así Constaia no guarda ni el fichero ni los campos extraídos (solo los metadatos de facturación). No pongas nunca el TIN en metadata, logs ni mensajes de error; muestra solo los cuatro últimos dígitos.
  • Cífralo en reposo en tu sistema y limita quién puede leerlo.
  • Cotéjalo con el IRS tú mismo. Constaia valida el formato de SSN y EIN, pero no consulta al IRS (no usa el programa TIN Matching) ni a la Social Security Administration ni ninguna otra base de datos pública. Si necesitas confirmar que la combinación de nombre y TIN es correcta, usa TIN Matching si cumples los requisitos, o el servicio de tu proveedor de pagos.
  • Firma. require_signature comprueba que hay una firma visible en la casilla Sign Here; no verifica quién firmó.

Constaia actúa como proveedor de servicios (service provider) según la CCPA y dentro de tu programa de seguridad de la GLBA si te aplica. Consulta con tu asesoría jurídica o fiscal la retención de respaldo (backup withholding), la conservación de registros y los requisitos del W-9 electrónico; esta guía no es asesoramiento jurídico.

Modo test

El simulador del modo test (claves ck_test_) no tiene escenarios de documentos de EE. UU. Un W-9 se responde con el escenario generic, así que con expect: "us_w9" 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 tu integración y tus rutas de error; para ver resultados reales de un W-9 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