Referencia de API

MODO PRUEBA

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.

GrupoAuthHeader 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).—
📄 El contrato completo, tipado
Cada endpoint de esta página también vive en el contrato OpenAPI (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ámetroTipoDefault · tope
after_idint64, opcional0 (desde el primer evento); cursor exclusivo
limitint, opcional50 · 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.

200 · página con más resultados
{
  "object": "list",
  "data": [ /* … filas … */ ],
  "has_more": true,
  "next_cursor": "eyJpZCI6MTI4LCJ0cyI6..."
}
ParámetroTipoDefault · tope
cursorstring opaco, opcionalel next_cursor de la página anterior; un cursor corrupto responde reports.invalid_cursor
limitint, opcionalvarí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áficoSe cuenta porLímite por minuto (default)
/v1/* autenticadotu llave sk_…300
/public/*IP de cliente confiable60

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.

429 · respuesta
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."
  }
}
Qué headers hay (y cuáles no)
Ante un 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

POST/v1/payment_intents

Crea un intent en requires_payment_method con un client_secret nuevo.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

amount_minorint64, requerido, > 0
currencystring ISO 4217, requerido (hoy solo MXN funciona con Sim)
payment_method_typesstring[], 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.
metadataobject<string,string>, opcional
tip_minorint64, 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).

request
POST /v1/payment_intents
Authorization: Bearer sk_test_...
Idempotency-Key: 6a1e3b2c-...
Content-Type: application/json

{ "amount_minor": 84900, "currency": "MXN" }
201
{
  "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"
}
GET/v1/payment_intents/{id}

Lee un intent. Con ?expand=attempts incluye el historial de intentos de cobro.

Authorizationrequerido

Errores posibles: payment_intent.not_found.

request
GET /v1/payment_intents/5b6b8b3e-...?expand=attempts
Authorization: Bearer sk_test_...
200
{
  "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"
    }
  ]
}
POST/v1/payment_intents/{id}/confirm

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_tokenstring, requerido (tok_sim_* en pruebas)
payment_methodstring, requerido: card | spei | codi | dimo | oxxo
tip_minorint64, 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.

request
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 }
200 · respuesta inmediata
{
  "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"
    }
  ]
}
POST/v1/payment_intents/{id}/capture

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Errores posibles: attempt.not_capturable (no hay intento authorized).

request
POST /v1/payment_intents/5b6b8b3e-.../capture
Authorization: Bearer sk_test_...
Idempotency-Key: 2c3e9f10-...
200
{
  "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" }
  ]
}
POST/v1/payment_intents/{id}/cancel

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Errores posibles: transición inválida si el intent ya está en un estado terminal distinto (succeeded/failed/expired).

request
POST /v1/payment_intents/5b6b8b3e-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 77aa1c3e-...
200
{
  "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

POST/v1/refunds

Crea una devolución en requested sobre un intento capturado.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

attempt_iduuid, requerido — el intento a devolver (no el payment_intent).
amount_minorint64, opcional. Si se omite, devuelve el saldo restante (cobrado menos devoluciones vivas) — no el monto original del cargo.
reasonstring, opcional

Errores posibles: ver la tabla de refunds en Errores.

request
POST /v1/refunds
Authorization: Bearer sk_test_...
Idempotency-Key: f1e2d3c4-...
Content-Type: application/json

{ "attempt_id": "a13fce02-...", "amount_minor": 84900, "reason": "devolución POS" }
201
{
  "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"
}
GET/v1/refunds/{id}

Lee una devolución por su id.

Authorizationrequerido

Errores posibles: refund.not_found.

request
GET /v1/refunds/d4c5b6a7-...
Authorization: Bearer sk_test_...
200
{
  "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.

POST/v1/customers

Crea un cliente. Cuerpo vacío permitido.

Authorizationrequerido

Cuerpo

namestring, opcional
emailstring, opcional
metadataobject<string,string>, opcional
201 · respuesta real
{
  "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"
}
GET/v1/customers · GET/v1/customers/{id}

Lista o lee un cliente por su id.

Authorizationrequerido

Errores posibles: customer.not_found.

200
{ "object": "list", "data": [ { "id": "7c17884b-...", "object": "customer", "...": "..." } ] }
POST/v1/customers/{id}/payment_methods

Guarda un método de pago tokenizado. Nace pending; el Worker lo activa fuera del request.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_tokenstring, requerido — token de un solo uso (tok_sim_* en pruebas)
connectorstring, opcional

Errores posibles: payment_method.invalid_token, payment_method.no_route, customer.not_found.

201 · respuesta real (pending)
{
  "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"
}
200 · GET /v1/payment_methods/{id}, segundos después (active)
{
  "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"
}
GET/v1/customers/{id}/payment_methods

Lista los métodos guardados del cliente (incluye detached).

Authorizationrequerido
DELETE/v1/payment_methods/{id}

Transición terminal a detached. Idempotente: repetir sobre uno ya detached no falla.

Authorizationrequerido

Errores posibles: payment_method.not_found.

200 · respuesta real
{
  "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

POST/v1/webhook_endpoints

Registra un endpoint para recibir entregas. El secret solo se devuelve aquí.

Authorizationrequerido
Nota: a diferencia de los endpoints anteriores, esta ruta no pasa por el middleware de idempotencia — Idempotency-Key es opcional aquí y, si lo mandas, se ignora.

Cuerpo

urlstring HTTPS, requerido
eventsstring[], opcional — lista blanca de event_type; vacío/omitido = todo el catálogo (ver Webhooks)
request
POST /v1/webhook_endpoints
Authorization: Bearer sk_test_...
Content-Type: application/json

{ "url": "https://tu-servidor.mx/webhooks/winal" }
201
{
  "id": "c1a9f2e0-...",
  "object": "webhook_endpoint",
  "url": "https://tu-servidor.mx/webhooks/winal",
  "secret": "whsec_8Kx9..."
}
GET/v1/webhook_endpoints

Lista los endpoints del tenant. Nunca incluye secretos.

Authorizationrequerido
200
{
  "object": "list",
  "data": [
    {
      "id": "c1a9f2e0-...",
      "object": "webhook_endpoint",
      "url": "https://tu-servidor.mx/webhooks/winal",
      "active": true,
      "created_at": "2026-07-05T18:00:00Z"
    }
  ]
}
DELETE/v1/webhook_endpoints/{id}

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.

Authorizationrequerido

Errores posibles: webhook_endpoint.not_found.

200
{ "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).

POST/public/payment_intents/{id}/confirm

Cuerpo

client_secretstring, requerido
payment_tokenstring, requerido
payment_methodstring, requerido
tip_minorint64, 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).

request
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
}
200 · proyección pública
{
  "id": "5b6b8b3e-1e4b-4c8a-9b2e-6f6a2c9d41a2",
  "object": "payment_intent",
  "amount_minor": 84900,
  "tip_minor": 3000,
  "total_minor": 87900,
  "currency": "MXN",
  "status": "processing"
}
GET/public/payment_intents/{id}

Query

client_secretrequerido

Usado por el polling interno de winal.js cada 2 s hasta un estado terminal.

request
GET /public/payment_intents/5b6b8b3e-...
X-Winal-Client-Secret: pi_secret_9fZ3kQ7bV1x...
200 · con next_action (SPEI)
{
  "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.

GET/v1/events

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.

Authorizationrequerido

Query

after_idint64, opcional (default 0) — cursor exclusivo: devuelve id > after_id.
limitint, opcional (default 50, tope 200; valores mayores se recortan).
request
GET /v1/events?after_id=0&limit=50
Authorization: Bearer sk_test_...
200 · respuesta real
{
  "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.

POST/v1/invoices

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

payment_intent_iduuid, requerido
receptorobjeto requerido: rfc, nombre, uso_cfdi, regimen_fiscal, cp (todos requeridos)
conceptosarreglo 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
seriestring opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio. Si se omite, la serie_default del perfil
foliostring 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).

request
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"
}
201 · CFDI timbrado
{
  "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": "..."
}
POST/v1/invoices/pue

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

total_minorint64, requerido, > 0 (incluye IVA)
currencystring ISO 4217, opcional (default MXN)
receptorobjeto requerido, mismos 5 campos que en POST /v1/invoices
forma_pagostring 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í
descripcionstring, opcional
conceptosarreglo 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)
seriestring opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio
foliostring 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).

request
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"
}
201 · CFDI timbrado
{
  "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": "..."
}
POST/v1/invoices/ppd

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

total_minorint64, requerido, > 0 (incluye IVA)
currencystring ISO 4217, opcional (default MXN)
receptorobjeto requerido, mismos 5 campos que en POST /v1/invoices
descripcionstring, opcional
conceptosarreglo 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)
seriestring opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio
foliostring 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).

request
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"
}
201 · respuesta real
{
  "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"
}
POST/v1/invoices/global

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

periodicidadstring requerido — c_Periodicidad: 01 diario, 02 semanal, 03 quincenal, 04 mensual, 05 bimestral (exclusiva del RIF, régimen 621)
mesesstring requerido — c_Meses: 01–12 meses naturales; 13–18 bimestres, solo con periodicidad 05
anioint32 requerido — año en curso o el inmediato anterior
forma_pagostring requerido — c_FormaPago de la operación de mayor monto del período. 99 no se acepta
conceptosarreglo 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)
currencystring ISO 4217, opcional (default MXN)
sustituye_uuidstring, opcional — uuid_fiscal de la global que ésta corrige (relación 04); omítelo en una emisión normal
seriestring opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio. Una global suele llevar su propia serie
foliostring 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).

request
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"
}
201 · CFDI global timbrado
{
  "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 }
}
POST/v1/invoices/{id}/payments

Registra un pago sobre una factura PPD ya stamped y timbra su complemento de pagos 2.0 (REP).

Authorizationrequerido
Idempotency-Keyrequerido (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_iduuid — 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_minorint, requerido sin payment_intent_id — importe recibido en centavos (incluye IVA); positivo y no mayor que el saldo insoluto
forma_pagostring, 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_pagotimestamp 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
currencystring opcional, MXN por defecto — debe coincidir con la de la factura
seriestring opcional, máx. 25, sin | — serie PROPIA de este REP (no la de la factura que liquida) — ver Facturación CFDI → Serie y folio
foliostring 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.

request
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"
}
201 · shape esperado
{
  "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": "..."
}
POST/v1/invoices/{id}/payments/{pid}/retry

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.

Authorizationrequerido

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.

request
POST /v1/invoices/5cee05bd-.../payments/8f2a1c7e-.../retry
Authorization: Bearer sk_test_...
GET/v1/invoices/{id}/payments

Lista los REP timbrados contra una factura PPD.

Authorizationrequerido
200 · respuesta real (factura sin pagos aún)
{ "object": "list", "data": [] }
GET/v1/invoices/{id}/xml · GET/v1/invoices/{id}/pdf

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.

Authorizationrequerido

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).

request
GET /v1/invoices/5cee05bd-.../xml
Authorization: Bearer sk_test_...
200
Content-Type: application/xml
Content-Disposition: attachment; filename="cfdi-5cee05bd-....xml"

<cfdi:Comprobante ...>...</cfdi:Comprobante>
POST/v1/invoices/{id}/cancel

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID) — misma regla que el resto de /v1/invoices*

Cuerpo

motivestring, opcional — 01/02/03/04; default 02
substitution_uuidstring, 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.

request
POST /v1/invoices/5cee05bd-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 8e9f0a1b-...
Content-Type: application/json

{ "motive": "02" }
200 · shape de la respuesta
{
  "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.

POST/v1/invoices/{id}/credit-notes

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

refund_iduuid, requerido — reembolso succeeded real; también la clave de idempotencia
amount_minorint64, requerido, > 0 (centavos, siempre positivo — el signo lo da el tipo de comprobante, no el monto)
currencystring ISO 4217, opcional (default MXN)
motivostring, opcional (default genérico "Devolución")
forma_pagostring, opcional — c_FormaPago con la que devolviste el dinero (default 03); 99 no se acepta
seriestring opcional, máx. 25, sin | — muchos comercios llevan una serie propia para sus notas (p. ej. "NC") — ver Facturación CFDI → Serie y folio
foliostring 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.

request · factura de un COBRO de Winal
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"
}
200 · nota de crédito timbrada (NO 201)
{
  "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"
}
POST/v1/invoices/{id}/credit-notes/pue

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

amount_minorint64, requerido, > 0 (centavos)
currencystring ISO 4217, opcional (default MXN)
motivostring, opcional (default genérico "Devolución")
forma_pagostring, opcional — con qué DEVOLVISTE el dinero (default 03); 99 no se acepta
seriestring opcional, máx. 25, sin | — ver Facturación CFDI → Serie y folio
foliostring 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.

request · factura de MOSTRADOR (sin cobro de Winal)
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"
}
200 · mismo shape, 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"
}
GET/v1/invoices/{id}/credit-notes

Lista las notas de crédito (cualquier estado) de una factura, del más nuevo al más viejo.

Authorizationrequerido
200 · respuesta real (sin notas aún)
{ "object": "list", "data": [] }
GET/v1/invoices/{id}/credit-notes/{creditNoteId}

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).

Authorizationrequerido

Errores posibles: invoice.credit_note_not_found (404).

request
GET /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-...
Authorization: Bearer sk_test_...
GET/v1/invoices/{id}/credit-notes/{creditNoteId}/xml · GET/v1/invoices/{id}/credit-notes/{creditNoteId}/pdf

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.

Authorizationrequerido

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.

request
GET /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../xml
Authorization: Bearer sk_test_...
200
Content-Type: application/xml
Content-Disposition: attachment; filename="nota-credito-7a2e4c1b-....xml"

<cfdi:Comprobante ...>...</cfdi:Comprobante>
POST/v1/invoices/{id}/credit-notes/{creditNoteId}/retry

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.

Authorizationrequerido
Idempotency-Keyrequerido (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.

request
POST /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../retry
Authorization: Bearer sk_test_...
Idempotency-Key: 0a1b2c3d-...
200 · el mismo credit_note, ya timbrado
{
  "object": "credit_note",
  "id": "7a2e4c1b-9f3d-4b6e-8a2c-1d5f9e3b7c4a",
  "status": "stamped",
  "uuid_fiscal": "..."
}
POST/v1/invoices/{id}/credit-notes/{creditNoteId}/cancel

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.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

motivestring, opcional — 01/02/03/04; default 02
substitution_uuidstring, 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.

request
POST /v1/invoices/5cee05bd-.../credit-notes/7a2e4c1b-.../cancel
Authorization: Bearer sk_test_...
Idempotency-Key: 0a1b2c3d-...
Content-Type: application/json

{ "motive": "02" }
200 · shape de la respuesta (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.

POST/v1/receivables

Crea una cuenta por cobrar; internamente crea un Payment Link de un solo uso.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

customer_name / customer_emailstring, requeridos
customer_phonestring, opcional — requerido solo para whatsapp_link
customer_rfc / customer_uso_cfdi / customer_regimen_fiscal / customer_cpopcionales, pero van juntos o ninguno (auto-CFDI al pagarse)
conceptostring, requerido
amount_minorint64, requerido, > 0
currencystring ISO 4217, requerido
due_datedatetime ISO-8601, requerido

Errores posibles: receivable.missing_fields, receivable.invalid_amount, receivable.invalid_currency, receivable.invalid_date, receivable.incomplete_fiscal_receptor.

201 · respuesta real
{
  "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"
}
GET/v1/receivables · GET/v1/receivables/{id}

Lista o lee una cuenta por cobrar. status es derivado: open | paid | overdue | canceled.

Authorizationrequerido

Errores posibles: receivable.not_found.

GET/v1/receivables/{id}/whatsapp_link

Arma la URL https://wa.me/... con el mensaje de cobro pre-redactado.

Authorizationrequerido

Errores posibles: receivable.no_phone (sin customer_phone capturado).

200 · respuesta real
{ "url": "https://wa.me/5215512345678?text=Hola%20Mar%C3%ADa..." }
GET/v1/receivables/statement?customer_email=

Estado de cuenta agregado de un cliente: sus cuentas y los totales abierto/vencido/pagado.

Authorizationrequerido
200 · respuesta real
{
  "customer_email": "maria.lopez@example.mx",
  "receivables": [ { "...": "..." } ],
  "total_open_minor": 150000,
  "total_overdue_minor": 0,
  "total_paid_minor": 0
}
GET/v1/reports/aging

Antigüedad de saldos por cliente, en 4 cubos contables estándar.

Authorizationrequerido
200 · respuesta real (nombres de campo exactos)
{
  "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.

POST/v1/reconciliation/bank-statements

Importa el CSV del estado de cuenta bancario. Multipart (file) o cuerpo crudo. Sin Idempotency-Key — su idempotencia real es el hash del archivo.

Authorizationrequerido

Campos (multipart o query)

bankstring, requerido
period_start / period_endyyyy-MM-dd, requeridos
tolerance_daysint, opcional
presetbbva | banorte | santander, o usa el mapeo explícito
date_column / description_column / credit_column / debit_columnint (0-based), requeridos si no hay preset
reference_columnint, opcional
has_headerbool, opcional (default true)

Errores posibles: ver la tabla completa en Conciliación bancaria.

201 · respuesta real
{
  "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
}
GET/v1/reconciliation/bank-statements · GET/v1/reconciliation/bank-statements/{id}/lines?match_status=

Lista los estados de cuenta importados, o las líneas de uno (filtro opcional matched/unmatched/partial).

Authorizationrequerido

Errores posibles: bank_statement.invalid_match_status.

200 · línea real
{
  "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.

POST/public/autofactura/{slug}/invoices

Sin Authorization — la llave es receipt_code + rfc.

Cuerpo

receipt_codestring, requerido — formato W-XXXXX, viene en cada payment_intent
rfcstring, requerido — formato SAT
nombre / uso_cfdi / regimen_fiscal / cpstring, requeridos
emailstring, 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.

400 · respuesta real (sin PAC configurado, este servidor)
{
  "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"
  }
}
GET/public/autofactura/{slug}/invoices/{id}/xml?receipt_code=&rfc= · GET.../pdf?receipt_code=&rfc=

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).

GET/v1/reports/summary

Cobrado, ticket promedio, reembolsado, disputas (con su holdback) y tasa de aprobación del período.

Authorizationrequerido

Query

from / toISO-8601, opcionales
currencyISO 4217, opcional (default MXN)

Errores posibles: reports.invalid_period, reports.invalid_currency.

200 · respuesta real
{
  "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"
}
GET/v1/reports/daily

El mismo período de summary, desglosado día por día.

Authorizationrequerido

Query

from / toISO-8601, opcionales
currencyISO 4217, opcional (default MXN)

Errores posibles: reports.invalid_period, reports.invalid_currency.

200 · respuesta real (fragmento)
{ "object": "list", "data": [
  { "object": "reporting.daily_point", "date": "2026-07-01", "charged_minor": 458200, "refunded_minor": 0, "operation_count": 6 }
] }
GET/v1/reports/by-method · GET/v1/reports/by-connector

El período agrupado por método de pago o por conector ganador del ruteo, cada fila con su propio approval_rate.

Authorizationrequerido

Query

from / toISO-8601, opcionales
currencyISO 4217, opcional (default MXN)

Errores posibles: reports.invalid_period, reports.invalid_currency.

200 · by-method, respuesta real (fragmento)
{ "object": "list", "data": [
  { "object": "reporting.method_breakdown", "method": "card", "amount_minor": 1256490, "charge_count": 20, "approval_rate": 0.9 }
] }
200 · 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 }
] }
GET/v1/reports/payments

Operaciones recientes, más nuevas primero — paginado por cursor opaco, sin from/to.

Authorizationrequerido

Query

limitopcional, default 20, tope 100
cursoropaco, opcional — el next_cursor de la página anterior

Errores posibles: reports.invalid_cursor.

200 · respuesta real
{
  "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..."
}
GET/v1/reports/ledger.csv

El asiento contable del ledger, línea por línea, sin agregar: cargo/abono en centavos ya separados en dos columnas. text/csv, streaming.

Authorizationrequerido

Query

from / toISO-8601, opcionales — SIN currency: trae todas las monedas del período

Errores posibles: reports.invalid_period.

200 · text/csv, fragmento real
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...
GET/v1/reports/retenciones

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í.

Authorizationrequerido

Query

periodrequerido, YYYY-MM
formatopcional, csv para el export contable (default JSON)

Errores posibles: reports.invalid_period.

200 · respuesta real (fragmento)
{
  "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", "...": "..." } ]
}
GET/v1/reports/savings

Cuánto le ahorró/recuperó Winal al tenant en el período: ruteo consciente de costo, retry cross-conector y dunning de suscripciones.

Authorizationrequerido

Query

from / toISO-8601, opcionales (default: últimos 30 días)

Errores posibles: reports.invalid_period.

200 · respuesta real
{
  "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"
}
GET/v1/reports/polizas

Pólizas contables del período, formato CONTPAQi o Aspel-COI, listas para importar.

Authorizationrequerido

Query

from / toISO-8601, opcionales (default: últimos 30 días)
formatrequerido, sin default: contpaqi | aspel_coi

Errores posibles: reports.invalid_period, reports.invalid_format.

request
GET /v1/reports/polizas?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&format=contpaqi
Authorization: Bearer sk_test_...
200 · text/plain, fragmento real
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...
GET/v1/reports/cash-cut

Corte de caja del turno: desglose por método/conector (cobrado, propinas, operaciones), totales y devoluciones. Ver Reportes → Multisucursal para el detalle narrativo.

Authorizationrequerido

Query

from / toISO-8601, ambos requeridos (sin default: no hay "turno" sin rango explícito)
branch / registeropcionales, igualdad exacta contra metadata.branch/metadata.register del intent — no validan contra ningún catálogo.

Errores posibles: reports.invalid_period, reports.invalid_currency.

200 · respuesta real, sin filtrar por branch (trae by_branch)
{
  "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étodoRutaNotas
POST/v1/accountsScope accounts:write; exige ventana de alta abierta desde tu consola. Idempotente por reference. No admite delegación.
GET/v1/accountsLista tus cuentas administradas. ?limit=, por omisión 100, tope 500.

Cuerpo de POST /v1/accounts

CampoTipoDescripción
referencestring, requeridoIdentificador de esta cuenta en TU sistema (1–128 caracteres: letras, dígitos, . _ : -). Hace el alta idempotente.
namestring, requeridoNombre comercial de la cuenta (hasta 200 caracteres).
scopesarreglo, opcionalScopes 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.
bash
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" }'
201 · respuesta real
{
  "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.

POST/v1/onboarding/applications

Alta mínima de una solicitud, en draft.

Authorizationrequerido

Cuerpo

legal_namestring, requerido
person_type"fisica" | "moral", requerido
contact_emailstring, 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.

201 · respuesta real
{
  "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": []
}
PUT/v1/onboarding/applications/{id}

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).

Authorizationrequerido

Errores posibles: onboarding_application.not_found, onboarding_application.transition_conflict, onboarding_application.invalid_document.

200 · respuesta real (fragmento)
{
  "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": "..." }, "..." ]
}
POST/v1/onboarding/applications/{id}/submit

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.

Authorizationrequerido

Errores posibles: onboarding_application.missing_fields, missing_documents, transition_conflict.

200 · respuesta real (aprobada)
{
  "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"
}
GET/v1/onboarding/applications · GET/v1/onboarding/applications/{id}

Lista (?status=, ?limit= opcionales; sin documents) o lee una solicitud por id (con documents[]).

Authorizationrequerido

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.

POST/v1/connect/accounts

Liga un sub-comercio con una solicitud de Onboarding ya approved.

Authorizationrequerido

Cuerpo

onboarding_application_idstring (uuid), requerido
connector_keystring, opcional (default "stp"; usa "sim" en pruebas)

Errores posibles: connect.invalid_application_id, connect.application_not_found, connect.application_not_approved, connect.account_conflict.

201 · respuesta real
{
  "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"
}
GET/v1/connect/accounts · GET/v1/connect/accounts/{id}

Lista (?limit=) o lee una cuenta Connect. La CLABE del sub nunca se expone completa.

Authorizationrequerido

Errores posibles: connect.account_not_found.

POST/v1/connect/transfers

Split manual post-cobro (multi sub-comercio). application_fee_minor + Σ splits[].amount_minor debe ser exactamente charge_amount_minor.

Authorizationrequerido
Idempotency-Keyrequerido (UUID) — compromete dinero

Cuerpo

payment_intent_idstring (uuid), requerido
charge_amount_minorint64, requerido, > 0
application_fee_minorint64, requerido (puede ser 0)
currencyISO 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.

201 · respuesta real
{
  "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-..." }
  ]
}
GET/v1/connect/transfers · GET/v1/connect/transfers/{id}

Lista (?limit=) o lee un transfer, con sus splits[].

Authorizationrequerido

Errores posibles: connect.transfer_not_found.

Payouts

Guía narrativa completa (la máquina de estados, STP vs. Sim) en Payouts.

POST/v1/payouts

Ordena una dispersión SPEI a una CLABE. Sin custodia (ADR-0001): sale de la cuenta del propio comercio.

Authorizationrequerido
Idempotency-Keyrequerido (UUID)

Cuerpo

clabestring, 18 dígitos con dígito de control válido, requerido
beneficiary_namestring, requerido
beneficiary_rfcstring, opcional
amount_minorint64, requerido, > 0
currencyISO 4217, requerido (solo MXN)
conceptostring, requerido
referencestring, opcional (se genera si falta)
connector_keyopcional (default "stp"; usa "sim" en pruebas)

Errores posibles: payout.missing_fields, payout.invalid_amount, payout.invalid_clabe, payout.unsupported_currency.

201 · respuesta real
{
  "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"
}
GET/v1/payouts · GET/v1/payouts/{id}

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.

Authorizationrequerido

Errores posibles: payout.not_found.

Billers (pago de servicios)

Guía narrativa completa en Pago de servicios.

GET/v1/billers

Catálogo global (sin variación por tenant). ?category= opcional.

Authorizationrequerido
200 · respuesta real (fragmento)
{ "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)"
] }
POST/v1/billers/{code}/inquiry

Consulta de adeudo — NO mueve dinero, no exige Idempotency-Key.

Authorizationrequerido

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.

200 · respuesta real
{
  "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"
}
POST/v1/service-payments

Confirma el pago del servicio ante el biller.

Authorizationrequerido
Idempotency-Keyrequerido (UUID) — validado por este endpoint mismo

Cuerpo

biller_codestring, requerido
referencestring, requerido
amount_minorint64, opcional — si viene, debe igualar el adeudo vigente
payment_intent_iduuid, 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).

201 · respuesta real (pagado)
{
  "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"
}
201 · el biller rechaza la confirmación (no es error HTTP)
{ "id": "...", "object": "service_payment", "status": "failed",
  "failure_reason": "El biller 'Telmex' rechazó la confirmación del pago para la referencia '...' (simulado)." }
GET/v1/service-payments · GET/v1/service-payments/{id}

Lista o lee un pago de servicio.

Authorizationrequerido

Query (solo en el listado)

limitopcional, default 100, tope 500
fromopcional, ISO-8601. Inicio del período, inclusive
toopcional, ISO-8601. Fin del período, exclusivo
cursoropcional, 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.

GET/v1/recharge-carriers

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.

Authorizationrequerido
200 · respuesta real (fragmento)
{ "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 }
] }
POST/v1/recharge-carriers/{code}/enable

Activa o desactiva una operadora para tu tenant (por default TODAS están activas; solo hace falta llamarlo para apagar una).

Authorizationrequerido

Cuerpo: { "active": "bool, opcional (default true)" }

Errores posibles: recharge.invalid_carrier, recharge.carrier_not_found.

GET/v1/recharge-products

Catálogo de productos (montos/paquetes/servicios) por tenant, con el nombre y categoría de su operadora ya resueltos.

Authorizationrequerido
200 · respuesta real (fragmento)
{ "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 }
] }
POST/v1/recharges

Ejecuta una recarga o pago vía recarga. Irreversible una vez entregada — ver El desenlace.

Authorizationrequerido, scope recharges:write (ver Recargas → El permiso que exige una recarga)
Idempotency-Keyrequerido (UUID) — validado por este endpoint mismo

Cuerpo

product_codestring, requerido
referencestring — destino moderno (celular o número de servicio). Gana sobre phone_number si mandas los dos.
phone_numberstring — alias histórico de reference, se conserva por compatibilidad
amount_minorint64, opcional — SOLO para productos de monto libre (open_amount: true); en denominación fija se rechaza si lo mandas
payment_intent_iduuid, 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.

201 · respuesta real (entregada)
{
  "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" } ] } }
  ]
}
201 · en curso (regla 5: no es fracaso)
{ "...": "...", "status": "processing", "outcome": "undetermined", "settled": false, "safe_to_retry": false }
200 · a revisión: no aparece en el reporte del agregador (fragmento)
{ "...": "...", "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." }
201 · agotado: ningún agregador pudo (fragmento)
{ "...": "...", "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", "...": "..." } ] }
GET/v1/recharges/balance

Ú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.

Authorizationrequerido

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).

200 · respuesta real
{
  "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" }
  ]
}
GET/v1/recharges · GET/v1/recharges/{id}

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.

Authorizationrequerido

Query (solo en el listado)

limitopcional, default 100, tope 500
fromopcional, ISO-8601. Inicio del período, inclusive
toopcional, ISO-8601. Fin del período, exclusivo
cursoropcional, opaco. El next_cursor de la página anterior
connector_keyopcional, 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.

GET/v1/recharges/statement · GET/v1/recharges/statement.csv

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.

Authorizationrequerido

Query

fromopcional, ISO-8601, inclusive. Por omisión, 30 días atrás
toopcional, ISO-8601, exclusivo. Por omisión, ahora
pouch_idopcional, 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_keyopcional, 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.

GET/v1/recharges/bag-movements

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".

Authorizationrequerido

Query

fromopcional, ISO-8601, inclusive, sobre registered_at
toopcional, ISO-8601, exclusivo
limitopcional, default 100, tope 500

Errores posibles: recharge.invalid_period.

200 · respuesta real (ejemplo de la documentación del proveedor)
{
  "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.

POST/v1/recharge-transfers

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.

Authorizationrequerido, scope recharges:write (el mismo que vender: ver Recargas → El permiso que exige una recarga)
Idempotency-Keyrequerido (UUID) — validado por este endpoint mismo. Sin ella, un reintento ordenaría el traspaso otra vez

Cuerpo

destination_accountuuid, 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_minorint64, requerido — centavos, MXN
pouch_idstring, 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
notestring, requerido, 3–200 caracteres — el agregador la exige y la muestra en su portal
source_iduuid, 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.

201 · aplicado
{
  "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"
}
201 · en curso (regla 5: no es fracaso)
{ "...": "...", "status": "requesting", "provider_movement_id": null, "settled_at": null }
GET/v1/recharge-transfers · GET/v1/recharge-transfers/{id}

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.

Authorizationrequerido

Query (solo en el listado)

limitopcional, default 100, tope 500

Errores posibles: recharge_transfer.not_found.

200 · en curso
{
  "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.

GET/v1/recharge-subaccounts

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.

Authorizationrequerido (lectura: payments:read)
Winal-Account-Keyopcional — la credencial del cliente sobre el que actúas

Campos de cada elemento

statuspending/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_referenceLa referencia con la que esta cuenta deposita: la que se escribe en la ficha para que el abono llegue a su cuenta.
report_urlEl 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_idSu número de cuenta en el agregador (cuentaID): el destino de un traspaso.
airtime_reference / services_referenceReferencias 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_itSi 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.

200
{
  "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).

GET/v1/recharge-report-links

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.

Authorizationrequerido (lectura: payments:read)
Winal-Account-Keyopcional — la credencial de la farmacia sobre la que actúas

Campos de cada elemento

source_id / source_labelLa bolsa de la cuenta.
is_subaccounttrue si es la cuenta propia que Winal dio de alta para esa cuenta.
deposit_referenceLa referencia con la que esa cuenta deposita, si el agregador la dio.
report_url / observed_atEl 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.
refreshEstado 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.
200 · respuesta real
{
  "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 }
  }]
}
POST/v1/recharge-report-links/refresh

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.

Authorizationrequerido (recharges:write), llave sk_live_
Winal-Account-Keyopcional — la farmacia sobre la que actúas

Cuerpo (opcional)

source_iduuid, opcional — una bolsa de la cuenta; omitido, todas sus bolsas propias.
wait_msint, 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).

202 · respuesta real
{
  "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 }
  }]
}
POST/v1/recharge-balance/refresh

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.

Authorizationrequerido (recharges:write)
Winal-Account-Keyopcional — la farmacia sobre la que actúas

Cuerpo (opcional)

wait_msint, 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).

202 · respuesta real
{
  "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 }
}
EVENTOrecharge.deposit.detected

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).

forma real del webhook (valores del movimiento de ejemplo)
{
  "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.