Clientes

MODO PRUEBA

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

bash
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" }'
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"
}

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.

bash
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" }'
201 · respuesta real — recién creado, todavía sin activar
{
  "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:

bash
curl -s https://api.winal.com.mx/v1/payment_methods/2a3c754f-... \
  -H "Authorization: Bearer $SK"
200 · respuesta real, segundos después
{
  "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).

bash
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" }'
200 · respuesta real
{
  "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.

Un método guardado inactivo no cobra
Si 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

bash
curl -s -X DELETE https://api.winal.com.mx/v1/payment_methods/2a3c754f-... \
  -H "Authorization: Bearer $SK"
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"
}

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:

bash
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 }'
El shape de la respuesta es IGUAL al de un método normal
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.

Gated: certificación real con Visa/Mastercard
El mecanismo de provisión, dedupe por PAR y cobro portable ya está construido y probado contra el conector Sim. Cobrar con un network token REAL requiere certificarse como Token Requestor ante Visa (VTS) o Mastercard (MDES) — trámite y contrato del dueño, no una limitación del código.

Endpoints

MétodoRutaNotas
POST/v1/customersCuerpo vacío permitido; sin Idempotency-Key.
GET/v1/customersLista los clientes del tenant.
GET/v1/customers/{id}404 customer.not_found si no existe.
POST/v1/customers/{id}/payment_methodsRequiere Idempotency-Key; payment_token requerido; network_token: true opcional (ver §5).
GET/v1/customers/{id}/payment_methodsLista 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

HTTPcodeCausa
404customer.not_foundEl id del cliente no existe.
400payment_method.invalid_tokenFalta payment_token al guardar un método.
404payment_method.not_foundEl id del método no existe.
409payment_method.not_chargeableEl método referido en confirm no está active (el mensaje trae su estado real).
400payment_method.no_routeNo hay conector configurado que pueda resolver el guardado del método.
409payment_method.concurrent_modificationDos operaciones intentaron mutar el mismo método a la vez.

Ver el envelope completo de error en Errores.