Guarda un método de pago tokenizado del lado del proveedor, asociado a un
customer, y cóbralo después con un solo campo en confirm
(payment_method_id) — sin pedirle al pagador su tarjeta otra vez. El PAN nunca
pasa por Winal ni por tu servidor en ningún paso de este flujo (PCI SAQ A, igual que el resto
de la API).
1. Crea el cliente
curl -s https://api.winal.com.mx/v1/customers \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{ "name": "Juan Pérez", "email": "juan.perez@example.mx" }'
{
"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"
}
name, email y metadata son todos opcionales — un
cuerpo vacío {} también crea un cliente válido. No hay Idempotency-Key
en este endpoint (no mueve dinero).
2. Guarda un método de pago (queda pending)
payment_token es el mismo token de un solo uso de la tokenización client-side
(tok_sim_* en pruebas; el token real del proveedor en producción — igual que en
confirm). Este endpoint sí exige Idempotency-Key: guardar un método
es una operación que sí queda registrada de forma persistente.
curl -s https://api.winal.com.mx/v1/customers/7c17884b-.../payment_methods \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payment_token": "tok_sim_ok" }'
{
"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"
}
El Worker registra el método con el conector y lo activa fuera del request (mismo patrón que
processing en un cobro: la evidencia llega después, no en la respuesta HTTP). Haz
GET del método (o espera el webhook correspondiente) hasta ver status:
"active", ya con brand/last4:
curl -s https://api.winal.com.mx/v1/payment_methods/2a3c754f-... \
-H "Authorization: Bearer $SK"
{
"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"
}
Nunca se expone la referencia interna del proveedor (provider_customer_ref /
provider_method_ref) en ningún payment_method — solo lo necesario
para mostrarlo en tu UI (brand, last4).
3. Cobro 1-click: payment_method_id en confirm
Con el método ya active, crea el intent normal y confírmalo con
payment_method_id en vez de payment_token +
payment_method — el método queda fijo en card y el ruteo se ata al
mismo conector donde vive el método guardado (no vuelve a evaluarse el ruteo por costo).
curl -s https://api.winal.com.mx/v1/payment_intents/5cb2d99f-.../confirm \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payment_method_id": "2a3c754f-c77a-4de0-9cf4-29aca3ff333e" }'
{
"id": "5cb2d99f-a290-45fc-81c5-acb5aa4b0d79",
"object": "payment_intent",
"amount_minor": 900,
"tip_minor": 0,
"total_minor": 900,
"currency": "MXN",
"status": "processing",
"receipt_code": "W-EMU7K",
"attempts": [
{ "id": "c0c4001a-...", "object": "attempt", "status": "pending", "connector_key": "sim", "method": "card" }
]
}
Segundos después, GET del mismo intent (o el webhook payment_intent.succeeded) confirma status: "succeeded" igual que cualquier otro cobro — el 1-click no cambia la máquina de estados, solo te ahorra pedir un token nuevo.
payment_method_id apunta a un método detached (ver abajo) o que
nunca llegó a active, confirm responde
400 payment_method.not_chargeable — el mensaje incluye el estado actual del
método.
4. Quita un método guardado
curl -s -X DELETE https://api.winal.com.mx/v1/payment_methods/2a3c754f-... \
-H "Authorization: Bearer $SK"
{
"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"
}
detached es un estado terminal — no hay borrado físico (regla del ledger append-only aplica igual al historial de métodos). Volver a mandar DELETE sobre uno ya detached es un no-op que devuelve 200 otra vez.
5. Network tokens (portables entre conectores)
El card-on-file de arriba es por conector: la referencia guardada solo cobra contra el mismo
proveedor con el que se guardó. Un network token (Visa VTS / Mastercard MDES) es distinto —
se provisiona una sola vez y queda portable: cualquier conector con soporte de network tokens
puede cobrarlo, sin volver a pedirle el token al pagador aunque cambies de proveedor. Actívalo con
network_token: true al guardar el método:
curl -s https://api.winal.com.mx/v1/customers/7c17884b-.../payment_methods \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payment_token": "tok_sim_ok", "network_token": true }'
POST devuelve el mismo pending de siempre y, minutos después,
active con brand/last4 — la API pública no expone
ningún campo par ni network_token en la respuesta HTTP (viven solo en el
modelo interno). La única diferencia observable desde tu integración es de comportamiento, no de
shape: el método queda cobrable por cualquier conector con la capacidad, en vez de fijo al que lo
guardó.
Dedupe por PAR: el Payment Account Reference (EMVCo) identifica la cuenta subyacente de forma
estable — la misma tarjeta siempre produce el mismo PAR, aunque se reponga con un número distinto.
Si guardas un network token dos veces para el mismo PAR del mismo cliente, el segundo guardado
termina detached automáticamente y el primero (el canónico) refresca su
referencia — nunca terminas con dos métodos activos duplicando la misma cuenta.
Cobrarlo es igual que cualquier payment_method_id en confirm (paso 3
arriba) — el ruteo elige cualquier conector con soporte de network tokens, sin fijarse al conector
que lo provisionó originalmente.
Endpoints
| Método | Ruta | Notas |
|---|---|---|
| POST | /v1/customers | Cuerpo vacío permitido; sin Idempotency-Key. |
| GET | /v1/customers | Lista los clientes del tenant. |
| GET | /v1/customers/{id} | 404 customer.not_found si no existe. |
| POST | /v1/customers/{id}/payment_methods | Requiere Idempotency-Key; payment_token requerido; network_token: true opcional (ver §5). |
| GET | /v1/customers/{id}/payment_methods | Lista los métodos guardados del cliente (incluye detached). |
| GET | /v1/payment_methods/{id} | 404 payment_method.not_found si no existe. |
| DELETE | /v1/payment_methods/{id} | Idempotente: repetir sobre uno ya detached no falla. |
Errores de customers/payment_methods
| HTTP | code | Causa |
|---|---|---|
404 | customer.not_found | El id del cliente no existe. |
400 | payment_method.invalid_token | Falta payment_token al guardar un método. |
404 | payment_method.not_found | El id del método no existe. |
409 | payment_method.not_chargeable | El método referido en confirm no está active (el mensaje trae su estado real). |
400 | payment_method.no_route | No hay conector configurado que pueda resolver el guardado del método. |
409 | payment_method.concurrent_modification | Dos operaciones intentaron mutar el mismo método a la vez. |
Ver el envelope completo de error en Errores.