☰ Menú · TPV y cajas
TPV y cajas
Todo lo que necesita una caja: encontrar al cliente, aplicar el ticket, canjear cashback, gestionar devoluciones y emitir tarjetas con los números de la propia caja.
El flujo habitual en el mostrador: el cajero escanea la tarjeta del wallet del cliente (o teclea el teléfono), la caja muestra el saldo, el cliente decide si paga parte del ticket con cashback, el ticket se cierra y la caja nos lo envía. La tarjeta en el teléfono del cliente se actualiza en segundos.
1. Encuentra al cliente
GET /v1/members/{member} acepta lo que tenga la caja: el número de tarjeta, el contenido del código de barras, el teléfono en formato internacional o nuestro id de socio. Codifica el teléfono para la URL (+ pasa a ser %2B).
# by the number on the card (or the barcode the scanner read)
curl https://loyaltyfy.io/api/v1/members/2000111703779 \
-H "Authorization: Bearer sk_test_..."
# by phone, with fresh add-to-wallet links
curl "https://loyaltyfy.io/api/v1/members/%2B37366791102?expand=wallet" \
-H "Authorization: Bearer sk_test_..."Muestra al cajero lo que importa según el programa, a partir del objeto del socio:
| program.mechanic | Mostrar |
|---|---|
| cashback | cashback.balance y el máximo que se puede canjear: el menor entre el saldo y el redeem_max_percent del ticket. |
| stamps | stamps.current de stamps.required, y stamps.rewards_available si el cliente tiene un producto gratis pendiente. |
| visit_discount, vip | discount.percent y discount.tier: aplica tú este descuento al ticket. |
2. Qué contiene el código de la tarjeta
El local elige el formato del código en Desarrolladores → Código de la tarjeta, para que coincida con lo que lee el escáner de la caja:
| Formato | El código contiene | Úsalo cuando |
|---|---|---|
| QR con el número de tarjeta | card_number, por ejemplo 2000111703779 | El escáner lee QR y la caja busca por número de tarjeta. Recomendado. |
| Código de barras (Code 128) con el número | El mismo número como código lineal, impreso bajo las barras | Escáneres láser antiguos que no leen QR. |
| QR para la caja de Loyaltyfy | Nuestro número de serie interno (un UUID) | Opción por defecto. Solo lo lee la app de caja de Loyaltyfy; un TPV puede seguir buscando al cliente por teléfono. |
Al cambiar el formato se actualizan las tarjetas que ya están en los wallets de los clientes; nadie tiene que reinstalar nada. Sea cual sea el formato, envía el valor escaneado tal cual a GET /v1/members/{member} o a POST /v1/scans, que además elimina los prefijos del escáner y los saltos de línea.
3. Emite tarjetas con los números de la caja
Si la caja ya tiene un sistema de tarjetas, conserva sus números. Inscribe al cliente con card_number igual al número de la caja: la tarjeta del wallet muestra ese número y su código lo contiene, así que el escáner también encuentra al cliente en la base de datos de la caja. Los números son únicos por local: un duplicado recibe 409 card_number_taken. Para asignar más tarde un número de caja a un socio existente, usa PATCH /v1/members/{member} con card_number.
curl https://loyaltyfy.io/api/v1/members \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7d1b2c4e-enrol-000123" \
-d '{
"phone": "+37366791102",
"first_name": "Ion",
"last_name": "Popescu",
"card_number": "2000111703779",
"language": "ro",
"consent": true
}'La respuesta incluye wallet.apple_url y wallet.google_url. Imprímelos como QR en el ticket, envíalos por SMS o muéstralos en el visor de cliente: el cliente abre el enlace y la tarjeta se añade a su teléfono.
4. Aplica el ticket
Envía cada ticket cerrado una sola vez, después del pago. amount es el ticket completo antes de cualquier cashback; redeem_amount es la parte que el cliente pagó con cashback. Aplicamos las reglas del programa (ticket mínimo, tope de canje, niveles) igual que la pantalla de caja del local.
curl https://loyaltyfy.io/api/v1/transactions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "CHK-2026-000123",
"member": "2000111703779",
"amount": 25000,
"currency": "MDL",
"register_id": "POS-1",
"closed_at": "2026-10-06T18:17:00+03:00",
"line_items": [
{ "sku": "1001", "name": "Espresso", "quantity": 2, "unit_price": 4000, "category": "Coffee" },
{ "sku": "2005", "name": "Cheesecake", "quantity": 1, "unit_price": 17000, "category": "Desserts" }
]
}'{
"object": "transaction",
"id": "f8acbb16-0b3b-4afc-acbb-e5eabd4dd072",
"external_id": "CHK-2026-000123",
"status": "completed",
"amount": 25000,
"redeem_amount": 0,
"cashback_earned": 1250,
"stamps_earned": 0,
"reward_unlocked": false,
"net_amount": 25000,
"currency": "MDL",
"register_id": "POS-1",
"closed_at": "2026-10-06T15:17:00+00:00",
"livemode": false,
"created_at": "2026-10-06T18:17:17.921+00:00",
"voided_at": null,
"member": {
"object": "member",
"id": "6dacfe0a-2500-44c6-a37c-05c660b093b4",
"card_number": "2000111703779",
"cashback": { "balance": 1250, "currency": "MDL", "earn_rate": 5, ... },
...
}
}external_id debe ser único entre todas las cajas del local. Si la numeración de tickets se reinicia en cada caja o en cada turno, combínala, por ejemplo POS-3-000123 (caja 3, ticket 123).El cashback se gana sobre lo que el cliente pagó de verdad: amount − redeem_amount. En los programas de sellos y de visitas, el ticket cuenta como una visita; stamps_earned y reward_unlocked te dicen qué pasó, para que la caja pueda decir "un café más y el siguiente es gratis".
5. Pago con cashback
Consulta primero el saldo, deja que el cliente elija y envía el ticket con redeem_amount. Si el saldo cambió entretanto (el cliente pagó en otro sitio hace un segundo), recibes 402 insufficient_balance con el importe available actual; muéstralo y deja que el cajero reintente.
curl https://loyaltyfy.io/api/v1/transactions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "CHK-2026-000124",
"member": "2000111703779",
"amount": 12000,
"redeem_amount": 1250
}'{
"error": {
"type": "invalid_request_error",
"code": "insufficient_balance",
"message": "redeem_amount is more than the available balance.",
"param": "redeem_amount",
"available": 1250
},
"request_id": "req_6f4c30b8ca7d4423a9d4"
}201 o 200. Envía el ticket cuando el total sea definitivo, pero antes de cobrar. Si no hay respuesta, reintenta con el mismo external_id o cierra el ticket sin cashback. Si no, el cliente puede llevarse el descuento y conservar el saldo.6. Entrega una recompensa de sellos
Cuando stamps.rewards_available es mayor que cero, el cliente tiene un producto gratis. Entrégalo en la caja y regístralo:
curl -X POST https://loyaltyfy.io/api/v1/members/4285324286429/rewards/redeem \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: reward-4285324286429-0001"7. Devoluciones y errores
Anula el ticket completo por su external_id. El cashback o los sellos ganados se retiran y el cashback canjeado se devuelve al cliente. Devoluciones parciales: anula el ticket y envía uno nuevo con el importe corregido y un external_id nuevo.
curl -X POST https://loyaltyfy.io/api/v1/transactions/CHK-2026-000123/void \
-H "Authorization: Bearer sk_test_..."8. Cuando la caja está sin conexión
Guarda los tickets en una cola local con su external_id y closed_at y envíalos cuando vuelva la conexión. Los duplicados se ignoran, así que puedes reenviar la cola entera. Las búsquedas necesitan red; si no hay, deja que el cajero omita la fidelización en ese ticket en lugar de bloquear la venta.
Tickets enviados desde el servidor de la caja
Algunos TPV no pueden llamar a una API desde el mostrador, pero sí enviar los tickets cerrados desde su servidor. También funciona: envía el mismo POST /v1/transactions desde el servidor, con el número de tarjeta que escaneó el cajero, en cuanto se cierre el ticket. Suscríbete a transaction.completed si la caja quiere mostrar el resultado más tarde.