Constaia
Guías por caso

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.

Las aperturas de cuenta, el alta de inquilinos, las membresías locales o los descuentos para residentes suelen pedir una factura reciente de suministros (utility bill) o un extracto bancario como justificante de domicilio. Las preguntas son siempre las mismas: ¿está a nombre de esta persona, es de la dirección que te ha dado y es lo bastante reciente?

Constaia tiene los tipos us_utility_bill (electricidad, gas, agua, internet o teléfono fijo) y us_bank_statement. Con expect aplica la antigüedad máxima y el titular por ti y devuelve un verdict; la comparación de la dirección con tu formulario la haces en tu código.

El flujo

El cliente rellena el formulario y sube una factura

Tu formulario recoge el nombre y la dirección; la factura llega a tu backend junto con ellos. La clave de API nunca sale de tu servidor.

Tu backend pide a Constaia que la verifique

Llama a POST /v1/analyze con expect: ["us_utility_bill", "us_bank_statement"], checks.max_age_days y checks.holder con el nombre del formulario. Recibes los campos, las comprobaciones y un verdict.

Tu código compara la dirección y decide

Compara la dirección extraída con la del formulario. Acepta las coincidencias claras, envía los casos dudosos a una persona y pide al cliente otro documento cuando la factura es demasiado antigua.

Los campos

TipoTitularDirecciónCódigo ZIPFecha para la antigüedad
us_utility_billaccount_holderservice_addresspostal_codeissue_date
us_bank_statementaccount_holderaddresspostal_codeperiod_end

us_utility_bill trae además provider, service_type, account_number, due_date, service_period_start, service_period_end y amount_due. La lista completa de cada tipo está en el catálogo de documentos y en GET /v1/document-types.

En la factura, service_address es la dirección de suministro (service address), no la postal: una factura puede enviarse a un apartado de correos o a otra dirección.

Qué comprueba Constaia

  • Antigüedad (max_age_days): la mayoría de negocios aceptan documentos de los últimos 60 o 90 días. Con max_age_days: 90, un documento más antiguo da un motivo error y el veredicto es invalid.
  • Titular (holder): compara el titular con el nombre que mandas, sin distinguir mayúsculas ni acentos y tolerando el orden y erratas pequeñas.
  • Código ZIP: formato de 5 o 9 cifras validado con el esquema us_zip, en checks[] como id_number_format. En la factura, un ZIP con formato inválido es un fallo; en el extracto bancario solo produce un aviso.

Constaia no contacta con la compañía suministradora ni con el banco, y no compara la dirección con tu formulario: eso lo hace tu código. Más en Comprobaciones.

Verificar y comparar

La comparación de direcciones de abajo es simple a propósito: normaliza las abreviaturas habituales y compara número, calle, piso o unidad y código ZIP. En producción, valora un servicio de normalización USPS o de geocodificación en tu backend.

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

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

const ABBREVIATIONS: Record<string, string> = {
  STREET: "ST", AVENUE: "AVE", ROAD: "RD", BOULEVARD: "BLVD", DRIVE: "DR", LANE: "LN", COURT: "CT",
  PLACE: "PL", TERRACE: "TER", HIGHWAY: "HWY", APARTMENT: "APT", SUITE: "STE", NORTH: "N", SOUTH: "S",
  EAST: "E", WEST: "W", "#": "APT", UNIT: "APT",
};

function normalizeAddress(s: string): string {
  return s
    .toUpperCase()
    .replace(/[.,]/g, " ")
    .replace(/#/g, " # ")
    .split(/\s+/)
    .filter(Boolean)
    .map((w) => ABBREVIATIONS[w] ?? w)
    .join(" ");
}

type FormData = { name: string; street: string; zip: string };

export async function checkProofOfAddress(path: string, form: FormData) {
  const analysis = await constaia.analyze(await fromPath(path), {
    expect: ["us_utility_bill", "us_bank_statement"],
    checks: { maxAgeDays: MAX_AGE_DAYS, holder: { fullName: form.name } },
    storage: "none",
    keepResults: false,
  });
  if (analysis.status !== "completed" || !analysis.verdict) return { decision: "pending", issues: [analysis.id] };

  const reasons = analysis.verdict.reasons;
  const has = (code: string) => reasons.some((r) => r.code === code && r.severity === "error");
  if (has("max_age_days")) return { decision: "ask_new_document", issues: [`older_than_${MAX_AGE_DAYS}_days`] };
  if (has("type_mismatch")) return { decision: "ask_new_document", issues: ["not_a_bill_or_statement"] };

  // Titular distinto, ZIP con formato inválido, calidad…: a revisión, no a rechazo.
  const issues = reasons.filter((r) => r.severity !== "info").map((r) => `${r.code}: ${r.message}`);

  const v = (name: string) => {
    const x = analysis.fields[name]?.value;
    return typeof x === "string" ? x.trim() : "";
  };
  const street = analysis.document?.type === "us_bank_statement" ? v("address") : v("service_address");
  if (!normalizeAddress(street).startsWith(normalizeAddress(form.street))) issues.push("street_differs");
  if (v("postal_code").slice(0, 5) !== form.zip.slice(0, 5)) issues.push("zip_differs");

  return { decision: issues.length ? "manual_review" : "accept", issues, type: analysis.document?.type };
}

checkProofOfAddress(process.argv[2] ?? "./bill.pdf", { name: "Jane Doe", street: "123 Main Street Apt 4B", zip: "94107" })
  .then((r) => console.log(r))
  .catch((err) => {
    if (err instanceof ConstaiaError) console.error(`Constaia error ${err.status} ${err.code} (request ${err.requestId})`);
    else console.error(err);
    process.exit(1);
  });

La dirección extraída suele incluir la ciudad y el estado detrás de la calle (por ejemplo 2570 24TH STREET, SACRAMENTO, CA), por eso el ejemplo comprueba que empiece por la calle del formulario.

Una diferencia de nombre suele ser legítima (el titular es el cónyuge o un compañero de piso, falta un segundo nombre). Por eso el ejemplo envía los motivos holder a revisión humana en lugar de rechazarlos; deja que tu política diga qué alternativas aceptas.

Avisos como edited_suspected o screen_photo_suspected son señales para que una persona vuelva a mirar, no prueba de fraude. Si también aceptas facturas de fuera de EE. UU., añade a expect el tipo genérico utility_bill.

Modo test

Con una clave ck_test_ el simulador no tiene escenarios específicos de EE. UU.: una factura estadounidense se responde con el escenario generic, así que el veredicto es review con type_unknown y el código de arriba responde manual_review. 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.

Si solo quieres unos campos

Si no necesitas veredicto, puedes pasar tu propio JSON Schema en extract sin expect (por ejemplo solo proveedor, titular, dirección y fecha). En ese caso verdict es null y max_age_days y holder no se aplican: la antigüedad y el nombre los compruebas en tu código.

Cada documento cuesta 1 crédito (hasta 2 páginas). Con storage: "none" y keep_results: false Constaia no conserva nada. 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