Invoice API
API para crear facturas de forma programática. Soporta envío de imagen de ticket + datos fiscales, procesamiento asincrónico, polling de estado y notificaciones via webhooks.
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).
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
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
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."
}
}
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, 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 |
|---|---|---|
invalid_url |
No | URL de imagen inválida |
download_failed |
Sí | No se pudo descargar la imagen |
file_too_large |
No | La imagen excede 10 MB |
invalid_image_type |
No | Formato de imagen no soportado (usar JPEG, PNG, WebP) |
storage_failed |
Sí | Error interno al guardar la imagen |
vision_failed |
Sí | No se pudo analizar el ticket con OCR |
image_download_failed |
Sí | Error general de descarga de imagen |
processing_failed |
No | El ticket no pudo ser procesado automáticamente |
browser_use_failed |
Sí | Error durante la automatización del portal |
browser_use_timeout |
Sí | Timeout durante la automatización |
retryable: true indica que enviar la misma imagen de nuevo 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
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 |
608 |
Demás ingresos |
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 |
Crear solicitud de factura |
| GET | /api/v1/invoices/{job_id} |
Consultar estado de solicitud |
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
Crear factura
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"
}'