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.
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:
/api/v1/invoicesEnviar ticket + datos fiscales → 202 Accepted
/api/v1/invoices/{id}(Opcional) Polling del estado del job
invoice.completed / invoice.failedNotificación automática a tu webhook con el resultado
Endpoints
1. Crear solicitud de factura
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 | Sí | URL pública de la imagen del ticket (JPEG, PNG, WebP). Máx 10 MB. |
fiscal_data |
object | Sí | Datos fiscales del receptor |
fiscal_data.rfc |
string | Sí | RFC del receptor (13 chars persona física, 12 chars persona moral) |
fiscal_data.person_type |
string | Sí | "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 | Sí | Clave del régimen fiscal (ver catálogos) |
fiscal_data.cfdi_usage |
string | Sí | Clave del uso de CFDI (ver catálogos) |
fiscal_data.tax_zip_code |
string | Sí | Código postal fiscal (5 dígitos) |
fiscal_data.email |
string | Sí | 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
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)
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 | Sí | Fecha del pago YYYY-MM-DD. Banxico solo conserva ~45
días hábiles. |
amount |
number | Sí | Monto exacto del pago. |
receiver_bank |
string | Sí | Banco receptor: nombre o clave SPEI de 5 dígitos. |
sender_bank |
string | No | Banco emisor: nombre o clave SPEI. |
beneficiary_account |
string | Sí | 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_window — sin consumir ticket.
4. Descargar Constancia de Situación Fiscal
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 | Sí | Archivo .cer de la e.firma. |
key |
file | Sí | Archivo .key de la clave privada. |
password |
string | Sí | Contraseña de la clave privada. |
rfc |
string | Sí | 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)
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 | Sí | Archivo .cer de la e.firma. |
key |
file | Sí | Archivo .key de la clave privada. |
password |
string | Sí | Contraseña de la clave privada. |
rfc |
string | Sí | 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)
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: falsecuando 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_urlsirve 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
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 | Sí | RFC del receptor. |
cliente_nombre |
string | Sí | Nombre o razón social del receptor. |
cliente_cp |
string | Sí | Código postal fiscal (5 dígitos). |
cliente_regimen_codigo |
string | Sí | Régimen fiscal del receptor (ver catálogos). |
cliente_uso_codigo |
string | Sí | 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 | Sí | Al menos 1 concepto. |
conceptos[].descripcion |
string | Sí | Descripción del concepto. |
conceptos[].valor_unitario |
number | Sí | Precio unitario (mayor a 0). |
conceptos[].precio_con_iva |
boolean | Sí | 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.
Cancela un CFDI timbrado con los motivos SAT
01–04; 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."
}
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.
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.
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)
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 conreason:conflict(ya hay una descarga activa del periodo),blocked(el periodo se logró hace <24 h o se pidió 3 veces hoy) osin_saldo.- Si ningún periodo se encoló, el código HTTP refleja el motivo:
409,429o402. - Errores 422:
no_efirma_validada,rfc_no_encontrado,periodo_futuro.
9. Consultar los CFDI descargados
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
{
"url": "https://tu-servidor.com/webhook",
"events": ["invoice.completed", "invoice.failed"],
"description": "Producción"
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url |
string | Sí | 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
{
"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
{
"url": "https://nuevo-servidor.com/webhook",
"events": ["invoice.completed"],
"is_active": false,
"description": "Staging"
}
Eliminar webhook endpoint
Respuesta: 204 No Content
Rotar secret
{
"webhook_endpoint": { ... },
"secret": "whsec_NuEvOsEcReTaQuI...",
"warning": "Guarda este nuevo secret de forma segura. No podrás verlo de nuevo."
}
Enviar webhook de prueba
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 |
Sí | El ticket no pudo prepararse para la automatización (sin URL de facturación, confianza baja, folio inválido) |
human_review |
Sí | 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
- Obtén el timestamp del header
X-Numy-Timestamp - Obtén la firma del header
X-Numy-Signature(quitar el prefijosha256=) - Computa el HMAC:
HMAC-SHA256(timestamp + "." + body, SHA256(secret))- El
secretes el plain textwhsec_...recibido al crear el endpoint - Primero hashea el secret con SHA-256, luego úsalo como clave del HMAC
- El
- 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"
}'