API Reference

API de Numy

API REST para automatizar trámites fiscales mexicanos de forma programática: facturar una compra desde el portal del comercio, emitir tus propios CFDI, validar pagos SPEI contra el CEP de Banxico, y descargar del SAT tu Constancia de Situación Fiscal, tu Opinión de Cumplimiento (32-D) y tu Buzón Tributario. La mayoría de las operaciones son asíncronas: respondemos con un job_id, consultas el estado por polling y/o recibes el resultado por webhook.

dns

Base URL

https://api.numy.mx

Autenticación

Todas las rutas bajo /api/v1/ requieren una API Key con formato numy_sk_.... Se puede enviar de dos formas:

Método Ejemplo
Header X-API-Key X-API-Key: numy_sk_abc123...
Bearer token Authorization: Bearer numy_sk_abc123...

Las rutas de gestión de webhooks (/api/me/webhook-endpoints) usan autenticación Sanctum (cookie de sesión o Bearer token de Sanctum).

Límites de tasa: 60 solicitudes/minuto por API key en general y 10/min en los endpoints respaldados por robot con e.firma (/constancias, /opinion-cumplimiento, /buzon-tributario, /cfdi-descargas). Al exceder: 429 {"error": "...", "code": "rate_limited"}.

Flujo General

El flujo de facturación es asincrónico. La API acepta tu solicitud y la procesa en segundo plano:

1
POST/api/v1/invoices

Enviar ticket + datos fiscales → 202 Accepted

2
GET/api/v1/invoices/{id}

(Opcional) Polling del estado del job

3
WHinvoice.completed / invoice.failed

Notificación automática a tu webhook con el resultado

Endpoints

1. Crear solicitud de factura

POST/api/v1/invoices

Solicita la factura (CFDI) de una compra a partir de la foto de un ticket: Numy obtiene el comprobante desde el portal de facturación del comercio por ti — aquí tú eres el comprador. Es asíncrono: respondemos con un job_id y el resultado llega por webhook o polling. ¿Eres tú quien vende y necesitas emitir el CFDI a tu cliente? Ese es otro flujo → Emitir CFDI propio.

Headers

X-API-Key: numy_sk_xxxxx
Content-Type: application/json

Body

{
  "ticket_image_url": "https://example.com/ticket.jpg",
  "fiscal_data": {
    "rfc": "XAXX010101000",
    "person_type": "fisica",
    "first_name": "Juan",
    "last_name": "Pérez",
    "mothers_last_name": "López",
    "tax_regime": "626",
    "cfdi_usage": "G03",
    "tax_zip_code": "06600",
    "email": "juan@example.com"
  },
  "metadata": {
    "external_id": "order-123",
    "client_ref": "abc"
  }
}

Campos del body

Campo Tipo Requerido Descripción
ticket_image_url string URL pública de la imagen del ticket (JPEG, PNG, WebP). Máx 10 MB.
fiscal_data object Datos fiscales del receptor
fiscal_data.rfc string RFC del receptor (13 chars persona física, 12 chars persona moral)
fiscal_data.person_type string "fisica" o "moral"
fiscal_data.first_name string Física Nombre(s) — requerido para persona física
fiscal_data.last_name string Física Apellido paterno — requerido para persona física
fiscal_data.mothers_last_name string No Apellido materno (opcional)
fiscal_data.legal_name string Moral Razón social — requerido para persona moral
fiscal_data.tax_regime string Clave del régimen fiscal (ver catálogos)
fiscal_data.cfdi_usage string Clave del uso de CFDI (ver catálogos)
fiscal_data.tax_zip_code string Código postal fiscal (5 dígitos)
fiscal_data.email string Email donde se envía la factura
metadata object No Datos libres para correlación (máx 10 keys, valores string, máx 500 chars c/u)

Respuesta exitosa — 202 Accepted

{
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Solicitud recibida. Recibirás el resultado en tu webhook.",
  "estimated_time_seconds": 120,
  "created_at": "2026-03-28T12:00:00.000000Z",
  "metadata": { "external_id": "order-123" }
}

Errores posibles

HTTP Código Descripción
401 missing_api_key No se envió API Key
401 invalid_api_key API Key inválida o revocada
402 insufficient_tickets No hay tickets disponibles (incluye tickets_remaining y upgrade_url)
422 validation_error Errores de validación (incluye details con campos específicos)

Ejemplo — Error 422:

{
  "error": "Validation failed",
  "code": "validation_error",
  "details": {
    "fiscal_data.rfc": ["El RFC no tiene un formato válido."],
    "fiscal_data.tax_regime": ["El régimen fiscal no corresponde al tipo de persona."]
  }
}

Ejemplo — Error 402:

{
  "error": "No hay tickets disponibles en tu suscripción.",
  "code": "insufficient_tickets",
  "tickets_remaining": 0,
  "upgrade_url": "https://console.numy.mx/planes"
}

2. Consultar estado de solicitud

GET/api/v1/invoices/{job_id}

Consulta el estado de una solicitud de factura usando el job_id que recibiste al crearla (polling). Útil como alternativa o respaldo del webhook.

Headers

X-API-Key: numy_sk_xxxxx

Respuestas según estado

queued — En cola:

{
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Tu solicitud está en cola. Recibirás el resultado en tu webhook.",
  "created_at": "2026-03-28T12:00:00.000000Z",
  "completed_at": null,
  "metadata": { "external_id": "order-123" },
  "estimated_time_seconds": 120
}

processing — En proceso:

{
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "processing",
  "message": "Tu factura está siendo procesada.",
  "created_at": "2026-03-28T12:00:00.000000Z",
  "completed_at": null,
  "metadata": { "external_id": "order-123" },
  "estimated_time_seconds": 120
}

success — Completada:

{
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "created_at": "2026-03-28T12:00:00.000000Z",
  "completed_at": "2026-03-28T12:02:15.000000Z",
  "metadata": { "external_id": "order-123" },
  "estimated_time_seconds": 120,
  "result": {
    "comercio": "Oxxo",
    "folio": "ABC123",
    "total": "152.50",
    "files": {
      "pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx",
      "xml_url": "https://api.numy.mx/api/dl/tok_xxxxx"
    },
    "download_page_url": "https://api.numy.mx/api/factura/tok_xxxxx"
  }
}

failed — Error:

{
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "failed",
  "created_at": "2026-03-28T12:00:00.000000Z",
  "completed_at": "2026-03-28T12:00:05.000000Z",
  "metadata": { "external_id": "order-123" },
  "estimated_time_seconds": 120,
  "error": {
    "code": "vision_failed",
    "message": "No se pudo leer el ticket. La imagen puede estar borrosa o dañada."
  }
}

3. Validar pago SPEI (CEP de Banxico)

POST/api/v1/payment-validations

Valida de forma asíncrona que un pago SPEI se haya liquidado, consultando el CEP del Banco de México. Entrada por datos estructurados (sin imagen). El resultado llega por webhook payment.completed / payment.failed o por polling.

Headers

X-API-Key: numy_sk_xxxxx
Content-Type: application/json

Body

{
  "tracking_key": "ABCD1234567890",
  "operation_date": "2026-05-20",
  "amount": "1500.00",
  "sender_bank": "BBVA",
  "receiver_bank": "STP",
  "beneficiary_account": "012345678901234567",
  "is_bank_payment": false,
  "metadata": { "external_id": "pago-001" }
}

Campos del body

Campo Tipo Requerido Descripción
tracking_key string Sí* Clave de rastreo (7–30 caracteres). *Requerido si no envías reference_number.
reference_number string Sí* Número de referencia (≤7 dígitos). Alternativa a la clave de rastreo.
operation_date string Fecha del pago YYYY-MM-DD. Banxico solo conserva ~45 días hábiles.
amount number Monto exacto del pago.
receiver_bank string Banco receptor: nombre o clave SPEI de 5 dígitos.
sender_bank string No Banco emisor: nombre o clave SPEI.
beneficiary_account string CLABE (18), tarjeta (16) o celular (10 dígitos).
is_bank_payment boolean No Marca "Pago a banco" en el CEP. Default false.
metadata object No Datos libres para correlación.

Respuesta — 202 Accepted

{
  "job_id": "pay_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Solicitud recibida. Recibirás el resultado en tu webhook.",
  "estimated_time_seconds": 180,
  "created_at": "2026-05-26T12:00:00.000000Z",
  "metadata": { "external_id": "pago-001" }
}

Consulta el estado con GET /api/v1/requests/{job_id}. El resultado (cep_status: liquidado, en_proceso, no_encontrado…) y los archivos del CEP llegan en el webhook payment.completed.

Si operation_date excede la ventana de ~45 días hábiles que Banxico conserva el CEP, la solicitud se rechaza con 422 y código out_of_windowsin consumir ticket.

4. Descargar Constancia de Situación Fiscal

POST/api/v1/constancias

Descarga de forma asíncrona la Constancia de Situación Fiscal desde el portal del SAT usando la e.firma. El PDF llega por webhook constancia.completed / constancia.failed o por polling.

Seguridad: la e.firma (.cer, .key y contraseña) se envía por multipart/form-data, se usa una sola vez y no se almacena en Numy. Envíala siempre sobre HTTPS.

Body — multipart/form-data

Campo Tipo Requerido Descripción
cer file Archivo .cer de la e.firma.
key file Archivo .key de la clave privada.
password string Contraseña de la clave privada.
rfc string RFC del contribuyente.
metadata object No Datos libres para correlación (se devuelve en polling y webhook).

Respuesta — 202 Accepted

{
  "job_id": "con_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Solicitud recibida. Recibirás la constancia en tu webhook.",
  "estimated_time_seconds": 300,
  "created_at": "2026-05-26T12:00:00.000000Z",
  "metadata": null
}

El webhook constancia.completed incluye el resultado:

{
  "event": "constancia.completed",
  "job_id": "con_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "result": {
    "rfc": "VAGL960809SQ0",
    "constancia_pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx"
  },
  "completed_at": "2026-05-26T12:05:00.000000Z"
}

5. Descargar Opinión de Cumplimiento (forma 32-D)

POST/api/v1/opinion-cumplimiento

Descarga de forma asíncrona la Opinión de Cumplimiento de Obligaciones Fiscales (forma 32-D) del portal SAT con la e.firma del contribuyente. El PDF oficial llega por webhook opinion_cumplimiento.completed / opinion_cumplimiento.failed o por polling. El resultado incluye el dictamen extraído (Positiva, Negativa, Inscrito sin obligaciones, Suspensión de actividades — Regla 2.1.36 RMF 2026).

Seguridad: la e.firma (.cer, .key y contraseña) se envía por multipart/form-data, se usa una sola vez y no se almacena en Numy. Envíala siempre sobre HTTPS.

Body — multipart/form-data

Campo Tipo Requerido Descripción
cer file Archivo .cer de la e.firma.
key file Archivo .key de la clave privada.
password string Contraseña de la clave privada.
rfc string RFC del contribuyente. PF (13 chars) y PM (12 chars) abren URLs distintas del portal.

Respuesta — 202 Accepted

{
  "job_id": "opn_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Solicitud recibida. Recibirás la Opinión de Cumplimiento en tu webhook.",
  "estimated_time_seconds": 300,
  "created_at": "2026-05-27T12:00:00.000000Z",
  "metadata": null
}

El webhook opinion_cumplimiento.completed incluye el resultado:

{
  "event": "opinion_cumplimiento.completed",
  "job_id": "opn_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "result": {
    "rfc": "VAGL960809SQ0",
    "dictamen": "Positiva",
    "opinion_pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx"
  },
  "completed_at": "2026-05-27T12:05:00.000000Z"
}

Estados posibles del dictamen (Regla 2.1.36 RMF 2026):

  • "Positiva" — al corriente.
  • "Negativa" — con incumplimientos.
  • "Inscrito sin obligaciones fiscales" — RFC sin obligaciones periódicas.
  • "Suspensión de actividades" — RFC suspendido.
  • null — el agente bajó el PDF pero no se pudo extraer el dictamen (el PDF está intacto, solo es metadata adicional).

Vigencia del documento: 30 días naturales en general, 90 días para subsidios y estímulos fiscales.

6. Consultar Buzón Tributario (modo solo lectura)

POST/api/v1/buzon-tributario

Consulta de forma asíncrona el Buzón Tributario del SAT del contribuyente. Devuelve los listados de las bandejas (Pendientes, Notificados, Comunicados, Documentos) más PDFs descargados de comunicados y documentos. El resultado llega por webhook buzon_tributario.completed / buzon_tributario.failed.

Salvaguarda legal — solo lectura: este endpoint NUNCA abre notificaciones en la bandeja "Pendientes". Abrirlas dispara plazos legales contra el contribuyente bajo CFF arts. 17-K y 134-I (jurisprudencia SCJN Comunicado 013/2024). Numy enumera los pendientes para que tú decidas cuándo abrirlos desde el portal SAT con tu e.firma.

Body — multipart/form-data

Mismos campos que /api/v1/opinion-cumplimiento: cer, key, password, rfc, metadata (opcional).

Respuesta — 202 Accepted

{
  "job_id": "bzn_aBcDeFgHiJkLmNoPqRsT",
  "status": "queued",
  "message": "Solicitud recibida. El listado de tu Buzón llegará a tu webhook (modo solo lectura, sin abrir notificaciones).",
  "estimated_time_seconds": 360,
  "created_at": "2026-05-27T12:00:00.000000Z",
  "metadata": null
}

El webhook buzon_tributario.completed incluye los listados completos:

{
  "event": "buzon_tributario.completed",
  "job_id": "bzn_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "result": {
    "rfc": "VAGL960809SQ0",
    "buzon_habilitado": true,
    "resumen": {
      "pendientes": 2,
      "comunicados": 3,
      "documentos": 5
    },
    "notificaciones_pendientes": [
      { "folio": "N001", "fecha_aviso": "2026-05-25", "asunto": "Requerimiento", "remitente": "SAT" }
    ],
    "comunicados": [
      { "folio": "C001", "fecha": "2026-05-20", "asunto": "Recordatorio", "pdf_path": "..." }
    ],
    "documentos": [
      { "folio": "D001", "fecha": "2026-05-15", "tipo": "Acuse", "pdf_path": "..." }
    ],
    "buzon_url": "https://api.numy.mx/api/buzon/abc123token",
    "expires_at": "2026-06-03T12:00:00+00:00"
  },
  "completed_at": "2026-05-27T12:06:00.000000Z"
}

Notas clave:

  • buzon_habilitado: false cuando el SAT reporta que el Buzón no está activado (caso típico: PF RESICO antes del 1-ene-2027). En ese caso los listados llegan vacíos.
  • buzon_url sirve a un endpoint público sin auth (token URL-safe, expira 7 días) con los PDFs descargados.
  • Si el agente IA intenta abrir una notificación pendiente, el job falla con código intent_blocked_open_pendiente (no reintentable).

Disclaimer legal: las notificaciones del Buzón tienen efectos jurídicos bajo CFF arts. 17-K y 134-I. Tienes 3 días hábiles desde el aviso para abrirlas; al cuarto día se tienen por notificadas. Numy NO las abre por ti — solo te informa que existen.

7. Emitir CFDI

POST/api/v1/cfdi

Emite un CFDI de Ingreso (tipo I) como emisor/vendedor, de forma síncrona: el comprobante timbrado se devuelve en la misma respuesta (no usa webhook). ¿Solo quieres la factura de algo que compraste? Ese es otro flujo → Facturar una compra.

Modo real vs prueba: la factura sale en real (livemode: true) cuando tu RFC emisor tiene su configuración completa (sellos CSD activados + carta manifiesto firmada). Si falta algo, el endpoint responde 422 not_live_ready antes de cobrar, con el faltante en gap y la liga al panel para completarlo. En entorno de prueba se timbra en sandbox (test_mode: true, sin validez fiscal).

Body

{
  "cliente_rfc": "XAXX010101000",
  "cliente_nombre": "JOHN DOE",
  "cliente_cp": "83240",
  "cliente_regimen_codigo": "616",
  "cliente_uso_codigo": "S01",
  "cliente_email": "cliente@ejemplo.com",
  "forma_pago_codigo": "08",
  "metodo_pago_codigo": "PUE",
  "conceptos": [
    {
      "descripcion": "Servicio de consultoría",
      "cantidad": 1,
      "valor_unitario": 1000,
      "precio_con_iva": false,
      "tipo_iva": "16"
    }
  ],
  "metadata": { "external_id": "cfdi-001" }
}

Campos del body

Campo Tipo Requerido Descripción
cliente_rfc string RFC del receptor.
cliente_nombre string Nombre o razón social del receptor.
cliente_cp string Código postal fiscal (5 dígitos).
cliente_regimen_codigo string Régimen fiscal del receptor (ver catálogos).
cliente_uso_codigo string Uso de CFDI (ver catálogos).
cliente_email string No Email del receptor.
forma_pago_codigo string No Forma de pago SAT (default 01).
metodo_pago_codigo string No PUE o PPD (default PUE).
currency string No Moneda ISO 4217 de 3 letras (default MXN).
conceptos[] array Al menos 1 concepto.
conceptos[].descripcion string Descripción del concepto.
conceptos[].valor_unitario number Precio unitario (mayor a 0).
conceptos[].precio_con_iva boolean true si el precio ya incluye IVA, false si es neto.
conceptos[].cantidad number No Default 1.
conceptos[].clave_prodserv string No Clave de producto/servicio SAT (default genérica).
conceptos[].clave_unidad string No Clave de unidad SAT (default E48).
conceptos[].tipo_iva string No 16, 8, 0 o exento (default 16).

Respuesta — 200 OK

{
  "uuid": "bb56f134-8427-4664-8d0f-7e3d8f83fea2",
  "folio": "1",
  "series": "F",
  "total": 1160.00,
  "currency": "MXN",
  "verification_url": "https://verificacfdi.facturaelectronica.sat.gob.mx/...",
  "livemode": false,
  "test_mode": true,
  "duplicate": false,
  "downloads": [
    { "type": "pdf", "url": "https://api.numy.mx/api/dl/tok_xxxxx" },
    { "type": "xml", "url": "https://api.numy.mx/api/dl/tok_xxxxx" }
  ],
  "metadata": { "external_id": "cfdi-001" }
}

Errores posibles

HTTP Código Descripción
422 validation_error / invalid_payload Datos faltantes o inválidos. No consume ticket.
422 tax_ambiguous Falta definir precio_con_iva en algún concepto.
422 no_titular La cuenta no tiene un perfil de WhatsApp titular configurado (no hay emisor). No consume ticket.
402 insufficient_tickets No hay tickets disponibles.
422 (código del PAC) Rechazo de FacturAPI posterior al envío (p.ej. RFC del receptor inexistente); el code llega tal cual del PAC. Sí consume ticket.
502 facturapi_error Error del proveedor de timbrado.
500 unexpected Error interno inesperado.

Cobro: 1 emisión = 1 ticket. Un payload inválido (422) no consume; una emisión que llega a FacturAPI consume aunque sea rechazada; un duplicado idempotente reusa el resultado previo sin volver a cobrar.

7.1 Ciclo del CFDI emitido: cancelar, nota de crédito, complemento y consulta

Todos estos endpoints aceptan {cfdi} como el cfdi_id que devuelve la emisión o el UUID fiscal, siempre acotado a tu cuenta.

POST/api/v1/cfdi/{cfdi}/cancel

Cancela un CFDI timbrado con los motivos SAT 0104; el motivo 01 exige substitution_uuid (una factura timbrada tuya que sustituye a la cancelada). No consume operaciones. La respuesta trae el cancellation_status inicial (accepted / pending — el receptor tiene hasta 72 h — / verifying); la resolución final llega por el webhook cfdi.cancellation.updated.

{
  "cfdi_id": 123,
  "uuid": "bb56f134-...",
  "folio": "15",
  "cancellation_status": "pending",
  "message": "Solicitud enviada. El receptor tiene hasta 72 horas para aceptar o rechazar."
}
POST/api/v1/cfdi/{cfdi}/nota-credito

Nota de crédito (Egreso, tipo E) contra un Ingreso timbrado. Body según mode: {"mode": "total"} (espejo del Ingreso), {"mode": "partial", "items": [{"index": 1, "cantidad": 1}]} (subset por índice, con cantidad nunca mayor a la original) o {"mode": "descuento", "monto": 500} (monto fijo como concepto plano). Opcional uso_cfdi (default G02). Consume 1 operación; el tope de sobre-acreditación responde 422 over_credited.

POST/api/v1/cfdi/{cfdi}/complemento-pago

Complemento de pago (REP, tipo P) contra una factura PPD con saldo. Body: forma_pago_codigo (forma SAT concreta; 99 no se permite), monto (≤ saldo restante) y fecha_pago opcional. La parcialidad se calcula sola. La respuesta trae last_balance_after y paid: true cuando liquida. Consume 1 operación.

GET/api/v1/cfdi · /api/v1/cfdi/{cfdi} · /{cfdi}/pdf · /{cfdi}/xml

Consulta de CFDI emitidos — listado paginado con filtros (type, status, cancellation_status, uuid, folio, fechas), detalle con derivados (complementos y notas de crédito) y descarga del PDF/XML timbrado. Sin costo.

Reintentos: manda idempotency_key. La nota de crédito y el complemento de pago son síncronos y hablan con el PAC, así que una respuesta perdida por timeout invita a reintentar. Con tu llave (única por operación real, máx 64 caracteres) el reintento devuelve el CFDI original con duplicate: true sin volver a cobrar ni timbrar. Mientras una operación corre sobre la misma factura, otra concurrente recibe 409 invoice_busy.

Cobro: nota de crédito y complemento de pago consumen 1 operación cada uno (mismas reglas que emitir: el 422 de validación no cobra y el duplicado idempotente reusa sin cobrar). Cancelar y consultar son gratis.

8. Descargar facturas del SAT (emitidas y recibidas)

POST/api/v1/cfdi-descargas

Descarga masiva, directo del SAT, de los CFDI que tu RFC emitió o recibió en un periodo, de forma asíncrona. Requiere una e.firma validada almacenada en la bóveda del RFC (se sube una sola vez en el panel; el proceso dura de minutos a horas y no acepta e.firma inline). El resultado llega por webhook cfdi_descarga.completed / sin_resultados / failed y por polling en /api/v1/requests/{job_id}.

Body

direction (requerido, emitidas|recibidas), year (requerido, 2014..año en curso), month (opcional, 1–12, no futuro), rfc (opcional — elige entre los RFC de tu cuenta; default: el del titular) y metadata (opcional).

Cobro por éxito: cada periodo completado consume 1 operación; sin resultados y los fallos no cobran. Un año de recibidas puede partirse en hasta 12 solicitudes mensuales (una operación por mes completado). Para arrancar se exige plan activo con saldo.

Respuesta — 202 Accepted

{
  "message": "Solicitud de descarga encolada. El SAT suele tardar de minutos a unas horas; el resultado llegará a tu webhook.",
  "downloads": [
    { "job_id": "dsc_aBcDeFgHiJkLmNoPqRsT", "direction": "emitidas", "year": 2025,
      "month": null, "status": "queued", "estimated_time_seconds": 3600 }
  ],
  "skipped": [],
  "metadata": null
}

El webhook cfdi_descarga.completed llega un periodo a la vez:

{
  "event": "cfdi_descarga.completed",
  "job_id": "dsc_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "result": {
    "rfc": "VAGC981118D95",
    "direction": "emitidas",
    "year": 2025,
    "month": null,
    "period_label": "2025",
    "total_cfdi_reported": 29,
    "stored_count": 29,
    "duplicate_count": 0,
    "charged": true
  },
  "metadata": null,
  "completed_at": "2026-08-22T12:00:00.000000Z"
}

Notas clave:

  • skipped[] lista periodos omitidos con reason: conflict (ya hay una descarga activa del periodo), blocked (el periodo se logró hace <24 h o se pidió 3 veces hoy) o sin_saldo.
  • Si ningún periodo se encoló, el código HTTP refleja el motivo: 409, 429 o 402.
  • Errores 422: no_efirma_validada, rfc_no_encontrado, periodo_futuro.

9. Consultar los CFDI descargados

GET/api/v1/sat-cfdis · /{uuid}/xml · /export

La cartera de CFDI descargados pertenece al RFC (sobrevive a bajas de números y de la e.firma). Solo lectura, sin costo. El listado (?direction=emitidas&year=2025[&month=3][&rfc=...]) pagina hasta 200 por página e incluye por CFDI el UUID, tipo, serie/folio, fechas, emisor/receptor, importes y el estado_sat (Vigente / Cancelado, sincronizado contra el SAT), más total_amount y el conteo by_month del año. /{uuid}/xml descarga el XML original y /export genera el reporte Excel (.xlsx, 5 pestañas — mismo layout que el panel) del periodo.

Webhooks

Configuración

Los webhooks se gestionan bajo /api/me/webhook-endpoints con autenticación Sanctum.

Crear webhook endpoint

POST/api/me/webhook-endpoints
{
  "url": "https://tu-servidor.com/webhook",
  "events": ["invoice.completed", "invoice.failed"],
  "description": "Producción"
}
Campo Tipo Requerido Descripción
url string URL HTTPS donde recibir los webhooks
events string[] No Filtrar por eventos. null = todos los eventos
description string No Etiqueta descriptiva

Eventos disponibles: invoice.completed, invoice.failed, payment.completed, payment.failed, constancia.completed, constancia.failed, opinion_cumplimiento.completed, opinion_cumplimiento.failed, buzon_tributario.completed, buzon_tributario.failed, cfdi_descarga.completed, cfdi_descarga.sin_resultados, cfdi_descarga.failed, cfdi.cancellation.updated, webhook.test

Respuesta 201 Created:

{
  "webhook_endpoint": {
    "id": 1,
    "url": "https://tu-servidor.com/webhook",
    "events": ["invoice.completed", "invoice.failed"],
    "is_active": true,
    "description": "Producción",
    "last_triggered_at": null,
    "created_at": "2026-03-28T10:00:00.000000Z"
  },
  "secret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ...",
  "warning": "Guarda este secret de forma segura. No podrás verlo de nuevo."
}

Importante: El secret solo se muestra una vez al crear el endpoint. Guárdalo de forma segura.

Listar webhook endpoints

GET/api/me/webhook-endpoints
{
  "data": [
    {
      "id": 1,
      "url": "https://tu-servidor.com/webhook",
      "events": ["invoice.completed", "invoice.failed"],
      "is_active": true,
      "description": "Producción",
      "last_triggered_at": "2026-03-28T12:05:00.000000Z",
      "created_at": "2026-03-28T10:00:00.000000Z"
    }
  ]
}

Actualizar webhook endpoint

PATCH/api/me/webhook-endpoints/{id}
{
  "url": "https://nuevo-servidor.com/webhook",
  "events": ["invoice.completed"],
  "is_active": false,
  "description": "Staging"
}

Eliminar webhook endpoint

DELETE/api/me/webhook-endpoints/{id}

Respuesta: 204 No Content

Rotar secret

POST/api/me/webhook-endpoints/{id}/rotate-secret
{
  "webhook_endpoint": { ... },
  "secret": "whsec_NuEvOsEcReTaQuI...",
  "warning": "Guarda este nuevo secret de forma segura. No podrás verlo de nuevo."
}

Enviar webhook de prueba

POST/api/me/webhook-endpoints/{id}/test

Respuesta exitosa:

{ "success": true, "message": "Webhook de prueba enviado correctamente." }

Respuesta fallida (502):

{ "success": false, "message": "No se pudo entregar el webhook de prueba. Verifica la URL." }

Payloads de webhook

Numy envía un POST con body JSON a la URL configurada.

Headers del webhook

Header Descripción
Content-Type application/json
X-Numy-Signature sha256=<hmac_hex> — firma HMAC-SHA256
X-Numy-Timestamp Unix timestamp del envío
User-Agent Numy-Webhook/1.0

Evento: invoice.completed

{
  "event": "invoice.completed",
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "success",
  "result": {
    "comercio": "Oxxo",
    "folio": "ABC123",
    "total": "152.50",
    "files": {
      "pdf_url": "https://api.numy.mx/api/dl/tok_xxxxx",
      "xml_url": "https://api.numy.mx/api/dl/tok_xxxxx"
    },
    "download_page_url": "https://api.numy.mx/api/factura/tok_xxxxx"
  },
  "metadata": { "external_id": "order-123" },
  "completed_at": "2026-03-28T12:02:15.000000Z"
}

Evento: invoice.failed

{
  "event": "invoice.failed",
  "job_id": "inv_aBcDeFgHiJkLmNoPqRsT",
  "status": "failed",
  "error": {
    "code": "vision_failed",
    "message": "No se pudo leer el ticket. La imagen puede estar borrosa o dañada.",
    "retryable": false
  },
  "metadata": { "external_id": "order-123" },
  "failed_at": "2026-03-28T12:00:05.000000Z"
}

Evento: webhook.test

{
  "event": "webhook.test",
  "message": "Este es un webhook de prueba desde Numy.",
  "timestamp": "2026-03-28T12:00:00.000000Z"
}

Códigos de error en eventos de webhook

Código Retryable Descripción
image_download_failed No No se pudo descargar la imagen (URL inalcanzable, respuesta no-2xx o imagen vacía)
image_too_large No La imagen descargada excede 10 MB
invalid_image_type No Formato de imagen no soportado (usar JPEG, PNG, WebP)
vision_failed No No se pudo analizar el ticket con el modelo de visión (imagen borrosa o dañada)
processing_failed El ticket no pudo prepararse para la automatización (sin URL de facturación, confianza baja, folio inválido)
human_review El ticket se escaló a revisión humana tras la automatización del portal

retryable: true indica que reintentar (p.ej. con una imagen más clara) podría funcionar.

Verificación de firma

  1. Obtén el timestamp del header X-Numy-Timestamp
  2. Obtén la firma del header X-Numy-Signature (quitar el prefijo sha256=)
  3. Computa el HMAC: HMAC-SHA256(timestamp + "." + body, SHA256(secret))
    • El secret es el plain text whsec_... recibido al crear el endpoint
    • Primero hashea el secret con SHA-256, luego úsalo como clave del HMAC
  4. Compara tu HMAC con la firma recibida

Ejemplo en Node.js

const crypto = require('crypto');

function verifyWebhook(body, signature, timestamp, secret) {
  const signingKey = crypto.createHash('sha256').update(secret).digest('hex');
  const expected = crypto
    .createHmac('sha256', signingKey)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  const received = signature.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}

Ejemplo en PHP

function verifyWebhook(string $body, string $signature, string $timestamp, string $secret): bool
{
    $signingKey = hash('sha256', $secret);
    $expected   = hash_hmac('sha256', $timestamp . '.' . $body, $signingKey);
    $received   = str_replace('sha256=', '', $signature);

    return hash_equals($expected, $received);
}

Tip de seguridad: Valida que el timestamp no tenga más de 5 minutos de diferencia para prevenir ataques de replay.

Reintentos y desactivación

  • Numy intenta entregar cada webhook hasta 4 veces (1 intento + 3 reintentos).
  • Intervalos: 5s, 30s, 120s entre reintentos.
  • Si todos los intentos fallan, el endpoint se desactiva automáticamente (is_active: false).
  • El endpoint se puede reactivar manualmente via PATCH.

Tu servidor debe responder con un código 2xx dentro de 10 segundos para confirmar la recepción.

Catálogos Fiscales

Estos catálogos son los que valida POST /api/v1/invoices. Para POST /api/v1/cfdi las claves del receptor (cliente_regimen_codigo, cliente_uso_codigo) se validan directamente contra el catálogo del SAT al timbrar — cualquier clave vigente de c_RegimenFiscal / c_UsoCFDI es aceptada.

Regímenes fiscales — Persona Física

Clave Descripción
605 Sueldos y Salarios e Ingresos Asimilados a Salarios
606 Arrendamiento
607 Régimen de Enajenación o Adquisición de Bienes
611 Ingresos por Dividendos (socios y accionistas)
612 Personas Físicas con Actividades Empresariales y Profesionales
614 Ingresos por intereses
615 Régimen de los ingresos por obtención de premios
621 Incorporación Fiscal
625 Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas
626 Régimen Simplificado de Confianza

Regímenes fiscales — Persona Moral

Clave Descripción
601 General de Ley Personas Morales
603 Personas Morales con Fines no Lucrativos
609 Consolidación
620 Sociedades Cooperativas de Producción
622 Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras
623 Opcional para Grupos de Sociedades
624 Coordinados
626 Régimen Simplificado de Confianza

Uso de CFDI — Ambos tipos de persona

Clave Descripción
G01 Adquisición de mercancías
G02 Devoluciones, descuentos o bonificaciones
G03 Gastos en general
I01 Construcciones
I02 Mobiliario y equipo de oficina por inversiones
I03 Equipo de transporte
I04 Equipo de cómputo y accesorios
I05 Dados, troqueles, moldes, matrices y herramental
I06 Comunicaciones telefónicas
I07 Comunicaciones satelitales
I08 Otra maquinaria y equipo
S01 Sin efectos fiscales
CP01 Pagos

Uso de CFDI — Solo Persona Física (adicionales)

Clave Descripción
D01 Honorarios médicos, dentales y gastos hospitalarios
D02 Gastos médicos por incapacidad o discapacidad
D03 Gastos funerales
D04 Donativos
D05 Intereses reales efectivamente pagados por créditos hipotecarios
D06 Aportaciones voluntarias al SAR
D07 Primas por seguros de gastos médicos
D08 Gastos de transportación escolar obligatoria
D09 Depósitos en cuentas para el ahorro, primas de pensiones
D10 Pagos por servicios educativos (colegiaturas)
CN01 Nómina

Resumen de rutas

API Key auth (X-API-Key / Bearer numy_sk_...)

Método Ruta Descripción
POST /api/v1/invoices Facturar una compra: obtener el CFDI desde el portal del comercio (async)
GET /api/v1/invoices/{job_id} Consultar estado de solicitud
POST /api/v1/payment-validations Validar pago SPEI contra el CEP de Banxico (async)
POST /api/v1/constancias Descargar Constancia de Situación Fiscal (async, e.firma)
POST /api/v1/opinion-cumplimiento Descargar Opinión de Cumplimiento 32-D (async, e.firma)
POST /api/v1/buzon-tributario Consultar Buzón Tributario en modo solo lectura (async, e.firma)
POST /api/v1/cfdi Emitir CFDI propio como emisor/vendedor (síncrono)
GET /api/v1/cfdi[/{cfdi}] Listar / detalle de CFDI emitidos (+ /pdf y /xml)
POST /api/v1/cfdi/{cfdi}/cancel Cancelar CFDI emitido (gratis, motivos SAT 01–04)
POST /api/v1/cfdi/{cfdi}/nota-credito Nota de crédito (Egreso, tipo E)
POST /api/v1/cfdi/{cfdi}/complemento-pago Complemento de pago (REP, tipo P)
POST /api/v1/cfdi-descargas Descargar facturas del SAT (async, cobro por éxito)
GET /api/v1/sat-cfdis Listar CFDI descargados (+ /{uuid}/xml y /export)
GET /api/v1/requests/{job_id} Estado de solicitud async (pago, constancia, opinión, buzón, descarga)

Sanctum auth (panel)

Método Ruta Descripción
GET /api/me/webhook-endpoints Listar webhooks
POST /api/me/webhook-endpoints Crear webhook endpoint
PATCH /api/me/webhook-endpoints/{id} Actualizar webhook
DELETE /api/me/webhook-endpoints/{id} Eliminar webhook
POST /api/me/webhook-endpoints/{id}/rotate-secret Rotar secret
POST /api/me/webhook-endpoints/{id}/test Enviar webhook de prueba

Ejemplos cURL

Descargar facturas del SAT

curl -X POST https://api.numy.mx/api/v1/cfdi-descargas \
  -H "X-API-Key: numy_sk_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "direction": "emitidas", "year": 2025, "metadata": { "external_id": "dl-1" } }'

Cancelar un CFDI emitido

curl -X POST https://api.numy.mx/api/v1/cfdi/123/cancel \
  -H "X-API-Key: numy_sk_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "motive": "02" }'

Facturar una compra

curl -X POST https://api.numy.mx/api/v1/invoices \
  -H "X-API-Key: numy_sk_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "ticket_image_url": "https://storage.example.com/ticket-001.jpg",
    "fiscal_data": {
      "rfc": "PELL900101ABC",
      "person_type": "fisica",
      "first_name": "Juan",
      "last_name": "Pérez",
      "mothers_last_name": "López",
      "tax_regime": "626",
      "cfdi_usage": "G03",
      "tax_zip_code": "06600",
      "email": "juan@example.com"
    },
    "metadata": { "external_id": "order-456" }
  }'

Consultar estado

curl https://api.numy.mx/api/v1/invoices/inv_aBcDeFgHiJkLmNoPqRsT \
  -H "X-API-Key: numy_sk_xxxxx"

Crear webhook endpoint

curl -X POST https://api.numy.mx/api/me/webhook-endpoints \
  -H "Authorization: Bearer {sanctum_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://tu-servidor.com/numy-webhook",
    "events": ["invoice.completed", "invoice.failed"],
    "description": "Producción"
  }'