Todos los cuerpos son JSON en snake_case; los campos en null se
omiten de la respuesta (no aparecen como null, no aparecen). Los montos
siempre son enteros en centavos (amount_minor), nunca flotantes.
| Grupo | Auth | Header adicional |
|---|---|---|
/v1/* | Authorization: Bearer sk_test_… / sk_live_… | Idempotency-Key (UUID) en los POST que mueven dinero — ver detalle por endpoint. |
/public/* | Sin Authorization: el client_secret del intent autentica — en el cuerpo (POST) o en el header X-Winal-Client-Secret (GET; query ?client_secret= legado). | — |
GET /openapi/v1.json): genera un cliente tipado en tu lenguaje
o consúltalo desde tu IDE. Ver Genera tu cliente. Antes de integrar,
revisa Entornos y Base URL y
Autenticación y seguridad.
Paginación
Los endpoints de listado devuelven un envelope {"object": "list", "data": [...]}.
Winal usa dos estilos de paginación según el endpoint; ambos son consistentes en su familia.
Cursor por id (exclusivo) — el flujo de eventos
GET /v1/events pagina con un cursor entero exclusivo: after_id
devuelve solo filas con id > after_id. Avanzas tu cursor al id
más alto que viste y repites; nunca repite ni salta eventos. Ver
Eventos y polling.
| Parámetro | Tipo | Default · tope |
|---|---|---|
after_id | int64, opcional | 0 (desde el primer evento); cursor exclusivo |
limit | int, opcional | 50 · tope 200 (valores mayores se recortan, no fallan) |
Cursor opaco con has_more — listados de reportes
Los listados paginados de Reportes (p. ej.
GET /v1/reports/payments) devuelven además has_more y
next_cursor. Sigue pidiendo con ?cursor=<next_cursor> mientras
has_more sea true; cuando es false, terminaste y
next_cursor se omite. El cursor es opaco: pásalo tal cual, no lo construyas.
{
"object": "list",
"data": [ /* … filas … */ ],
"has_more": true,
"next_cursor": "eyJpZCI6MTI4LCJ0cyI6..."
}
| Parámetro | Tipo | Default · tope |
|---|---|---|
cursor | string opaco, opcional | el next_cursor de la página anterior; un cursor corrupto responde reports.invalid_cursor |
limit | int, opcional | varía por endpoint (típico 20, tope 100; los listados admin usan default 100, tope 500) — siempre se recorta al tope |
Los listados simples (sin cursor) devuelven todo el conjunto del tenant en
data y aceptan ?limit= para acotar; no traen has_more.
Cada endpoint indica su estilo en su fila de la Referencia.
Límites de solicitudes
El API aplica rate limiting por ventana fija de 1 minuto. Al excederlo recibes
429 con el envelope de error estándar y un header Retry-After.
| Tráfico | Se cuenta por | Límite por minuto (default) |
|---|---|---|
/v1/* autenticado | tu llave sk_… | 300 |
/public/* | IP de cliente confiable | 60 |
Exentos (no consumen cupo): /health, /metrics, /status,
/portal, /demo y /js/*. Los límites son configurables por
despliegue, así que trata los valores de arriba como el default, no como un contrato duro.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Se excedió el límite de solicitudes; reintenta más tarde."
}
}
429, respeta siempre el header Retry-After (segundos):
espera ese tiempo y reintenta con backoff. Hoy Winal no emite headers
X-RateLimit-Limit/X-RateLimit-Remaining; no cuentes con ellos —
reintenta guiándote por Retry-After. La idempotencia
(Idempotency-Key) hace que reintentar un POST que mueve dinero sea
seguro, sin doble cargo.
Payment Intents
Crea un intent en requires_payment_method con un client_secret nuevo.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
amount_minor | int64, requerido, > 0 |
currency | string ISO 4217, requerido (hoy solo MXN funciona con Sim) |
payment_method_types | string[], opcional — informativo: solo se registra en el historial, no fija el método real ni se guarda en el intent. El método que de verdad se usa es el que mandas en confirm. |
metadata | object<string,string>, opcional |
tip_minor | int64, opcional, no negativo. null/omitido = 0. Caso típico del POS: se omite aquí y se fija después en confirm (ver Reportes → Propinas). |
Errores posibles: payment_intent.invalid_amount, payment_intent.invalid_currency, payment_intent.invalid_tip (ver Errores).
POST /v1/payment_intents
Authorization: Bearer sk_test_...
Idempotency-Key: 6a1e3b2c-...
Content-Type: application/json
{ "amount_minor": 84900, "currency": "MXN" }
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 0,
"total_minor": 84900,
"currency": "MXN",
"status": "requires_payment_method",
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"livemode": false,
"created_at": "2026-07-05T18:30:00Z",
"updated_at": "2026-07-05T18:30:00Z"
}
Lee un intent. Con ?expand=attempts incluye el historial de intentos de cobro.
Authorization | requerido |
Errores posibles: payment_intent.not_found.
GET /v1/payment_intents/5b6b8b3e-...?expand=attempts
Authorization: Bearer sk_test_...
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 0,
"total_minor": 84900,
"currency": "MXN",
"status": "succeeded",
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"livemode": false,
"created_at": "2026-07-05T18:30:00Z",
"updated_at": "2026-07-05T18:30:04Z",
"attempts": [
{
"id": "a13fce02-...",
"object": "attempt",
"status": "captured",
"connector_key": "sim",
"method": "card",
"provider_ref": "sim_charge_9c1f...",
"created_at": "2026-07-05T18:30:01Z"
}
]
}
Confirma el cobro: valida ruteo, crea el attempt y encola su ejecución. La
respuesta HTTP siempre llega con status: "processing" — el cobro se
ejecuta después, fuera del request (regla: processing solo se resuelve con
evidencia del proveedor). El resultado final llega por webhook o por un GET
posterior.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
payment_token | string, requerido (tok_sim_* en pruebas) |
payment_method | string, requerido: card | spei | codi | dimo | oxxo |
tip_minor | int64, opcional, no negativo. Si se manda, reemplaza la propina que el intent ya tuviera; null/omitido deja la existente sin tocar. La respuesta trae tip_minor y total_minor (= amount_minor + tip_minor) — ver Reportes → Propinas. |
Errores posibles: payment_intent.unauthorized, payment_intent.not_confirmable, payment_intent.no_route, payment_intent.concurrent_modification, payment_intent.invalid_tip.
POST /v1/payment_intents/5b6b8b3e-.../confirm
Authorization: Bearer sk_test_...
Idempotency-Key: 9d2f1a4e-...
Content-Type: application/json
{ "payment_token": "tok_sim_ok", "payment_method": "card", "tip_minor": 5000 }
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 5000,
"total_minor": 89900,
"currency": "MXN",
"status": "processing",
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"livemode": false,
"created_at": "2026-07-05T18:30:00Z",
"updated_at": "2026-07-05T18:30:00Z",
"attempts": [
{
"id": "a13fce02-...",
"object": "attempt",
"status": "pending",
"connector_key": "sim",
"method": "card",
"created_at": "2026-07-05T18:30:00Z"
}
]
}
Captura un intento previamente autorizado sin capturar (flujo auth/capture del POS,
p. ej. tras confirmar con tok_sim_auth). Sin cuerpo. El intent no cambia de
estado en la respuesta — sigue processing hasta que el proveedor confirme
la captura.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Errores posibles: attempt.not_capturable (no hay intento authorized).
POST /v1/payment_intents/5b6b8b3e-.../capture
Authorization: Bearer sk_test_...
Idempotency-Key: 2c3e9f10-...
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 0,
"total_minor": 84900,
"currency": "MXN",
"status": "processing",
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"livemode": false,
"created_at": "2026-07-05T18:30:00Z",
"updated_at": "2026-07-05T18:31:10Z",
"attempts": [
{ "id": "a13fce02-...", "object": "attempt", "status": "authorized",
"connector_key": "sim", "method": "card",
"provider_ref": "sim_auth_7b2c...", "created_at": "2026-07-05T18:30:00Z" }
]
}
Cancela el intent (si la máquina de estados lo permite). Sin cuerpo. Cancelar un intent ya canceled es un no-op que devuelve 200.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Errores posibles: transición inválida si el intent ya está en un estado terminal distinto (succeeded/failed/expired).
POST /v1/payment_intents/5b6b8b3e-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 77aa1c3e-...
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 0,
"total_minor": 84900,
"currency": "MXN",
"status": "canceled",
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"livemode": false,
"created_at": "2026-07-05T18:30:00Z",
"updated_at": "2026-07-05T18:32:00Z"
}
Refunds
Crea una devolución en requested sobre un intento capturado.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
attempt_id | uuid, requerido — el intento a devolver (no el payment_intent). |
amount_minor | int64, opcional. Si se omite, devuelve el saldo restante (cobrado menos devoluciones vivas) — no el monto original del cargo. |
reason | string, opcional |
Errores posibles: ver la tabla de refunds en Errores.
POST /v1/refunds
Authorization: Bearer sk_test_...
Idempotency-Key: f1e2d3c4-...
Content-Type: application/json
{ "attempt_id": "a13fce02-...", "amount_minor": 84900, "reason": "devolución POS" }
{
"id": "d4c5b6a7-...",
"object": "refund",
"attempt_id": "a13fce02-...",
"amount_minor": 84900,
"currency": "MXN",
"status": "requested",
"reason": "devolución POS",
"created_at": "2026-07-05T19:00:00Z"
}
Lee una devolución por su id.
Authorization | requerido |
Errores posibles: refund.not_found.
GET /v1/refunds/d4c5b6a7-...
Authorization: Bearer sk_test_...
{
"id": "d4c5b6a7-...",
"object": "refund",
"attempt_id": "a13fce02-...",
"amount_minor": 84900,
"currency": "MXN",
"status": "succeeded",
"provider_ref": "sim_refund_1a2b...",
"reason": "devolución POS",
"created_at": "2026-07-05T19:00:00Z"
}
Clientes (card-on-file)
Guía narrativa completa (activación asíncrona del método, cobro 1-click) en Clientes.
Crea un cliente. Cuerpo vacío permitido.
Authorization | requerido |
Cuerpo
name | string, opcional |
email | string, opcional |
metadata | object<string,string>, opcional |
{
"id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
"object": "customer",
"name": "Juan Pérez",
"email": "juan.perez@example.mx",
"livemode": false,
"created_at": "2026-07-08T02:18:51.369868+00:00",
"updated_at": "2026-07-08T02:18:51.369868+00:00"
}
Lista o lee un cliente por su id.
Authorization | requerido |
Errores posibles: customer.not_found.
{ "object": "list", "data": [ { "id": "7c17884b-...", "object": "customer", "...": "..." } ] }
Guarda un método de pago tokenizado. Nace pending; el Worker lo activa fuera del request.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
payment_token | string, requerido — token de un solo uso (tok_sim_* en pruebas) |
connector | string, opcional |
Errores posibles: payment_method.invalid_token, payment_method.no_route, customer.not_found.
{
"id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
"object": "payment_method",
"customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
"connector": "sim",
"status": "pending",
"livemode": false,
"created_at": "2026-07-08T02:19:31.043184+00:00",
"updated_at": "2026-07-08T02:19:31.043184+00:00"
}
{
"id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
"object": "payment_method",
"customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
"connector": "sim",
"brand": "visa",
"last4": "1764",
"status": "active",
"livemode": false,
"created_at": "2026-07-08T02:19:31.043184+00:00",
"updated_at": "2026-07-08T02:19:31.367513+00:00"
}
Lista los métodos guardados del cliente (incluye detached).
Authorization | requerido |
Transición terminal a detached. Idempotente: repetir sobre uno ya detached no falla.
Authorization | requerido |
Errores posibles: payment_method.not_found.
{
"id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e",
"object": "payment_method",
"customer_id": "7c17884b-b250-4aea-9a7b-582f1dae55db",
"connector": "sim",
"brand": "visa",
"last4": "1764",
"status": "detached",
"livemode": false,
"created_at": "2026-07-08T02:19:31.043184+00:00",
"updated_at": "2026-07-08T02:19:46.231193+00:00"
}
Cobro 1-click: manda payment_method_id en POST
/v1/payment_intents/{id}/confirm en vez de payment_token +
payment_method — ver Clientes para el ejemplo
completo. Errores posibles propios de esa variante:
payment_method.not_chargeable, payment_method.concurrent_modification.
Webhook Endpoints
Registra un endpoint para recibir entregas. El secret solo se devuelve aquí.
Authorization | requerido |
Idempotency-Key es opcional aquí y, si lo
mandas, se ignora.
Cuerpo
url | string HTTPS, requerido |
events | string[], opcional — lista blanca de event_type; vacío/omitido = todo el catálogo (ver Webhooks) |
POST /v1/webhook_endpoints
Authorization: Bearer sk_test_...
Content-Type: application/json
{ "url": "https://tu-servidor.mx/webhooks/winal" }
{
"id": "c1a9f2e0-...",
"object": "webhook_endpoint",
"url": "https://tu-servidor.mx/webhooks/winal",
"secret": "whsec_8Kx9..."
}
Lista los endpoints del tenant. Nunca incluye secretos.
Authorization | requerido |
{
"object": "list",
"data": [
{
"id": "c1a9f2e0-...",
"object": "webhook_endpoint",
"url": "https://tu-servidor.mx/webhooks/winal",
"active": true,
"created_at": "2026-07-05T18:00:00Z"
}
]
}
Deshabilita el endpoint (baja lógica: deja de recibir entregas nuevas). No es un borrado
físico — el ledger y el historial son append-only por diseño, y esta fila sigue existiendo
con active: false.
Authorization | requerido |
Errores posibles: webhook_endpoint.not_found.
{ "id": "c1a9f2e0-...", "object": "webhook_endpoint", "deleted": true }
Checkout público (/public)
Diseñados para llamarse directo desde el navegador del pagador (así es como los usa
winal.js): sin Authorization, el client_secret del
intent autentica. La proyección de respuesta es reducida —
nunca incluye client_secret, attempts,
provider_ref ni metadata, y tampoco livemode
(a diferencia de la respuesta autenticada de /v1).
Cuerpo
client_secret | string, requerido |
payment_token | string, requerido |
payment_method | string, requerido |
tip_minor | int64, opcional, no negativo — igual semántica que en /v1/payment_intents/{id}/confirm (reemplaza la propina existente si se manda). |
Si el id no existe o el client_secret no coincide, la
respuesta es siempre el mismo 404 genérico — nunca revela cuál de los dos
falló (defensa contra fuerza bruta).
POST /public/payment_intents/5b6b8b3e-.../confirm
Content-Type: application/json
{
"client_secret": "pi_secret_9fZ3kQ7bV1x...",
"payment_token": "tok_sim_ok",
"payment_method": "card",
"tip_minor": 3000
}
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 3000,
"total_minor": 87900,
"currency": "MXN",
"status": "processing"
}
Query
client_secret | requerido |
Usado por el polling interno de winal.js cada 2 s hasta un estado terminal.
GET /public/payment_intents/5b6b8b3e-...
X-Winal-Client-Secret: pi_secret_9fZ3kQ7bV1x...
{
"id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
"object": "payment_intent",
"amount_minor": 84900,
"tip_minor": 0,
"total_minor": 84900,
"currency": "MXN",
"status": "requires_action",
"next_action": {
"type": "bank_transfer",
"clabe": "646180473921058317",
"beneficiary": "SIM SPEI",
"expires_at": "2026-07-06T18:30:00Z"
}
}
Eventos
Detalle narrativo, patrón de polling y el shape completo del envelope en Eventos y polling.
Stream de polling sobre las entregas de webhook del tenant — alternativa a recibir HTTP
entrante, pensado para desarrollo local y para el CLI oficial
winal listen. Solo devuelve filas si el tenant tiene al menos un
webhook_endpoint activo registrado.
Authorization | requerido |
Query
after_id | int64, opcional (default 0) — cursor exclusivo: devuelve id > after_id. |
limit | int, opcional (default 50, tope 200; valores mayores se recortan). |
GET /v1/events?after_id=0&limit=50
Authorization: Bearer sk_test_...
{
"object": "list",
"data": [
{
"id": 15,
"object": "event",
"event_id": "5e332767-ae86-42e3-b395-cc325de90f2b",
"event_type": "payment_intent.succeeded",
"delivered": false,
"created_at": "2026-07-07T22:55:49Z",
"data": {
"event_id": "5e332767-ae86-42e3-b395-cc325de90f2b",
"event_type": "payment_intent.succeeded",
"created_at": "2026-07-07T22:55:48Z",
"api_version": "2026-07-01",
"livemode": false,
"data": {
"id": "f5f3fc01-8d3b-4abd-881e-52664a1d9c44",
"object": "payment_intent",
"status": "succeeded",
"amount_minor": 10000,
"tip_minor": 1500,
"total_minor": 11500,
"currency": "MXN",
"metadata": { "payment_link_id": "pl_smoke_fosos" }
}
}
}
]
}
Facturas (CFDI)
Guía narrativa completa (PUE vs. PPD, complemento de pagos, factura global, IEPS por concepto y notas de crédito) en Facturación CFDI.
Timbra un CFDI de ingreso, método de pago PUE, sobre un payment_intent ya succeeded. El receptor debe llevar el RFC real de tu cliente: el genérico XAXX010101000 ya no se acepta aquí (rechazo CFDI40130 del SAT) — esas ventas van por POST /v1/invoices/global.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
payment_intent_id | uuid, requerido |
receptor | objeto requerido: rfc, nombre, uso_cfdi, regimen_fiscal, cp (todos requeridos) |
conceptos | arreglo opcional con IVA por línea (16/8/0/exento); cada línea admite además ieps opcional (tasa o cuota) — ver Facturación CFDI → IEPS por concepto — y cantidad_micro/valor_unitario_micro opcionales (los dos juntos o ninguno) para que el concepto refleje lo que de verdad se vendió, no «1 unidad» — ver Facturación CFDI → Cantidad y valor unitario. Si se omite, tasa única 16% sin IEPS |
serie | string opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio. Si se omite, la serie_default del perfil |
folio | string opcional, máx. 40, sin |. Sin default de cuenta |
Errores posibles: invoice.invalid_body, invoice.invalid_receptor, invoice.global_required_for_publico_en_general, invoice.invalid_treatment, invoice.invalid_ieps, invoice.ieps_exceeds_importe, invoice.invalid_cantidad, invoice.cantidad_incompleta, invoice.conceptos_sum_mismatch, invoice.ieps_pac_unsupported, invoice.cfdi_field_invalid, invoice.pac_error (ver Errores). Y los dos de la regla 5: invoice.pac_unreachable y invoice.pac_unreachable_unverified, que no son un rechazo sino un desenlace INDETERMINADO (el PAC no contestó): se reintentan con la misma Idempotency-Key —nunca emitiendo de nuevo— y el segundo ni siquiera automáticamente (ver Facturación CFDI → Si el PAC no contesta).
POST /v1/invoices
Authorization: Bearer sk_test_...
Idempotency-Key: 1a2b3c4d-...
Content-Type: application/json
{
"payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"receptor": {
"rfc": "EKU9003173C9",
"nombre": "ESCUELA KEMPER URGATE",
"uso_cfdi": "G03",
"regimen_fiscal": "601",
"cp": "45050"
},
"serie": "CAJA-2",
"folio": "00981"
}
{
"id": "...",
"object": "invoice",
"status": "stamped",
"payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"uuid_fiscal": "...",
"serie": "CAJA-2", "folio": "00981",
"total_minor": 55000, "base_minor": 47414, "ieps_minor": 0, "iva_minor": 7586,
"currency": "MXN",
"receptor": { "...": "..." },
"pac": "facturama",
"metodo_pago": "PUE",
"parcialidades": 0,
"created_at": "...", "updated_at": "..."
}
Factura una venta ya pagada que no pasó por un cobro de Winal: el mostrador que cobró en efectivo, una transferencia recibida directo en tu banco, una terminal ajena — de un cliente que sí te dio su RFC. Emite un CFDI método de pago PUE sin payment_intent_id.
Como no hay cobro del cual deducirla, tú declaras la forma de pago del catálogo c_FormaPago del SAT. Si el pago aún está pendiente, ésta no es la ruta: usa POST /v1/invoices/ppd. Si el cliente NO dio su RFC, tampoco: esas ventas van por POST /v1/invoices/global.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
total_minor | int64, requerido, > 0 (incluye IVA) |
currency | string ISO 4217, opcional (default MXN) |
receptor | objeto requerido, mismos 5 campos que en POST /v1/invoices |
forma_pago | string requerido: clave c_FormaPago (01 efectivo, 02 cheque, 03 transferencia, 04 tarjeta de crédito, 28 tarjeta de débito…). 99 no se acepta aquí |
descripcion | string, opcional |
conceptos | arreglo opcional con IVA por línea (16/8/0/exento), igual que en POST /v1/invoices — incluido el ieps opcional por línea (tasa o cuota) y cantidad_micro/valor_unitario_micro opcionales (cantidad y valor unitario) |
serie | string opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio |
folio | string opcional, máx. 40, sin | |
Errores posibles: invoice.invalid_body, invoice.invalid_receptor, invoice.invalid_forma_pago, invoice.forma_pago_requires_ppd, invoice.global_required_for_publico_en_general, invoice.receptor_incoherente, invoice.invalid_treatment, invoice.invalid_ieps, invoice.ieps_exceeds_importe, invoice.invalid_cantidad, invoice.cantidad_incompleta, invoice.conceptos_sum_mismatch, invoice.ieps_pac_unsupported, invoice.livemode_mismatch, invoice.cfdi_field_invalid, invoice.pac_error. Y los dos de la regla 5: invoice.pac_unreachable y invoice.pac_unreachable_unverified, que no son un rechazo sino un desenlace INDETERMINADO (el PAC no contestó): se reintentan con la misma Idempotency-Key —nunca emitiendo de nuevo— y el segundo ni siquiera automáticamente (ver Facturación CFDI → Si el PAC no contesta).
POST /v1/invoices/pue
Authorization: Bearer sk_test_...
Idempotency-Key: 7f8a9b0c-...
Content-Type: application/json
{
"total_minor": 23200,
"currency": "MXN",
"forma_pago": "01",
"receptor": {
"rfc": "EKU9003173C9",
"nombre": "ESCUELA KEMPER URGATE",
"uso_cfdi": "G03",
"regimen_fiscal": "601",
"cp": "45050"
},
"descripcion": "Venta de mostrador",
"serie": "CAJA-2",
"folio": "00981"
}
{
"id": "...",
"object": "invoice",
"status": "stamped",
"uuid_fiscal": "...",
"serie": "CAJA-2", "folio": "00981",
"total_minor": 23200, "base_minor": 20000, "ieps_minor": 0, "iva_minor": 3200,
"currency": "MXN",
"receptor": { "...": "..." },
"pac": "facturama",
"metodo_pago": "PUE",
"parcialidades": 0,
"created_at": "...", "updated_at": "..."
}
Emite un CFDI método de pago PPD por un total acordado, SIN cobro previo — se liquidará después con uno o más REP. Como en /pue, el receptor necesita el RFC real de tu cliente; el genérico no se acepta.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
total_minor | int64, requerido, > 0 (incluye IVA) |
currency | string ISO 4217, opcional (default MXN) |
receptor | objeto requerido, mismos 5 campos que en POST /v1/invoices |
descripcion | string, opcional |
conceptos | arreglo opcional con IVA por línea, igual que en POST /v1/invoices, con dos restricciones propias de la PPD: ninguna línea puede llevar ieps (invoice.ppd_ieps_unsupported) y todas deben compartir el mismo treatment (invoice.ppd_mixed_rate_unsupported). Las dos se rechazan antes de timbrar, porque el REP de esa factura no podría emitirse nunca y el comprobante quedaría ante el SAT sin forma de cerrarse — ver Facturación CFDI → PPD → Lo que una PPD no admite. cantidad_micro/valor_unitario_micro opcionales, sin restricción propia de la PPD (cantidad y valor unitario) |
serie | string opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio |
folio | string opcional, máx. 40, sin | |
Errores posibles: invoice.invalid_body, invoice.invalid_receptor, invoice.invalid_currency, invoice.global_required_for_publico_en_general, invoice.invalid_treatment, invoice.invalid_ieps, invoice.ppd_ieps_unsupported, invoice.ppd_mixed_rate_unsupported, invoice.ieps_exceeds_importe, invoice.invalid_cantidad, invoice.cantidad_incompleta, invoice.conceptos_sum_mismatch, invoice.ieps_pac_unsupported, invoice.cfdi_field_invalid, invoice.pac_error. Y los dos de la regla 5: invoice.pac_unreachable y invoice.pac_unreachable_unverified, que no son un rechazo sino un desenlace INDETERMINADO (el PAC no contestó): se reintentan con la misma Idempotency-Key —nunca emitiendo de nuevo— y el segundo ni siquiera automáticamente (ver Facturación CFDI → Si el PAC no contesta).
POST /v1/invoices/ppd
Authorization: Bearer sk_test_...
Idempotency-Key: 2b3c4d5e-...
Content-Type: application/json
{
"total_minor": 348000,
"currency": "MXN",
"receptor": {
"rfc": "EKU9003173C9",
"nombre": "ESCUELA KEMPER URGATE",
"uso_cfdi": "G03",
"regimen_fiscal": "601",
"cp": "45050"
},
"descripcion": "Servicios profesionales - anticipo PPD",
"serie": "PPD-A",
"folio": "500"
}
{
"id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
"object": "invoice",
"status": "stamped",
"total_minor": 348000,
"base_minor": 300000,
"ieps_minor": 0,
"iva_minor": 48000,
"currency": "MXN",
"metodo_pago": "PPD",
"saldo_insoluto_minor": 348000,
"parcialidades": 0,
"pac": "facturama",
"created_at": "2026-07-07T23:02:30Z",
"updated_at": "2026-07-07T23:02:31Z"
}
La FACTURA GLOBAL del período: un CFDI de ingreso a "PÚBLICO EN GENERAL" que agrupa todas las ventas de las que nadie pidió comprobante. Método de pago PUE siempre. No recibe receptor ni total_minor: el receptor lo arma Winal (RFC genérico + CP de tu perfil fiscal) y el total sale de sumar conceptos. Guía narrativa completa en Facturación CFDI → Factura global.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
periodicidad | string requerido — c_Periodicidad: 01 diario, 02 semanal, 03 quincenal, 04 mensual, 05 bimestral (exclusiva del RIF, régimen 621) |
meses | string requerido — c_Meses: 01–12 meses naturales; 13–18 bimestres, solo con periodicidad 05 |
anio | int32 requerido — año en curso o el inmediato anterior |
forma_pago | string requerido — c_FormaPago de la operación de mayor monto del período. 99 no se acepta |
conceptos | arreglo requerido, ≥ 1 elemento — mismo shape que en las otras rutas (incluido ieps y cantidad_micro/valor_unitario_micro por línea — cantidad y valor unitario), más no_identificacion opcional (folio del ticket, 1–100 caracteres) |
currency | string ISO 4217, opcional (default MXN) |
sustituye_uuid | string, opcional — uuid_fiscal de la global que ésta corrige (relación 04); omítelo en una emisión normal |
serie | string opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio. Una global suele llevar su propia serie |
folio | string opcional, máx. 40, sin |. No es no_identificacion (el folio del ticket, por línea): éste es el folio DEL COMPROBANTE |
Errores posibles: invoice.invalid_body, invoice.invalid_forma_pago, invoice.forma_pago_requires_ppd, invoice.invalid_concepto, invoice.invalid_treatment, invoice.invalid_ieps, invoice.ieps_exceeds_importe, invoice.invalid_cantidad, invoice.cantidad_incompleta, invoice.ieps_pac_unsupported, invoice.invalid_currency, invoice.global_periodo_invalido, invoice.livemode_mismatch, invoice.global_already_exists, invoice.invalid_sustituye_uuid, invoice.sustituida_not_found, invoice.sustitucion_periodo_distinto, invoice.cfdi_field_invalid, invoice.pac_error (ver Errores). Y los dos de la regla 5: invoice.pac_unreachable y invoice.pac_unreachable_unverified, que no son un rechazo sino un desenlace INDETERMINADO (el PAC no contestó): se reintentan con la misma Idempotency-Key —nunca emitiendo de nuevo— y el segundo ni siquiera automáticamente (ver Facturación CFDI → Si el PAC no contesta).
POST /v1/invoices/global
Authorization: Bearer sk_test_...
Idempotency-Key: 4d5e6f70-...
Content-Type: application/json
{
"periodicidad": "04",
"meses": "07",
"anio": 2026,
"forma_pago": "01",
"conceptos": [
{ "importe_minor": 116000, "treatment": "tasa16", "descripcion": "Ventas gravadas del período" },
{ "importe_minor": 96500, "treatment": "exento", "descripcion": "Medicinas de patente" }
],
"serie": "G",
"folio": "0007"
}
{
"id": "...",
"object": "invoice",
"status": "stamped",
"uuid_fiscal": "...",
"serie": "G",
"folio": "0007",
"total_minor": 212500, "base_minor": 196500, "ieps_minor": 0, "iva_minor": 16000,
"currency": "MXN",
"receptor": { "...": "..." },
"pac": "finkok",
"metodo_pago": "PUE",
"parcialidades": 0,
"created_at": "...", "updated_at": "...",
"informacion_global": { "periodicidad": "04", "meses": "07", "anio": 2026 }
}
Registra un pago sobre una factura PPD ya stamped y timbra su complemento de pagos 2.0 (REP).
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo — dos variantes, elige una: con payment_intent_id (cobró Winal) o con amount_minor + forma_pago (cobraste tú por fuera). Mandar las dos responde invoice.invalid_body.
payment_intent_id | uuid — cobro ya succeeded que liquida (parte de) el saldo insoluto. Con él, el importe, la forma de pago y la fecha se LEEN del cobro; sin él, se declaran abajo |
amount_minor | int, requerido sin payment_intent_id — importe recibido en centavos (incluye IVA); positivo y no mayor que el saldo insoluto |
forma_pago | string, requerido sin payment_intent_id — clave c_FormaPago del SAT (01 efectivo, 03 transferencia, 04 crédito, 28 débito…). Nunca 99: es la de la factura PPD que este REP liquida |
fecha_pago | timestamp opcional, por defecto ahora — cuándo se recibió el pago. Ni futura ni de un día anterior al de expedición de la factura (comparado por día calendario en la zona horaria del lugar de expedición, no por instante — el mismo día con hora anterior a la de la factura vale) (invoice.payment_fecha_invalid); se admite una holgura de 5 minutos sobre el instante actual, para el POS sin NTP (medido contra el PAC: un pago fechado 4 min 59 s por delante timbra) — ver Facturación CFDI → pago sin cobro de Winal |
currency | string opcional, MXN por defecto — debe coincidir con la de la factura |
serie | string opcional, máx. 25, sin | — serie PROPIA de este REP (no la de la factura que liquida) — ver Facturación CFDI → Serie y folio |
folio | string opcional, máx. 40, sin | — folio propio de este REP. Se escribe en el XML pero InvoicePaymentResponse NO lo devuelve (a diferencia de invoice) |
Errores posibles: invoice.invalid_body, invoice.not_found (404), invoice.not_ppd, invoice.not_stamped, invoice.intent_not_found (404), invoice.payment_intent_already_applied, invoice.payment_exceeds_saldo, invoice.payment_currency_mismatch, invoice.payment_already_exists, invoice.invalid_forma_pago, invoice.payment_fecha_invalid, invoice.invalid_currency, invoice.livemode_mismatch, invoice.mixed_rate_unsupported, invoice.ieps_breakdown_unsupported, invoice.cfdi_field_invalid, invoice.pac_error, invoice.pac_unreachable, invoice.pac_unreachable_unverified, invoice.idempotency_conflict (409, misma llave con otro pago — incluido reusarla nombrando OTRO payment_intent_id), invoice.payment_not_retryable, invoice.payment_retry_conflict (409, otro reintento del mismo REP se adelantó). La variante SIN cobro cruza el ambiente contra tu LLAVE, y las dos lo cruzan además contra el ambiente en que se timbró la factura PPD que se liquida (ver Facturación CFDI → pago sin cobro de Winal). Reintentar con la MISMA Idempotency-Key no devuelve el fallo congelado: REANUDA ese mismo complemento.
POST /v1/invoices/5cee05bd-.../payments
Authorization: Bearer sk_test_...
Idempotency-Key: 3c4d5e6f-...
Content-Type: application/json
{
"payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"serie": "REP-A",
"folio": "9001"
}
{
"id": "...",
"object": "invoice_payment",
"invoice_id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
"payment_intent_id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"parcialidad": 1,
"monto_minor": 55000,
"saldo_anterior_minor": 348000,
"saldo_insoluto_minor": 293000,
"currency": "MXN",
"status": "stamped",
"rep_uuid": "...",
"created_at": "...", "updated_at": "..."
}
Vuelve a timbrar el MISMO complemento de pago que quedó en error. Sin cuerpo: todo lo que determina el comprobante ya está guardado en su fila, y reintentar es reenviar exactamente eso —misma fecha de expedición, mismos saldos, misma serie/folio— marcado como reintento, para que un PAC que sabe recuperar el timbre devuelva el folio anterior en vez de emitir otro. Con un PAC SIN esa recuperación (Facturama, por ejemplo) el reenvío es idéntico pero el PAC no está obligado a reconocerlo como el mismo intento.
Authorization | requerido |
Errores posibles: invoice.payment_not_retryable, invoice.payment_already_exists, invoice.payment_retry_conflict (409), invoice.payment_not_found (404), invoice.not_found (404), invoice.not_ppd, invoice.not_stamped, invoice.livemode_mismatch, invoice.pac_error, invoice.pac_unreachable, invoice.pac_unreachable_unverified. Reintentar uno ya stamped devuelve 200 con él, sin volver a llamar al PAC — y lo mismo recibe quien pierda una carrera contra otro reintento que sí llegó a timbrarlo. Re-timbrar exige, además, que la factura PPD siga timbrada y viva: si se canceló entretanto responde invoice.not_stamped, porque un complemento no liquida un comprobante cancelado. Y el ambiente: si el REP nace de un cobro lo dicta el cobro, no la llave con la que llamas; solo el REP del mostrador toma el de tu llave.
POST /v1/invoices/5cee05bd-.../payments/8f2a1c7e-.../retry
Authorization: Bearer sk_test_...
Lista los REP timbrados contra una factura PPD.
Authorization | requerido |
{ "object": "list", "data": [] }
Descarga el CFDI ya timbrado de tu cuenta — sin importar cuál de las cuatro rutas de emisión lo generó (POST /v1/invoices, /pue, /ppd o /global). Ruta privada, con tu Authorization: no confundir con las descargas públicas de Autofactura más abajo, que son otra ruta (/public/autofactura/{slug}/invoices/{id}/xml/pdf) sin clave, pensada para que el cliente final baje su propio comprobante.
Authorization | requerido |
Errores posibles: invoice.not_found (404); invoice.not_stamped (400 — el CFDI existe pero aún no timbra), invoice.no_provider_ref, invoice.pac_download_error y invoice.pac_empty_file responden 400 también (ver Errores).
GET /v1/invoices/5cee05bd-.../xml
Authorization: Bearer sk_test_...
Content-Type: application/xml
Content-Disposition: attachment; filename="cfdi-5cee05bd-....xml"
<cfdi:Comprobante ...>...</cfdi:Comprobante>
Cancela ante el SAT un CFDI de ingreso ya stamped (PUE, PPD o global), con los cuatro motivos del Anexo 20. Idempotente: cancelar un CFDI ya canceled devuelve éxito sin volver a llamar al PAC. Guía narrativa completa —incluida la demora real del SAT para reconocer un UUID recién timbrado— en Facturación CFDI → Cancelar un CFDI ya timbrado.
Authorization | requerido |
Idempotency-Key | requerido (UUID) — misma regla que el resto de /v1/invoices* |
Cuerpo
motive | string, opcional — 01/02/03/04; default 02 |
substitution_uuid | string, opcional — uuid_fiscal del CFDI sustituto; requerido con motive: "01" |
Errores posibles: invoice.not_found (404); el resto —invoice.invalid_cancel_motive, invoice.cancel_substitution_required, invoice.not_stamped, invoice.cancel_requires_substitution, invoice.no_provider_ref, invoice.livemode_mismatch, invoice.cancel_pending, invoice.pac_cancel_error (incluye el "No Encontrado" por propagación del SAT — ver la guía narrativa)— responde 400, no 409 (ver Errores para el detalle de cada uno). El ambiente se cruza contra el comprobante, no solo contra tu perfil: un CFDI que se timbró en pruebas se cancela SIEMPRE en pruebas (sk_test_ y perfil en sandbox), y el de producción, en producción — aunque tu cuenta haya cambiado de ambiente entretanto.
POST /v1/invoices/5cee05bd-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 8e9f0a1b-...
Content-Type: application/json
{ "motive": "02" }
{
"object": "invoice",
"id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
"status": "canceled",
"uuid_fiscal": "..."
}
Notas de crédito (CFDI de egreso por un reembolso)
Guía narrativa completa —la relación obligatoria con la factura de ingreso, los importes SIEMPRE en positivo, el tope agregado por factura y los límites con IEPS/tasas mezcladas— en Facturación CFDI → Nota de crédito.
Emite una nota de crédito (CFDI 4.0 de EGRESO) por el reembolso de una factura de ingreso ya stamped. La relación con la factura (CfdiRelacionados tipo 01) la arma Winal; el monto FISCAL sale del refund_id real, y amount_minor es solo una confirmación que debe coincidir EXACTO. Idempotente por refund_id: reintentar con el mismo devuelve la nota viva ya existente.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
refund_id | uuid, requerido — reembolso succeeded real; también la clave de idempotencia |
amount_minor | int64, requerido, > 0 (centavos, siempre positivo — el signo lo da el tipo de comprobante, no el monto) |
currency | string ISO 4217, opcional (default MXN) |
motivo | string, opcional (default genérico "Devolución") |
forma_pago | string, opcional — c_FormaPago con la que devolviste el dinero (default 03); 99 no se acepta |
serie | string opcional, máx. 25, sin | — muchos comercios llevan una serie propia para sus notas (p. ej. "NC") — ver Facturación CFDI → Serie y folio |
folio | string opcional, máx. 40, sin | |
Errores posibles: invoice.credit_note_invalid_request, invoice.invalid_currency, invoice.invalid_forma_pago, invoice.not_found (404), invoice.not_stamped, invoice.refund_not_found (404), invoice.refund_not_succeeded, invoice.refund_amount_mismatch, invoice.credit_note_currency_mismatch, invoice.refund_intent_mismatch, invoice.credit_notes_exceed_total, invoice.mixed_rate_unsupported, invoice.ieps_breakdown_unsupported, invoice.cfdi_field_invalid, invoice.pac_error. Todos los demás responden 400, no 409 (mapeo por subcadena — ver Errores → Notas de crédito). Exige un cobro DE WINAL detrás de la factura (un payment_intent_id real del cual colgar el refund_id): una factura de POST /v1/invoices/pue nunca lo tiene — usa /credit-notes/pue abajo.
POST /v1/invoices/f47e2b91-6a3d-4c8e-9b2a-1d5e8f3c6a97/credit-notes
Authorization: Bearer sk_test_...
Idempotency-Key: 9f0a1b2c-...
Content-Type: application/json
{
"refund_id": "b3f6a1c2-8e4d-4a9b-9c1e-2f6a8b0d5e17",
"amount_minor": 23200,
"forma_pago": "01",
"motivo": "Devolución de mercancía",
"serie": "NC",
"folio": "0007"
}
{
"object": "credit_note",
"id": "7a2e4c1b-9f3d-4b6e-8a2c-1d5f9e3b7c4a",
"invoice_id": "f47e2b91-6a3d-4c8e-9b2a-1d5e8f3c6a97",
"refund_id": "b3f6a1c2-8e4d-4a9b-9c1e-2f6a8b0d5e17",
"status": "stamped",
"uuid_fiscal": "...",
"related_uuid": "...",
"serie": "NC",
"folio": "0007",
"total_minor": 23200,
"base_minor": 20000,
"iva_minor": 3200,
"currency": "MXN",
"motivo": "Devolución de mercancía"
}
El equivalente de POST /v1/invoices/pue para el EGRESO: emite una nota de crédito SIN reembolso de Winal — la devolución de mostrador de una venta que Winal nunca cobró. Sin refund_id del cual leer el monto, lo declaras tú (amount_minor + forma_pago). Mismo trámite fiscal que la ruta de arriba (tope agregado, relación 01, sellador de egreso); sin idempotencia por refund (no hay refund) — la protege el Idempotency-Key de siempre.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
amount_minor | int64, requerido, > 0 (centavos) |
currency | string ISO 4217, opcional (default MXN) |
motivo | string, opcional (default genérico "Devolución") |
forma_pago | string, opcional — con qué DEVOLVISTE el dinero (default 03); 99 no se acepta |
serie | string opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio |
folio | string opcional, máx. 40, sin | |
Errores posibles: invoice.credit_note_invalid_request, invoice.invalid_currency, invoice.invalid_forma_pago, invoice.not_found (404), invoice.not_stamped, invoice.credit_note_currency_mismatch, invoice.credit_notes_exceed_total, invoice.mixed_rate_unsupported, invoice.ieps_breakdown_unsupported, invoice.cfdi_field_invalid, invoice.pac_error.
POST /v1/invoices/5cee05bd-de0c-4961-98eb-e0cacfc6aae8/credit-notes/pue
Authorization: Bearer sk_test_...
Idempotency-Key: 9f0a1b2c-...
Content-Type: application/json
{
"amount_minor": 23200,
"forma_pago": "01",
"motivo": "Devolución de mercancía en el mostrador",
"serie": "NC",
"folio": "0008"
}
refund_id: null{
"object": "credit_note",
"id": "3c8a7d1e-5b2f-4e9a-8c6d-0f1a3b7c9e42",
"invoice_id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
"refund_id": null,
"status": "stamped",
"uuid_fiscal": "...",
"related_uuid": "...",
"serie": "NC",
"folio": "0008",
"total_minor": 23200,
"base_minor": 20000,
"iva_minor": 3200,
"currency": "MXN",
"motivo": "Devolución de mercancía en el mostrador"
}
Lista las notas de crédito (cualquier estado) de una factura, del más nuevo al más viejo.
Authorization | requerido |
{ "object": "list", "data": [] }
Consulta una nota de crédito puntual. Debe pertenecer a la factura {id} de la ruta: una nota de OTRA factura del mismo tenant se ve como invoice.credit_note_not_found (404, no 403 — no confirma que el id exista en otro lado).
Authorization | requerido |
Errores posibles: invoice.credit_note_not_found (404).
GET /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-...
Authorization: Bearer sk_test_...
Descarga el XML/PDF de una nota timbrada. A diferencia de la descarga de una factura de ingreso, aquí SÍ se puede descargar una nota ya canceled (el comprobante existió y hay que conservarlo); lo que no se descarga es una que nunca llegó a timbrarse.
Authorization | requerido |
Errores posibles: invoice.credit_note_not_found (404); invoice.credit_note_not_stamped, invoice.no_provider_ref, invoice.pac_download_error, invoice.pac_empty_file responden 400.
GET /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../xml
Authorization: Bearer sk_test_...
Content-Type: application/xml
Content-Disposition: attachment; filename="nota-credito-7a2e4c1b-....xml"
<cfdi:Comprobante ...>...</cfdi:Comprobante>
Vuelve a timbrar una nota de crédito que quedó en error — espejo de POST /v1/invoices/{id}/retry. Reenvía EL MISMO comprobante (mismos importes, mismas líneas, misma serie/folio, misma forma de pago y la misma fecha de expedición), marcado como reintento, para que un PAC que sabe recuperar el timbre devuelva el folio anterior en vez de emitir otro. Con un PAC SIN esa recuperación (Facturama, por ejemplo) el reenvío es idéntico pero el PAC no está obligado a reconocerlo como el mismo intento. Sin cuerpo. Si la nota ya está stamped o canceled, es un no-op exitoso que no llama al PAC.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Errores posibles: invoice.credit_note_not_found, invoice.not_found (404); invoice.credit_note_not_retryable, invoice.fiscal_profile_missing, invoice.livemode_mismatch, invoice.pac_error, invoice.pac_unreachable responden 400.
POST /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../retry
Authorization: Bearer sk_test_...
Idempotency-Key: 0a1b2c3d-...
credit_note, ya timbrado{
"object": "credit_note",
"id": "7a2e4c1b-9f3d-4b6e-8a2c-1d5f9e3b7c4a",
"status": "stamped",
"uuid_fiscal": "..."
}
Cancela ante el SAT una nota de crédito ya stamped — mismo contrato que POST /v1/invoices/{id}/cancel (los cuatro motivos, motive: "01" exige substitution_uuid, idempotente). La diferencia: la respuesta aquí es el credit_note completo, no un shape recortado.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
motive | string, opcional — 01/02/03/04; default 02 |
substitution_uuid | string, opcional — requerido con motive: "01" |
Errores posibles: invoice.credit_note_not_found (404); invoice.invalid_cancel_motive, invoice.cancel_substitution_required, invoice.credit_note_not_stamped, invoice.no_provider_ref, invoice.livemode_mismatch, invoice.cancel_pending, invoice.pac_cancel_error responden 400. El ambiente se cruza contra la nota, igual que en la cancelación de la factura de ingreso.
POST /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 0a1b2c3d-...
Content-Type: application/json
{ "motive": "02" }
credit_note completo){
"object": "credit_note",
"id": "7a2e4c1b-9f3d-4b6e-8a2c-1d5f9e3b7c4a",
"invoice_id": "5cee05bd-de0c-4961-98eb-e0cacfc6aae8",
"refund_id": "b3f6a1c2-8e4d-4a9b-9c1e-2f6a8b0d5e17",
"status": "canceled",
"uuid_fiscal": "...",
"related_uuid": "...",
"total_minor": 23200,
"base_minor": 20000,
"iva_minor": 3200,
"currency": "MXN",
"motivo": "Devolución de mercancía"
}
Cobranza (Receivables)
Guía narrativa completa (Payment Link automático, marcado como paid, auto-CFDI)
en Cobranza.
Crea una cuenta por cobrar; internamente crea un Payment Link de un solo uso.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
customer_name / customer_email | string, requeridos |
customer_phone | string, opcional — requerido solo para whatsapp_link |
customer_rfc / customer_uso_cfdi / customer_regimen_fiscal / customer_cp | opcionales, pero van juntos o ninguno (auto-CFDI al pagarse) |
concepto | string, requerido |
amount_minor | int64, requerido, > 0 |
currency | string ISO 4217, requerido |
due_date | datetime ISO-8601, requerido |
Errores posibles: receivable.missing_fields, receivable.invalid_amount, receivable.invalid_currency, receivable.invalid_date, receivable.incomplete_fiscal_receptor.
{
"id": "0ec348fb-2341-4f31-b870-9cc4d3087b29",
"object": "receivable",
"customer_name": "María López",
"customer_email": "maria.lopez@example.mx",
"customer_phone": "5215512345678",
"amount_minor": 150000,
"currency": "MXN",
"concepto": "Mensualidad julio 2026 - plan Pro",
"due_date": "2026-07-20T00:00:00+00:00",
"status": "open",
"payment_link_id": "27011835-fd92-44dd-8d6c-549c00536703",
"payment_link_url": "/pay/QOECDPCH2HC",
"created_at": "2026-07-08T02:19:53.743499+00:00"
}
Lista o lee una cuenta por cobrar. status es derivado: open | paid | overdue | canceled.
Authorization | requerido |
Errores posibles: receivable.not_found.
Arma la URL https://wa.me/... con el mensaje de cobro pre-redactado.
Authorization | requerido |
Errores posibles: receivable.no_phone (sin customer_phone capturado).
{ "url": "https://wa.me/5215512345678?text=Hola%20Mar%C3%ADa..." }
Estado de cuenta agregado de un cliente: sus cuentas y los totales abierto/vencido/pagado.
Authorization | requerido |
{
"customer_email": "maria.lopez@example.mx",
"receivables": [ { "...": "..." } ],
"total_open_minor": 150000,
"total_overdue_minor": 0,
"total_paid_minor": 0
}
Antigüedad de saldos por cliente, en 4 cubos contables estándar.
Authorization | requerido |
{
"object": "list",
"data": [
{
"customer_email": "aging@prueba.mx",
"customer_name": "Prueba Aging",
"currency": "MXN",
"bucket0_to30_minor": 30000,
"bucket31_to60_minor": 0,
"bucket61_to90_minor": 0,
"bucket90_plus_minor": 0,
"total_minor": 30000
}
]
}
Conciliación bancaria
Guía narrativa completa (presets, mapeo de columnas, tipos de excepción) en Conciliación bancaria.
Importa el CSV del estado de cuenta bancario. Multipart (file) o cuerpo
crudo. Sin Idempotency-Key — su idempotencia real es el hash del
archivo.
Authorization | requerido |
Campos (multipart o query)
bank | string, requerido |
period_start / period_end | yyyy-MM-dd, requeridos |
tolerance_days | int, opcional |
preset | bbva | banorte | santander, o usa el mapeo explícito |
date_column / description_column / credit_column / debit_column | int (0-based), requeridos si no hay preset |
reference_column | int, opcional |
has_header | bool, opcional (default true) |
Errores posibles: ver la tabla completa en Conciliación bancaria.
{
"id": "271a21c8-be04-4d6e-909a-8f1394fd333a",
"object": "bank_statement",
"bank": "bbva",
"period_start": "2026-07-01",
"period_end": "2026-07-08",
"filename": "estado_bbva.csv",
"lines_total": 2,
"matched": 0,
"partial": 0,
"unmatched": 1,
"exceptions": 2,
"already_imported": false
}
Lista los estados de cuenta importados, o las líneas de uno (filtro opcional matched/unmatched/partial).
Authorization | requerido |
Errores posibles: bank_statement.invalid_match_status.
{
"id": 3,
"object": "bank_statement_line",
"value_date": "2026-07-01",
"description": "SPEI RECIBIDO ANTECH",
"reference": "REF001",
"credit_minor": 50000,
"currency": "MXN",
"match_status": "unmatched"
}
Autofactura pública
Guía narrativa completa (receipt_code, anti-enumeración) en
Facturación CFDI → Autofactura.
Sin Authorization — la llave es receipt_code + rfc.
Cuerpo
receipt_code | string, requerido — formato W-XXXXX, viene en cada payment_intent |
rfc | string, requerido — formato SAT |
nombre / uso_cfdi / regimen_fiscal / cp | string, requeridos |
email | string, opcional (captura sin efecto en el timbrado hoy) |
Errores posibles: autofactura.invalid_body, autofactura.invalid_rfc, autofactura.generic_rfc_not_allowed (el RFC genérico XAXX010101000, bien formado pero ya no admitido — se rechaza antes de resolver slug o receipt_code), autofactura.not_available (404), autofactura.receipt_not_found (404), más los errores de invoice.* del PAC reenviados tal cual.
{
"error": {
"type": "invalid_request_error",
"code": "invoice.pac_error",
"message": "El PAC rechazó el timbrado (CFDI 492400bb-... quedó en 'error'): ",
"doc_url": "https://winal.com.mx/docs/errores.html#err-invoice.pac_error",
"request_id": "0HNMSIOBNVS9K:00000001"
}
}
Descarga del CFDI ya timbrado; ambos parámetros deben coincidir con el CFDI.
Errores posibles: invoice.not_stamped, autofactura.receipt_not_found.
Reportes
Guía narrativa con el significado de cada campo en Reportes. Salvo
donde se indica lo contrario, from/to son ISO-8601 opcionales
(default: últimos 30 días), rango semi-abierto [from, to).
Cobrado, ticket promedio, reembolsado, disputas (con su holdback) y tasa de aprobación del período.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales |
currency | ISO 4217, opcional (default MXN) |
Errores posibles: reports.invalid_period, reports.invalid_currency.
{
"object": "reporting.summary",
"currency": "MXN",
"charged_minor": 1598624,
"charge_count": 28,
"average_ticket_minor": 57094,
"refunded_minor": 15000,
"dispute_count": 1,
"disputes_held_minor": 25000,
"approval_rate": 0.933,
"period_from": "2026-07-01T00:00:00Z",
"period_to": "2026-08-01T00:00:00Z"
}
El mismo período de summary, desglosado día por día.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales |
currency | ISO 4217, opcional (default MXN) |
Errores posibles: reports.invalid_period, reports.invalid_currency.
{ "object": "list", "data": [
{ "object": "reporting.daily_point", "date": "2026-07-01", "charged_minor": 458200, "refunded_minor": 0, "operation_count": 6 }
] }
El período agrupado por método de pago o por conector ganador del ruteo, cada fila con su propio approval_rate.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales |
currency | ISO 4217, opcional (default MXN) |
Errores posibles: reports.invalid_period, reports.invalid_currency.
by-method, respuesta real (fragmento){ "object": "list", "data": [
{ "object": "reporting.method_breakdown", "method": "card", "amount_minor": 1256490, "charge_count": 20, "approval_rate": 0.9 }
] }
by-connector, respuesta real (fragmento){ "object": "list", "data": [
{ "object": "reporting.connector_breakdown", "connector_key": "sim", "amount_minor": 1598624, "charge_count": 28, "approval_rate": 0.933 }
] }
Operaciones recientes, más nuevas primero — paginado por cursor opaco, sin from/to.
Authorization | requerido |
Query
limit | opcional, default 20, tope 100 |
cursor | opaco, opcional — el next_cursor de la página anterior |
Errores posibles: reports.invalid_cursor.
{
"object": "list",
"data": [
{ "object": "reporting.payment", "id": "a55fc12f-...", "amount_minor": 55000,
"currency": "MXN", "method": "card", "connector_key": "sim", "status": "succeeded",
"created_at": "2026-07-08T06:47:46.86Z" }
],
"has_more": true,
"next_cursor": "eyJpZCI6MTI4LCJ0cyI6..."
}
El asiento contable del ledger, línea por línea, sin agregar: cargo/abono en centavos ya separados en dos columnas. text/csv, streaming.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales — SIN currency: trae todas las monedas del período |
Errores posibles: reports.invalid_period.
fecha_efectiva,tipo,cuenta,cargo_minor,abono_minor,referencia
2026-07-06T18:03:11.0000000+00:00,charge,merchant_settlement,150000,0,charge:attempt_852d...
Informe de retenciones ISR/IVA del régimen de plataformas digitales por vendedor, base del DIOT. Período mensual, requerido — from/to no aplican aquí.
Authorization | requerido |
Query
period | requerido, YYYY-MM |
format | opcional, csv para el export contable (default JSON) |
Errores posibles: reports.invalid_period.
{
"object": "retention_report", "period": "2026-07", "vendor_count": 2,
"total_gross_minor": 458200, "total_base_minor": 394655,
"total_isr_retenido_minor": 4582, "total_iva_retenido_minor": 39466,
"total_retenido_minor": 44048,
"data": [ { "sub_merchant_name": "Farmacia del Centro", "rfc": "EWE1709045U0", "...": "..." } ]
}
Cuánto le ahorró/recuperó Winal al tenant en el período: ruteo consciente de costo, retry cross-conector y dunning de suscripciones.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales (default: últimos 30 días) |
Errores posibles: reports.invalid_period.
{
"object": "reporting.savings",
"routing_saved_minor": 0,
"retry_recovered_minor": 0,
"dunning_recovered_minor": 0,
"total_minor": 0,
"by_connector": [],
"period_from": "2026-06-07T23:02:03Z",
"period_to": "2026-07-07T23:02:03Z"
}
Pólizas contables del período, formato CONTPAQi o Aspel-COI, listas para importar.
Authorization | requerido |
Query
from / to | ISO-8601, opcionales (default: últimos 30 días) |
format | requerido, sin default: contpaqi | aspel_coi |
Errores posibles: reports.invalid_period, reports.invalid_format.
GET /v1/reports/polizas?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&format=contpaqi
Authorization: Bearer sk_test_...
P 20260706 3 1 1 0 Poliza diario Winal 2026-07-06
M 108-001 1 0 150.00 0 0.00 charge: charge:attempt_852d...
Corte de caja del turno: desglose por método/conector (cobrado, propinas, operaciones), totales y devoluciones. Ver Reportes → Multisucursal para el detalle narrativo.
Authorization | requerido |
Query
from / to | ISO-8601, ambos requeridos (sin default: no hay "turno" sin rango explícito) |
branch / register | opcionales, igualdad exacta contra metadata.branch/metadata.register del intent — no validan contra ningún catálogo. |
Errores posibles: reports.invalid_period, reports.invalid_currency.
{
"object": "reporting.cash_cut",
"currency": "MXN",
"period_from": "2026-07-01T00:00:00Z",
"period_to": "2026-07-08T00:00:00Z",
"by_method": [
{ "key": "card", "charged_minor": 256490, "tip_minor": 6500, "total_minor": 262990, "operation_count": 13 }
],
"by_connector": [
{ "key": "sim", "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 }
],
"totals": { "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 },
"refunds": { "count": 0, "amount_minor": 0 },
"by_branch": [
{ "key": "(sin sucursal)", "charged_minor": 1589124, "tip_minor": 9500, "total_minor": 1598624, "operation_count": 27 },
{ "key": "centro", "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 }
]
}
Cuentas administradas
Si tu negocio tiene sus propios clientes —cada uno con su propio RFC— das de alta una cuenta por
cada uno y operas por ellas presentando dos credenciales: la tuya (Authorization, quién
actúa) y la de la cuenta (Winal-Account-Key, sobre quién). Guía completa, con un
ejemplo real de facturación por delegación y un script de firma con openssl, en
Cuentas administradas.
| Método | Ruta | Notas |
|---|---|---|
| POST | /v1/accounts | Scope accounts:write; exige ventana de alta abierta desde tu consola. Idempotente por reference. No admite delegación. |
| GET | /v1/accounts | Lista tus cuentas administradas. ?limit=, por omisión 100, tope 500. |
Cuerpo de POST /v1/accounts
| Campo | Tipo | Descripción |
|---|---|---|
reference | string, requerido | Identificador de esta cuenta en TU sistema (1–128 caracteres: letras, dígitos, . _ : -). Hace el alta idempotente. |
name | string, requerido | Nombre comercial de la cuenta (hasta 200 caracteres). |
scopes | arreglo, opcional | Scopes de la credencial que se emite. Sin este campo: payments:read, payments:write, refunds:write, customers:write, webhooks:manage, disputes:write — los de operación. Nunca el comodín *, payouts:write ni accounts:write: pedirlos responde account.scope_not_allowed, porque esa credencial se la entregas a un tercero y dispersar o administrar cuentas son actos tuyos. |
curl -s https://api.winal.com.mx/v1/accounts \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "reference": "farmacia-001", "name": "Farmacia Ejemplo" }'
{
"account": {
"id": "54d6557b-a52e-4e8a-9d49-880df22edeac",
"object": "account",
"reference": "farmacia-001",
"name": "Farmacia Ejemplo",
"livemode_enabled": false,
"created_at": "2026-08-13T05:08:06.489321+00:00"
},
"created": true,
"api_key": {
"id": "ace71fa8-d642-4dc9-b68e-648bb4183763",
"object": "api_key",
"value": "sk_test_To6kITfivBdX0knMOz0e4qtinBMsg8qesZ2sfIfqcwF",
"prefix": "sk_test_",
"last4": "qcwF"
}
}
api_key.value solo viaja aquí, una vez. Repetir el alta con la misma
reference devuelve 200 con created: false y
api_key: null.
Para operar por la cuenta —facturar, cobrar, guardar un método de pago— añade la cabecera
Winal-Account-Key con la credencial de arriba a cualquier llamada de /v1
que ya uses; la respuesta trae Winal-Account: <id> confirmando sobre quién se
actuó. Ver el ejemplo completo en Cuentas administradas
→ Opera por una cuenta.
Onboarding de sub-comercios
Guía narrativa completa (flujo draft → submit → aprobado, campos y documentos) en Onboarding. Requisito para Winal Connect.
Alta mínima de una solicitud, en draft.
Authorization | requerido |
Cuerpo
legal_name | string, requerido |
person_type | "fisica" | "moral", requerido |
contact_email | string, requerido |
| resto de campos (ver Onboarding) | opcionales al crear; completos exige submit |
Errores posibles: onboarding_application.invalid_legal_name, invalid_person_type, invalid_contact_email.
{
"id": "75deacfb-ff0d-476e-8292-f9471941694a",
"object": "onboarding_application",
"status": "draft",
"legal_name": "Tienda Docs SA de CV",
"person_type": "moral",
"contact_email": "docs@example.mx",
"created_at": "2026-07-08T06:48:56.880311+00:00",
"updated_at": "2026-07-08T06:48:56.880311+00:00",
"documents": []
}
Patch parcial (campos null/omitidos no se tocan), solo mientras la solicitud sea editable (draft/needs_info). Mismo cuerpo que POST, más documents[] (ver Onboarding para los 4 tipos requeridos).
Authorization | requerido |
Errores posibles: onboarding_application.not_found, onboarding_application.transition_conflict, onboarding_application.invalid_document.
{
"id": "75deacfb-...", "object": "onboarding_application", "status": "draft",
"rfc": "TDS900101AB1", "clabe_masked": "**** **** **** 0004",
"documents": [ { "document_type": "ine", "reference": "INE-DOC-001", "reference_hash": "a1b2c3…", "created_at": "..." }, "..." ]
}
Dispara la verificación (síncrona en fase 0): completitud → formato de RFC/CLABE → check de listas (simulado). Resuelve a approved, needs_info o rejected en la misma llamada.
Authorization | requerido |
Errores posibles: onboarding_application.missing_fields, missing_documents, transition_conflict.
{
"id": "75deacfb-...", "object": "onboarding_application", "status": "approved",
"status_reason": "Verificación automática aprobada: RFC y CLABE con formato válido, sin coincidencias en listas.",
"resolved_by": "system:auto_verification",
"submitted_at": "2026-07-08T06:49:17.36Z", "resolved_at": "2026-07-08T06:49:17.36Z"
}
Lista (?status=, ?limit= opcionales; sin documents) o lee una solicitud por id (con documents[]).
Authorization | requerido |
Errores posibles: onboarding_application.not_found.
La resolución manual (aprobar/rechazar/pedir información) es una operación de portal → Onboarding
para operadores de Winal, montada bajo /admin/tenants/{tenantId}/onboarding/applications/... —
no se documenta aquí como endpoint de tu integración.
Winal Connect
Guía narrativa completa (el modelo, las dos formas de marcar un split, la dispersión) en Winal Connect.
Liga un sub-comercio con una solicitud de Onboarding ya approved.
Authorization | requerido |
Cuerpo
onboarding_application_id | string (uuid), requerido |
connector_key | string, opcional (default "stp"; usa "sim" en pruebas) |
Errores posibles: connect.invalid_application_id, connect.application_not_found, connect.application_not_approved, connect.account_conflict.
{
"id": "0d0de738-0133-4625-97e2-9c285ae20f9e",
"object": "connect_account",
"onboarding_application_id": "0732573a-5567-4b2a-87b6-8721654c5060",
"settlement_clabe_masked": "**** **** **** 0004",
"sub_merchant_name": "Sub Comercio Sim SA de CV",
"status": "active",
"connector_key": "sim",
"livemode": false,
"created_at": "2026-07-08T06:49:58.292982+00:00"
}
Lista (?limit=) o lee una cuenta Connect. La CLABE del sub nunca se expone completa.
Authorization | requerido |
Errores posibles: connect.account_not_found.
Split manual post-cobro (multi sub-comercio). application_fee_minor + Σ splits[].amount_minor debe ser exactamente charge_amount_minor.
Authorization | requerido |
Idempotency-Key | requerido (UUID) — compromete dinero |
Cuerpo
payment_intent_id | string (uuid), requerido |
charge_amount_minor | int64, requerido, > 0 |
application_fee_minor | int64, requerido (puede ser 0) |
currency | ISO 4217, requerido |
splits | [{connect_account_id, amount_minor}], al menos uno |
Errores posibles: connect.invalid_charge, connect.missing_allocations, connect.invalid_allocation, connect.split_mismatch, connect.account_not_found.
{
"id": "53fb6fd5-92c8-4e13-93a1-f01aefb21ce8",
"object": "connect_transfer",
"payment_intent_id": "aaab3de3-3cfe-4d55-9bbd-24a89977abf7",
"charge_amount_minor": 100000,
"application_fee_minor": 10000,
"currency": "MXN",
"status": "split",
"created_at": "2026-07-08T06:50:09.078051+00:00",
"splits": [
{ "id": "09702eba-...", "connect_account_id": "0d0de738-...", "amount_minor": 90000, "status": "dispersing", "payout_id": "fa888dfc-..." }
]
}
Lista (?limit=) o lee un transfer, con sus splits[].
Authorization | requerido |
Errores posibles: connect.transfer_not_found.
Payouts
Guía narrativa completa (la máquina de estados, STP vs. Sim) en Payouts.
Ordena una dispersión SPEI a una CLABE. Sin custodia (ADR-0001): sale de la cuenta del propio comercio.
Authorization | requerido |
Idempotency-Key | requerido (UUID) |
Cuerpo
clabe | string, 18 dígitos con dígito de control válido, requerido |
beneficiary_name | string, requerido |
beneficiary_rfc | string, opcional |
amount_minor | int64, requerido, > 0 |
currency | ISO 4217, requerido (solo MXN) |
concepto | string, requerido |
reference | string, opcional (se genera si falta) |
connector_key | opcional (default "stp"; usa "sim" en pruebas) |
Errores posibles: payout.missing_fields, payout.invalid_amount, payout.invalid_clabe, payout.unsupported_currency.
{
"id": "b4e5115a-e401-48b6-9b4d-e80b7a915748",
"object": "payout",
"clabe": "646180157000000004",
"beneficiary_name": "Proveedor Docs SA de CV",
"amount_minor": 250000,
"currency": "MXN",
"concepto": "pago de prueba docs",
"reference": "2441786",
"status": "processing",
"connector_key": "sim",
"livemode": false,
"created_at": "2026-07-08T06:46:43.157657+00:00"
}
Lista (?limit=, default 100, máx. 500) o lee un payout — provider_ref/tracking_key/failure_reason aparecen cuando el Worker ya ejecutó la orden.
Authorization | requerido |
Errores posibles: payout.not_found.
Billers (pago de servicios)
Guía narrativa completa en Pago de servicios.
Catálogo global (sin variación por tenant). ?category= opcional.
Authorization | requerido |
{ "object": "list", "data": [
{ "object": "biller", "code": "cfe", "name": "CFE (Comisión Federal de Electricidad)",
"category": "luz", "reference_label": "Número de servicio (10 a 12 dígitos)", "active": true },
"... (agua_cdmx, telcel_recarga, telmex, izzi)"
] }
Consulta de adeudo — NO mueve dinero, no exige Idempotency-Key.
Authorization | requerido |
Cuerpo: { "reference": "string, requerido" }
Errores posibles: biller.missing_reference, biller.invalid_code, biller.not_found, biller.invalid_reference, biller.reference_not_found, biller.livemode_unsupported.
⚠️ amount_due_minor es SIMULADO hoy (hash determinista de reference, sin llamada real al biller) y esta ruta de consulta está bloqueada en producción (sk_live_ recibe 400 biller.livemode_unsupported, no un adeudo): el simulador no puede devolver una cifra real, y devolverla igual sería tan peligroso como fabricar el pago mismo — ver Pago de servicios.
{
"object": "biller_inquiry", "biller_code": "cfe", "biller_name": "CFE (Comisión Federal de Electricidad)",
"reference": "1234567890", "amount_due_minor": 118700, "currency": "MXN",
"service_holder_name": "Cliente simulado (ref. 1234567890)", "due_date": "2026-07-18T06:47:41.85Z"
}
Confirma el pago del servicio ante el biller.
Authorization | requerido |
Idempotency-Key | requerido (UUID) — validado por este endpoint mismo |
Cuerpo
biller_code | string, requerido |
reference | string, requerido |
amount_minor | int64, opcional — si viene, debe igualar el adeudo vigente |
payment_intent_id | uuid, opcional — solo correlación, sin FK real |
Errores posibles: service_payment.missing_fields, service_payment.amount_mismatch, service_payment.idempotency_conflict, service_payment.livemode_unsupported (llave sk_live_ sin agregador real conectado).
{
"id": "3f490a8e-630e-4a4b-ba4e-81be735c0c14", "object": "service_payment",
"biller_code": "cfe", "reference": "1234567890", "amount_minor": 118700, "currency": "MXN",
"status": "paid", "provider_ref": "SIMBILL-7188545cf02c4532b9681371fbe806b3",
"created_at": "2026-07-08T06:47:46.86Z", "updated_at": "2026-07-08T06:47:46.86Z"
}
{ "id": "...", "object": "service_payment", "status": "failed",
"failure_reason": "El biller 'Telmex' rechazó la confirmación del pago para la referencia '...' (simulado)." }
Lista o lee un pago de servicio.
Authorization | requerido |
Query (solo en el listado)
limit | opcional, default 100, tope 500 |
from | opcional, ISO-8601. Inicio del período, inclusive |
to | opcional, ISO-8601. Fin del período, exclusivo |
cursor | opcional, opaco. El next_cursor de la página anterior |
Devuelve has_more y next_cursor junto a data, y cada pago
trae confirm_requested_at: cuándo se pidió la confirmación al biller (null
si nunca se llegó a pedir).
Errores posibles: service_payment.not_found,
service_payment.invalid_period, service_payment.invalid_cursor.
Recargas de tiempo aire
Guía narrativa completa en Recargas de tiempo aire.
Catálogo de operadoras (Telcel, AT&T, CFE…) para tu tenant, con la regla de validación de referencia de cada una y su disponibilidad reciente.
Authorization | requerido |
{ "object": "list", "data": [
{ "object": "recharge_carrier", "code": "telcel", "name": "Telcel", "active": true,
"reference": { "label": "Celular a 10 dígitos", "min_length": 10, "max_length": 10,
"format": "numeric", "allows_leading_zero": false },
"category": "Tiempo Aire", "availability": "ok", "logo_url": null }
] }
Activa o desactiva una operadora para tu tenant (por default TODAS están activas; solo hace falta llamarlo para apagar una).
Authorization | requerido |
Cuerpo: { "active": "bool, opcional (default true)" }
Errores posibles: recharge.invalid_carrier, recharge.carrier_not_found.
Catálogo de productos (montos/paquetes/servicios) por tenant, con el nombre y categoría de su operadora ya resueltos.
Authorization | requerido |
{ "object": "list", "data": [
{ "object": "recharge_product", "code": "telcel_100", "carrier_code": "telcel", "kind": "airtime",
"name": "Telcel $100", "face_amount_minor": 10000, "merchant_commission_minor": 300,
"currency": "MXN", "active": true, "open_amount": false,
"carrier_name": "Telcel", "carrier_category": "Tiempo Aire", "carrier_logo_url": null },
{ "object": "recharge_product", "code": "cfe_recibo", "carrier_code": "cfe", "kind": "service",
"name": "CFE — recibo de luz", "face_amount_minor": 0, "merchant_commission_minor": 0,
"currency": "MXN", "active": true, "open_amount": true,
"carrier_name": "CFE", "carrier_category": "Servicios", "carrier_logo_url": null }
] }
Ejecuta una recarga o pago vía recarga. Irreversible una vez entregada — ver El desenlace.
Authorization | requerido, scope recharges:write (ver Recargas → El permiso que exige una recarga) |
Idempotency-Key | requerido (UUID) — validado por este endpoint mismo |
Cuerpo
product_code | string, requerido |
reference | string — destino moderno (celular o número de servicio). Gana sobre phone_number si mandas los dos. |
phone_number | string — alias histórico de reference, se conserva por compatibilidad |
amount_minor | int64, opcional — SOLO para productos de monto libre (open_amount: true); en denominación fija se rechaza si lo mandas |
payment_intent_id | uuid, opcional — solo correlación, sin FK real |
Errores posibles: recharge.missing_fields, recharge.invalid_phone, recharge.invalid_reference, recharge.carrier_not_enabled, recharge.carrier_not_found, recharge.livemode_unsupported, recharge.product_not_found, recharge.product_not_supported_by_provider, recharge.catalog_not_synced, recharge.amount_required, recharge.amount_not_allowed, recharge.idempotency_conflict, recharge.payment_intent_not_found, recharge.payment_intent_not_succeeded, recharge.payment_intent_amount_mismatch, recharge.allowance_exhausted.
{
"id": "029fc23d-bef6-477f-a969-8ad8f9c15f6e", "object": "recharge",
"carrier_code": "telcel", "product_code": "telcel_100", "phone_masked": "•••• •••890",
"face_amount_minor": 10000, "merchant_commission_minor": 300, "currency": "MXN",
"status": "delivered", "provider_ref": "TAECEL-...", "payment_intent_id": null,
"failure_reason": null, "created_at": "2026-08-20T18:03:11Z", "updated_at": "2026-08-20T18:03:12Z",
"outcome": "delivered", "settled": true, "safe_to_retry": false, "needs_review": false,
"provider_cost_minor": 9700, "client_reference": "winal029fc23dbef6477fa9698ad8f9c15f6e",
"provider_transaction_id": "1183920", "connector_key": "taecel", "livemode": true,
"requested_at": "2026-08-20T18:03:11Z",
"attempt_count": 1,
"attempts": [
{ "id": "029fc23d-bef6-477f-a969-8ad8f9c15f6e", "attempt_no": 1, "connector_key": "taecel",
"status": "delivered", "provider_ref": "TAECEL-...", "provider_transaction_id": "1183920",
"provider_cost_minor": 9700, "client_reference": "winal029fc23dbef6477fa9698ad8f9c15f6e",
"requested_at": "2026-08-20T18:03:11Z", "resolved_at": "2026-08-20T18:03:12Z",
"routing": { "reason": "'taecel' para el intento 1: 'taecel' primero: sano; saldo suficiente: $6,430.00 disponibles para $97.00", "excluded": [],
"evaluated": [ { "provider_key": "taecel", "eligible": true, "reason": "sano; saldo suficiente: $6,430.00 disponibles para $97.00" } ] } }
]
}
{ "...": "...", "status": "processing", "outcome": "undetermined", "settled": false, "safe_to_retry": false }
{ "...": "...", "status": "failed", "failure_code": "absent_from_report",
"outcome": "undetermined", "settled": false, "safe_to_retry": false, "needs_review": true,
"failure_reason": "La solicitud no aparece en el reporte de ventas del proveedor para la ventana en que se pidió, así que lo más probable es que no llegara a registrarse y no hubiera cargo — pero no está probado: ... comprueba en el portal del agregador que no aparezca y, solo entonces, vuelve a pedirla con una llave de idempotencia nueva. Mientras tanto Winal sigue buscándola en los reportes de los días siguientes; si aparece, esta misma operación cambia sola." }
{ "...": "...", "status": "failed", "failure_code": "exhausted", "outcome": "failed", "settled": true, "safe_to_retry": true, "needs_review": false,
"failure_reason": "El único proveedor disponible ('taecel') no entregó la recarga. Último motivo: ... Corrige lo que indique el motivo y vuelve a intentarla con una llave de idempotencia nueva.",
"attempt_count": 1, "attempts": [ { "attempt_no": 1, "connector_key": "taecel", "status": "failed", "failure_code": "rejected", "...": "..." } ] }
Última foto conocida (no en vivo) del saldo prefondeado de la bolsa que consume tu cuenta. Nunca es base contable — solo para saber si alcanza para la siguiente venta.
Authorization | requerido |
uses_own_credentials es true cuando la bolsa es de la cuenta (sus credenciales,
o una bolsa de la que es dueña) y false cuando es la de quien la administra. El bloque
refresh —solo cuando la bolsa se puede leer al momento— dice qué pasó con la última lectura
pedida con POST /v1/recharge-balance/refresh y desde cuándo una
nueva llega al agregador (next_available_at).
{
"object": "recharge_balance", "provider_key": "taecel", "livemode": true,
"uses_own_credentials": true, "low": false,
"observed_at": "2026-08-20T18:00:00Z",
"pouches": [
{ "pouch_id": "1", "name": "Saldo", "balance_minor": 4500000, "currency": "MXN",
"observed_at": "2026-08-20T18:00:00Z" }
]
}
Lista o lee una recarga. Es tu ÚNICA forma de saber si una recarga processing ya se resolvió — ver Cómo entrega Winal una recarga.
Authorization | requerido |
Query (solo en el listado)
limit | opcional, default 100, tope 500 |
from | opcional, ISO-8601. Inicio del período, inclusive |
to | opcional, ISO-8601. Fin del período, exclusivo |
cursor | opcional, opaco. El next_cursor de la página anterior |
connector_key | opcional, un agregador. Solo los pedidos que ese agregador entregó o, sin entrega, fue el último en intentar (la misma definición de connector_key en cada recarga). Desconocido → 400 recharge.connector_key_unknown |
El listado devuelve has_more y next_cursor junto a data.
Sin fechas ni cursor se comporta como siempre. Cada recarga trae además
provider_cost_minor (el costo REAL descontado, puede ser null),
client_reference, provider_transaction_id, connector_key,
livemode y requested_at — los del intento que entregó, o del último.
Una recarga es un pedido con uno o más intentos contra un agregador
(Cuando un agregador falla). Campos del pedido:
attempt_count; failure_code (solo con status: "failed",
catálogo cerrado de seis valores: not_requested, rejected,
failed, absent_from_report, exhausted,
unfunded); needs_review (true SOLO con
absent_from_report: ahí outcome es undetermined y
safe_to_retry es false — ver
Cuando un agregador falla); y attempts[], uno por intento y en orden, cada uno con
id, attempt_no, connector_key, status,
provider_ref, provider_transaction_id, provider_cost_minor,
client_reference, requested_at, resolved_at,
failure_code, failure_reason y routing
(reason legible; excluded: los agregadores no considerados por haberse
intentado ya; y evaluated[]: cada agregador evaluado con provider_key,
eligible y su reason —salud, saldo, prioridad o costo—, ver
Cómo elige Winal el agregador). El id del intento 1 es el del pedido; los siguientes tienen el suyo y
no resuelven por GET /v1/recharges/{id}.
Errores posibles: recharge.not_found, recharge.invalid_period,
recharge.invalid_cursor, recharge.connector_key_unknown.
Estado de cuenta del período de la cuenta EFECTIVA: entregado con su costo real, en vuelo,
rechazado, pagos de servicio, abonos recibidos, cupo y —solo si la cuenta es DUEÑA de la bolsa— su
saldo. La variante .csv devuelve el mismo período con una línea por movimiento.
Los totales y las líneas son por intento contra un agregador (el costo real es del
intento): un pedido surtido por respaldo suma una rechazada y una entregada, y en el CSV son dos
líneas con el mismo pedido_id — las dos últimas columnas, pedido_id e
intento_no, van al final para no mover ninguna de las anteriores.
No incluye un «saldo de la cuenta»: el dinero vive en el agregador a nombre de quien fondeó la bolsa (Winal no custodia fondos) y una bolsa se comparte entre las cuentas que su dueño apuntó a ella. Lo de una cuenta administrada son sus movimientos y su cupo. Ver Recargas de tiempo aire.
Authorization | requerido |
Query
from | opcional, ISO-8601, inclusive. Por omisión, 30 días atrás |
to | opcional, ISO-8601, exclusivo. Por omisión, ahora |
pouch_id | opcional, bolsa del agregador. Por omisión 1 (tiempo aire). Solo en la variante JSON: un movimiento del CSV no pertenece a una bolsa del agregador, así que statement.csv lo rechaza en vez de ignorarlo |
connector_key | opcional, un agregador (taecel; sim en pruebas). Acota TODO el documento a ese agregador: recargas, abonos, cupo y bolsa. Desconocido → 400 recharge.connector_key_unknown, nunca ceros. En el CSV acotado van solo las líneas de recarga de ese agregador (los pagos de servicio no guardan agregador) |
Por agregador. Sin filtro, la respuesta trae by_connector[]: el mismo período por
agregador (connector_key, charges, in_flight, failed,
deposits y allowance), y cada cifra del total es la suma de la misma cifra en
los agregadores. Con filtro, connector_key aparece en la respuesta como eco (sin filtro no
aparece) y by_connector tiene una sola entrada. Los pagos de servicio no se acotan y su
note lo dice. Ver Cuadrar por agregador.
Errores posibles: recharge.invalid_period, recharge.connector_key_unknown,
y en el CSV recharge.pouch_id_not_applicable si mandas pouch_id.
El HISTORIAL de movimientos de tu bolsa —ventas, cargos y ABONOS (depósitos)— tal como los
reportó el agregador, cacheado por Winal. Sirve para conciliar un comprobante de depósito contra
lo que el agregador dice haber recibido; no sustituye /statement, que sigue
siendo la única prueba de que el TOTAL entró — esto es el detalle día a día para encontrar UN
movimiento concreto. Ver Recargas de tiempo aire → Conciliar un
depósito.
Solo tu bolsa PROPIA. Si tu cuenta consume la bolsa de quien te administra, esta ruta
devuelve una página vacía: el detalle crudo de una bolsa compartida mezcla el consumo de todas
las cuentas que la apuntan (y movimientos ajenos a tiempo aire, como Timbres CFDI), así que solo
su dueño lo ve. Sigues viendo lo tuyo en /statement.
Vigencia declarada, nunca en vivo. Lo que devuelve esta ruta es lo que un ciclo periódico
sincronizó del agregador (cada ~15 minutos, hoy y ayer); synced_through dice hasta
cuándo se sabe con certeza que no faltan filas y coverage_from desde cuándo hay
historial. Un array vacío con coverage_from: null significa "todavía no se ha
sincronizado", no "no hubo movimientos". Y truncated_at con fecha significa que ese
día se leyó A MEDIAS (el reporte del agregador viene paginado y se alcanzó el tope de páginas):
el listado está incompleto aunque synced_through tenga valor, así que la
ausencia de un movimiento no prueba nada. Los tres campos juntos son los que permiten distinguir
"no lo hubo" de "no lo sabemos todavía".
Authorization | requerido |
Query
from | opcional, ISO-8601, inclusive, sobre registered_at |
to | opcional, ISO-8601, exclusivo |
limit | opcional, default 100, tope 500 |
Errores posibles: recharge.invalid_period.
{
"object": "list",
"data": [
{ "movement_id": "404", "registered_at": "2023-02-28T12:06:11-06:00",
"pouch_name": "Timbres CFDI", "movement_type": "Abono", "is_deposit": true,
"amount_minor": 12000, "currency": "MXN", "raw_amount": "120.0000",
"units": "10.0000", "additional_folio": "trxwcsxf50uvfpznynzn",
"status": "Aplicado", "via": "Openpay" }
],
"synced_through": "2026-08-20T18:00:07Z",
"coverage_from": "2026-08-05T00:00:00Z",
"truncated_at": null
}
Traspasos de saldo entre bolsas
Mueve saldo YA prefondeado de una bolsa tuya a otra del mismo comercio en el agregador — normalmente de tu bolsa central a la que consume una cuenta administrada. Resuelve el caso de un integrador con decenas de farmacias, cada una con su propia cuenta en el agregador: en vez de un depósito bancario por cada una, depositas una vez a tu cuenta central y repartes desde tu sistema. Ver Recargas de tiempo aire → Traspasar saldo entre tus bolsas.
requesting y processing NO son un fracaso. El agregador no
reversa un traspaso aplicado, así que un timeout o una respuesta inconclusa se quedan
INDETERMINADOS hasta que haya evidencia (regla 5) — el saldo pudo haberse movido ya.
No vuelvas a ordenarlo: consulta GET /v1/recharge-transfers/{id} hasta ver un
estado terminal (settled o failed), o reintenta con la MISMA
Idempotency-Key — nunca pide un traspaso nuevo, siempre devuelve el mismo que ya
existía.
Ordena un traspaso. Irreversible una vez aplicado —el agregador no lo cancela ni lo
reversa— y no delegable: no admite Winal-Account-Key (ordenar un traspaso
mueve tu propia tesorería, no la de una cuenta que administras) y si la mandas responde
403 account.delegation_not_allowed.
Authorization | requerido, scope recharges:write (el mismo que vender: ver Recargas → El permiso que exige una recarga) |
Idempotency-Key | requerido (UUID) — validado por este endpoint mismo. Sin ella, un reintento ordenaría el traspaso otra vez |
Cuerpo
destination_account | uuid, requerido — la cuenta ADMINISTRADA que se fondea (el id que devolvió POST /v1/accounts). Nunca la cuenta del agregador: Winal la resuelve del vínculo de esa cuenta con su bolsa —una bolsa tuya a su nombre, o su cuenta propia en el agregador, cuyo número de cuenta viene del alta—, así que no hay forma de nombrar la cuenta de un desconocido |
amount_minor | int64, requerido — centavos, MXN |
pouch_id | string, requerido, sin valor por omisión — "1" Tiempo Aire o "2" Pago de Servicios. El agregador no mezcla el saldo de sus bolsillos: un traspaso al equivocado deja el dinero donde no se puede gastar |
note | string, requerido, 3–200 caracteres — el agregador la exige y la muestra en su portal |
source_id | uuid, opcional — bolsa origen; por omisión, la que tu propia cuenta consume |
Errores posibles: idempotency_key_required, recharge_transfer.missing_fields, recharge_transfer.idempotency_key_required, recharge_transfer.invalid_amount, recharge_transfer.invalid_note, recharge_transfer.invalid_pouch, recharge_transfer.livemode_unsupported, recharge_transfer.idempotency_conflict, recharge_transfer.source_not_found, recharge_transfer.destination_not_managed, recharge_transfer.destination_not_found, recharge_transfer.same_bag, recharge_transfer.destination_account_ref_missing, recharge_transfer.insufficient_bag_balance, recharge_transfer.provider_unavailable, recharge_transfer.provider_rejected, recharge_transfer.not_found (defensivo: es la guarda interna del servicio, en la práctica no se alcanza por esta ruta), y account.delegation_not_allowed si mandas Winal-Account-Key.
{
"object": "recharge_transfer", "id": "01936b8a-1c2d-7e3f-9a4b-5c6d7e8f9a0b",
"status": "settled", "livemode": true,
"destination_account": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"source_id": "8c21f8e0-...", "destination_source_id": "9d02aa10-...",
"pouch_id": "1", "amount_minor": 500000, "currency": "MXN",
"note": "Fondeo semanal farmacia Centro", "folio": "182",
"provider_movement_id": "40123", "provider": "taecel",
"failure_code": null, "failure_reason": null,
"created_at": "2026-09-10T18:00:00Z", "settled_at": "2026-09-10T18:00:04Z"
}
{ "...": "...", "status": "requesting", "provider_movement_id": null, "settled_at": null }
Lista o lee un traspaso, acotado al ambiente de la llave que pregunta. Es tu única forma de
saber si uno en requesting o processing ya se resolvió — no lo
reordenes mientras esperas.
Authorization | requerido |
Query (solo en el listado)
limit | opcional, default 100, tope 500 |
Errores posibles: recharge_transfer.not_found.
{
"object": "recharge_transfer", "id": "01936b8a-...", "status": "requesting",
"livemode": true, "destination_account": "3fa85f64-...",
"provider_movement_id": null, "failure_code": null, "failure_reason": null,
"created_at": "2026-09-10T18:00:00Z", "settled_at": null
}
La cuenta propia de cada cliente en el agregador
Cada cuenta que administras puede tener su propia cuenta en el agregador de recargas (TAECEL:
RegistroCuenta): su referencia de depósito, su formulario para reportar un depósito y su
saldo. El alta se pide desde tu consola —con contraseña y segundo factor, nunca por API— y lo que
tu punto de venta necesita para enseñárselo a cada farmacia se lee aquí. Ver
Recargas → La cuenta propia de cada farmacia.
Las cuentas propias de la cuenta efectiva en cada agregador: con tu llave sola, las tuyas;
actuando sobre un cliente (Winal-Account-Key), las de ese cliente — nunca las de otro.
No lleva parámetros: devuelve una por agregador, vivas (en curso, sin desenlace, esperando
credenciales o dadas de alta). Solo lectura: dar de alta la cuenta vive en la consola.
Authorization | requerido (lectura: payments:read) |
Winal-Account-Key | opcional — la credencial del cliente sobre el que actúas |
Campos de cada elemento
status | pending/requesting (dándose de alta), indeterminate (sin desenlace: la cuenta pudo crearse; lo resuelve su integrador desde la consola), awaiting_credentials o created. Solo en created hay referencia y enlace. |
deposit_reference | La referencia con la que esta cuenta deposita: la que se escribe en la ficha para que el abono llegue a su cuenta. |
report_url | El formulario del agregador donde se reporta un depósito (se sube el comprobante). El agregador lo publica como incrustable en cualquier página o aplicación; siempre https de su dominio. |
provider_account_id | Su número de cuenta en el agregador (cuentaID): el destino de un traspaso. |
airtime_reference / services_reference | Referencias por bolsillo, cuando el agregador las reportó. Las coordenadas bancarias completas (CLABE, cuenta) no viajan por aquí: viven en la consola, con sesión. |
sells_from_it | Si esta cuenta vende de su cuenta propia (y no de la bolsa compartida de quien la administra). |
Nunca viajan credenciales ni los datos del titular.
{
"object": "list",
"data": [{
"object": "recharge_subaccount", "provider_key": "taecel", "status": "created",
"provider_account_id": "336", "deposit_reference": "88003365",
"report_url": "https://taecel.com/app/public/ReportarCompra?key=fe961d412d3224e",
"report_link_observed_at": "2026-09-23T18:00:04Z",
"sells_from_it": true,
"created_at": "2026-09-23T18:00:00Z", "resolved_at": "2026-09-23T18:00:04Z"
}]
}
Depósitos: enlace de reporte y saldo al momento
Lo que tu punto de venta necesita alrededor de un depósito de saldo: el enlace del formulario de TAECEL donde
la farmacia sube su comprobante (lo recibe y lo valida TAECEL; Winal no ve el archivo), el
saldo al momento para saber si ya se lo acreditaron, y el aviso recharge.deposit.detected.
Las tres rutas actúan sobre la cuenta efectiva (con Winal-Account-Key, la farmacia).
Por cada bolsa que la cuenta efectiva posee en un agregador con formulario de reporte (su cuenta propia, o la cuenta madre del integrador), su referencia de depósito y el enlace del formulario, con la hora en que se leyó. Nunca el de una bolsa ajena: una farmacia que vende de la bolsa de su integrador recibe una lista vacía. No le pregunta nada al agregador. Sin parámetros.
Authorization | requerido (lectura: payments:read) |
Winal-Account-Key | opcional — la credencial de la farmacia sobre la que actúas |
Campos de cada elemento
source_id / source_label | La bolsa de la cuenta. |
is_subaccount | true si es la cuenta propia que Winal dio de alta para esa cuenta. |
deposit_reference | La referencia con la que esa cuenta deposita, si el agregador la dio. |
report_url / observed_at | El formulario (https del dominio del agregador) y cuándo se leyó. Se omiten mientras no se haya leído: pídelo con el POST de abajo. |
refresh | Estado de su lectura al momento: status (idle, pending, refreshed, unreachable, rejected, not_requested, expired), requested_at, completed_at, detail, next_available_at, cooldown_seconds (300, la ventana de REINTENTO del enlace; la de frescura son 24 h y ya las refleja next_available_at) y triggered. |
{
"object": "list",
"data": [{
"object": "recharge_report_link", "provider_key": "taecel",
"source_id": "01a0d1bd-5032-7869-a117-7a0e4e3c2a70",
"source_label": "Cuenta propia en taecel (267936)", "is_subaccount": true,
"deposit_reference": "88000142",
"report_url": "https://taecel.com/app/public/ReportarCompra?key=01a0d1bd50327869a1177a0e4e3c2a70",
"observed_at": "2026-09-24T04:47:20.386896+00:00",
"refresh": { "status": "refreshed", "requested_at": "2026-09-24T04:47:20.343922+00:00",
"completed_at": "2026-09-24T04:47:20.391424+00:00",
"next_available_at": "2026-09-25T04:47:20.386896+00:00",
"cooldown_seconds": 300, "triggered": false }
}]
}
Vuelve a leer el enlace del agregador (TAECEL: urlReporteCompra) con las credenciales de
cada bolsa propia de la cuenta, o de una sola. Solo con llave de PRODUCCIÓN: pedirlo con
sk_test_ llamaría al agregador con las credenciales REALES de la bolsa, así que se
rechaza antes de tocar nada. Detrás del freno del enlace —DOS ventanas: 24 horas de frescura
desde la última lectura EXITOSA, y 5 minutos de reintento desde el último intento, tenga o no
éxito— dentro de él contesta con el que ya hay, sin llamar a nadie. Espera hasta wait_ms
al desenlace: 200 si ya no hay nada en vuelo, 202 si alguna lectura sigue en
camino (mira el GET, que no exige ambiente). Sin Idempotency-Key: repetirla
no duplica nada.
Authorization | requerido (recharges:write), llave sk_live_ |
Winal-Account-Key | opcional — la farmacia sobre la que actúas |
Cuerpo (opcional)
source_id | uuid, opcional — una bolsa de la cuenta; omitido, todas sus bolsas propias. |
wait_ms | int, opcional — cuánto esperar (6 000 por omisión, tope 20 000; 0 = no esperar). |
Errores posibles: recharge.report_link_unavailable, recharge.report_link_source_not_found, recharge.report_link_requires_live_key (llave de prueba).
{
"object": "list",
"data": [{
"object": "recharge_report_link", "provider_key": "taecel",
"source_id": "01a0d1bd-5032-7869-a117-7a0e4e3c2a70",
"source_label": "Cuenta propia en taecel (267936)", "is_subaccount": true,
"refresh": { "status": "pending", "requested_at": "2026-09-24T04:47:20.343922+00:00",
"next_available_at": "2026-09-24T04:52:20.343922+00:00",
"cooldown_seconds": 300, "triggered": true }
}]
}
Lee ahora el saldo de la bolsa de la que vende la cuenta efectiva (TAECEL: getBalance)
—la misma de GET /v1/recharges/balance, y escrita en la misma foto que su monitor—. Freno de
10 minutos por bolsa, contados desde su última lectura de CUALQUIER origen: dentro de él contesta
200 con la última foto y su observed_at, sin llamar al agregador. Fuera, pide la
lectura y espera hasta wait_ms: 200 con la foto fresca, o 202 con la
anterior y refresh.status: "pending". Responde el mismo recurso que
GET /v1/recharges/balance con el bloque refresh.
Authorization | requerido (recharges:write) |
Winal-Account-Key | opcional — la farmacia sobre la que actúas |
Cuerpo (opcional)
wait_ms | int, opcional — cuánto esperar (6 000 por omisión, tope 20 000; 0 = no esperar). |
Errores posibles: recharge.balance_refresh_unavailable (llave de prueba, o ninguna bolsa legible).
{
"object": "recharge_balance", "provider_key": "taecel", "livemode": true,
"uses_own_credentials": true, "low": false,
"observed_at": "2026-09-24T04:35:20.135647+00:00",
"pouches": [
{ "pouch_id": "1", "name": "Tiempo Aire", "balance_minor": 30000, "currency": "MXN",
"observed_at": "2026-09-24T04:35:20.135647+00:00" }
],
"refresh": { "status": "pending", "requested_at": "2026-09-24T04:47:20.455211+00:00",
"next_available_at": "2026-09-24T04:57:20.455211+00:00",
"cooldown_seconds": 600, "triggered": true }
}
Webhook a la cuenta dueña de la bolsa la primera vez que Winal ve un abono nuevo en su
reporte de movimientos (TAECEL: getReports, leído cada ~15 minutos, hoy y ayer). Exactamente
uno por movimiento de cada bolsa; el event_id se deriva del movimiento. Solo abonos, nunca
cargos. Llega en los dos ambientes: comprueba livemode antes de acreditar nada.
data es el movimiento con los mismos campos de
GET /v1/recharges/bag-movements, más account_id, provider_key,
source_id, pouch_id y observed_at (cuándo lo vio Winal).
No está medido que el reporte traiga el abono de un depósito validado desde el formulario: si no
llega, confirma con POST /v1/recharge-balance/refresh (arriba).
{
"event_id": "b540b230-f73d-0e86-ab6d-4574279ba0ea",
"event_type": "recharge.deposit.detected",
"created_at": "2026-09-24T04:47:19.378987+00:00",
"api_version": "2026-07-01", "livemode": true,
"data": {
"object": "recharge_bag_movement",
"account_id": "e7d62ffd-ba10-41eb-a99b-aa36b5249bf3",
"provider_key": "taecel", "source_id": "01a0d1bd-5032-7869-a117-7a0e4e3c2a70",
"movement_id": "404", "registered_at": "2026-09-24T04:44:19.378906+00:00",
"observed_at": "2026-09-24T04:47:19.378987+00:00",
"pouch_id": "1", "pouch_name": "Tiempo Aire", "movement_type": "Abono", "is_deposit": true,
"amount_minor": 50000, "currency": "MXN", "raw_amount": "500.0000", "units": null,
"additional_folio": "88000142", "status": "Aplicado", "via": "Deposito"
}
}
Pago de servicios (recargas)
GET /v1/service-payments comparte exactamente el mismo contrato de listado que
GET /v1/recharges (?limit= default 100 y tope 500, ?from=/
?to= semiabierto y ?cursor=) — ver
Billers (pago de servicios) arriba para su shape completo.
Terminales (card-present)
El alta y administración de terminales SmartPOS (número de serie, modelo, sucursal, estado
active/inactive/lost) es una operación de
portal → Terminales, no un endpoint público de /v1 — tu integración solo necesita
el id de la terminal ya activa, para mandarlo como metadata.terminal_id al
cobrar card_present o CoDi en mostrador. Ver Métodos
de pago → Card-present y CoDi en mostrador.