Para empezar
La API habla JSON y todas sus rutas cuelgan de esta dirección. Si es la primera vez que conectas una caja, empieza por la
guía paso a paso.
https://simplepuntos.com/api/v1
Cada petición lleva una llave de API en el encabezado Authorization. Las llaves se crean en
Configuración › Conexiones; usa una por caja para poder revocar una sin afectar a las demás.
Authorization: Bearer pts_…
Para programar y probar, crea una llave de prueba (empieza con pts_test_). Trabaja con una copia de las reglas
de Patitas Club: no ve a sus clientes reales, el código de canje siempre es 000000 y no manda WhatsApp ni correos.
Cuando la caja esté lista, cámbiala por una llave normal.
- Los puntos viajan como texto decimal con 2 decimales (
"120.50"); los pesos siempre con dos ("60.25"). Al mandarlos puedes usar texto o número, sin más decimales de esos.
- Las fechas van en ISO 8601 con zona horaria y los ids son UUID.
- Hasta 300 peticiones por minuto por llave; arriba de eso responde
429.
- Manda el cuerpo como JSON con
Content-Type: application/json. Los errores siempre responden JSON, también un cuerpo mal formado o una ruta que no existe (mira errores).
- Los ejemplos usan variables de la terminal:
$LLAVE para tu llave y $CLIENTE, $CANJE y $MOVIMIENTO para los ids que te devuelve la API.
- Si prefieres importarla en Postman o generar tu cliente, la misma API está descrita en OpenAPI.
Leer las reglas del programa
GET/program
Lo que tu caja necesita para calcular y mostrar antes de cobrar, tal como está hoy en la configuración de Patitas Club.
Léelo al arrancar la caja en vez de copiar las reglas a mano: si el dueño las cambia, la caja se entera sola.
Lo que no aplica a un monedero viene en null.
points_per_100: los puntos que da cada $100 de compra, redondeando hacia abajo; point_value: lo que vale un punto al canjear.
otp: si el canje pide código (required) y desde qué monto canjeado en el día (threshold; "0.00" es en cada canje).
points_expire_after_months: los meses sin compras tras los que vence el saldo; null si no vence.
identifier: con qué se identifica al cliente (phone, email o text) y, si hay, el patrón que debe cumplir.
sandbox: true mientras la caja use una llave de prueba; muéstralo en pantalla para que nadie cobre con ella.
curl https://simplepuntos.com/api/v1/program \
-H "Authorization: Bearer $LLAVE"
{
"business": "Patitas Club",
"sandbox": false,
"program": "monedero",
"points_label": "puntos",
"points_precision": 2,
"points_per_100": "5.00",
"point_value": "1.00",
"otp": {
"required": true,
"threshold": "300.00"
},
"points_expire_after_months": null,
"stamps_per_card": null,
"stamp_rewards": null,
"identifier": {
"kind": "phone",
"label": "Celular",
"pattern": null
},
"portal_url": "https://simplepuntos.com/c/tu-negocio"
}
Escanear la tarjeta
En Patitas Club cada cliente se identifica por su celular
(teléfono celular). Es único por cliente: con él se busca, se acumula y se canjea.
- El lector de código de barras o QR funciona como un teclado: escribe el identificador y manda Enter.
- Deja el cursor en un campo de búsqueda; al recibir el Enter, el punto de venta llama
POST /members/lookup con lo que leyó.
- La respuesta trae el
id del cliente: con él van todas las demás rutas (/members/{id}/earnings, /members/{id}/redemptions…).
- Si responde
404, el cliente no está registrado: ofrécele registrarse con POST /members.
El celular viaja en el cuerpo de la petición y nunca en la dirección, para que no quede escrito en los registros
de servidores y proxies.
El teléfono se acepta tal como lo teclea el cajero: 229 123 4567 encuentra a +522291234567.
Clientes
Buscar un cliente
POST/members/lookup
Devuelve sus datos, su metadata y su saldo en puntos (balance) y en pesos (balance_amount).
curl -X POST https://simplepuntos.com/api/v1/members/lookup \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"identifier": "229 123 4567"
}'
{
"id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"identifier": "+522291234567",
"name": "María López",
"email": "maria@correo.mx",
"phone": "+522291234567",
"metadata": {
"mascota": "Firulais",
"sucursal": "Riviera"
},
"balance": "120.50",
"balance_amount": "120.50",
"created_at": "2026-09-14T11:32:08.120-06:00",
"updated_at": "2026-10-01T18:05:44.310-06:00"
}
Registrar un cliente
POST/members
Responde 201 con el cliente nuevo. Si el identificador ya existía, actualiza el nombre, fusiona la metadata y responde
200; su teléfono y su correo se quedan como estaban. En la metadata guarda lo que te ayude a reconocerlo: su mascota, su sucursal, su número de cliente.
curl -X POST https://simplepuntos.com/api/v1/members \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"identifier": "+522291234567",
"name": "María López",
"email": "maria@correo.mx",
"metadata": {
"mascota": "Firulais",
"sucursal": "Riviera"
}
}'
Consultar un cliente
GET/members/{id}
Lo mismo que la búsqueda, con el id que ya tienes; sirve para refrescar el saldo antes de cobrar.
curl https://simplepuntos.com/api/v1/members/$CLIENTE \
-H "Authorization: Bearer $LLAVE"
Actualizar un cliente
PATCH/members/{id}
Cambia el nombre y la metadata, que se fusiona con la que ya tenía; una llave con null se borra.
El identifier, el teléfono y el correo no cambian por la API porque deciden a quién le llega el código de cada canje:
si mandas uno distinto responde 422 contact_locked y no guarda nada. Esos cambios se hacen en el panel y quedan marcados en la bitácora.
curl -X PATCH https://simplepuntos.com/api/v1/members/$CLIENTE \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"name": "María López Ruiz",
"metadata": {
"sucursal": null,
"especie": "Perro"
}
}'
Movimientos de un cliente
GET/members/{id}/entries
Del más nuevo al más viejo. limit da el tamaño de página (25 por omisión, máximo 100); para la siguiente,
manda en before el next_before de la respuesta. Cuando next_before viene vacío ya no hay más.
curl https://simplepuntos.com/api/v1/members/$CLIENTE/entries?limit=10 \
-H "Authorization: Bearer $LLAVE"
Acumular y ajustar
Acumular
POST/members/{id}/earnings
Manda el monto de la compra en amount y Patitas Club aplica su regla: 5 puntos por cada $100.
Si tu caja calcula sus propios puntos (por ejemplo, por categoría), manda points en lugar de amount.
Uno de los dos es obligatorio. reference es el folio del ticket (mira
reintentos seguros); description y metadata son opcionales y quedan
guardados en el movimiento. Responde 201 con el movimiento, el saldo que dejó (balance_after) y el saldo
actual del cliente (balance).
curl -X POST https://simplepuntos.com/api/v1/members/$CLIENTE/earnings \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"amount": "850.00",
"reference": "riviera-3001",
"description": "Venta 3001"
}'
{
"id": "0199a1b5-77e0-7c21-b0f4-5d6e7f8a9b0c",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"kind": "earn",
"points": "42.50",
"balance_after": "162.50",
"source": "api",
"reference": "riviera-3001",
"description": "Venta 3001",
"metadata": {},
"reversed_entry_id": null,
"redemption_id": null,
"created_at": "2026-10-02T13:41:09.502-06:00",
"replayed": false,
"rewards": [],
"balance": "162.50",
"card_completed": false
}
Con los puntos que calculó tu caja:
curl -X POST https://simplepuntos.com/api/v1/members/$CLIENTE/earnings \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"points": "15.50",
"reference": "riviera-3002",
"description": "Venta 3002",
"metadata": {
"total": 310
}
}'
Ajustar
POST/members/{id}/adjustments
points lleva signo: negativo descuenta. description es obligatoria porque explica el ajuste en el historial del cliente.
curl -X POST https://simplepuntos.com/api/v1/members/$CLIENTE/adjustments \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"points": "-20.00",
"description": "Corrección de caja",
"reference": "riviera-ajuste-17"
}'
Consultar un movimiento
GET/entries/{id}
curl https://simplepuntos.com/api/v1/entries/$MOVIMIENTO \
-H "Authorization: Bearer $LLAVE"
Canje con código
Patitas Club pide un código para confirmar
un canje cuando, con él, el cliente llega a $300.00 canjeados en el día:
el cliente lo recibe por WhatsApp si tiene celular; si no, por correo.
Así nadie gasta puntos ajenos con solo conocer un teléfono o un número de tarjeta.
Abajo de ese monto el canje se confirma de inmediato y responde 201.
- El cliente quiere usar sus puntos. La caja llama
POST /members/{id}/redemptions con cuántos.
- Responde
202: el canje queda pendiente y al cliente le llega un código de 6 dígitos. otp.sent_to dice a dónde, enmascarado, para que el cajero lo pueda decir en voz alta.
- El cajero le pide el código y lo manda a
POST /redemptions/{id}/confirm. Responde 200 con el canje confirmado: aplica amount como descuento en pesos.
- ¿No llegó?
POST /redemptions/{id}/resend. ¿Se arrepintió? POST /redemptions/{id}/cancel.
El código vence en 10 minutos y se bloquea tras 5 intentos fallidos; se pueden pedir hasta 3 códigos cada 10 minutos.
Pedir un canje
POST/members/{id}/redemptions
otp_channel es opcional (whatsapp o email); si el cliente no tiene ese medio se usa el otro.
Si repites la petición con la misma reference y los mismos puntos, responde el mismo canje con replayed: true:
202 si sigue pendiente (no manda otro código; para eso está resend) y 200 si ya estaba confirmado.
El canje trae el saldo actual del cliente en balance.
curl -X POST https://simplepuntos.com/api/v1/members/$CLIENTE/redemptions \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"points": "50.00",
"reference": "riviera-canje-88",
"otp_channel": "whatsapp"
}'
{
"id": "0199a1c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"status": "pending",
"points": "50.00",
"amount": "50.00",
"reference": "riviera-canje-88",
"channel": "pos",
"discount_code": null,
"otp": {
"channel": "whatsapp",
"sent_to": "•••• 4567",
"expires_at": "2026-10-02T13:55:12.004-06:00"
},
"entry": null,
"balance": "120.50",
"created_at": "2026-10-02T13:45:12.004-06:00",
"confirmed_at": null,
"cancelled_at": null,
"replayed": false
}
Confirmar con el código
POST/redemptions/{id}/confirm
Manda el código como texto. Si es incorrecto responde invalid_code con los intentos que quedan en details.attempts_left.
Si el canje ya estaba confirmado, por ejemplo porque reintentas tras un timeout, responde 200 con el mismo canje sin descontar dos veces,
aunque no mandes el código.
curl -X POST https://simplepuntos.com/api/v1/redemptions/$CANJE/confirm \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"code": "482913"
}'
Reenviar el código
POST/redemptions/{id}/resend
Manda un código nuevo; el anterior deja de servir. Puedes cambiar de medio con otp_channel.
curl -X POST https://simplepuntos.com/api/v1/redemptions/$CANJE/resend \
-H "Authorization: Bearer $LLAVE"
Cancelar un canje
POST/redemptions/{id}/cancel
Si el canje ya estaba confirmado, le devuelve los puntos al cliente y, si era de la tienda en línea, borra su código de descuento.
Repetirlo responde 200 con el mismo canje, sin devolver dos veces; balance trae el saldo que quedó para el comprobante.
Un canje cuyo código de Shopify ya se usó no se cancela (409 not_pending).
curl -X POST https://simplepuntos.com/api/v1/redemptions/$CANJE/cancel \
-H "Authorization: Bearer $LLAVE"
{
"id": "0199a1c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"status": "cancelled",
"points": "50.00",
"amount": "50.00",
"reference": "riviera-canje-88",
"channel": "pos",
"discount_code": null,
"otp": null,
"entry": {
"id": "0199a1c1-0b1c-7d2e-9f3a-4b5c6d7e8f90",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"kind": "redeem",
"points": "-50.00",
"balance_after": "70.50",
"source": "api",
"reference": null,
"description": "Canje en tienda",
"metadata": {},
"reversed_entry_id": null,
"redemption_id": "0199a1c0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"created_at": "2026-10-02T13:46:30.209-06:00"
},
"balance": "120.50",
"created_at": "2026-10-02T13:45:12.004-06:00",
"confirmed_at": "2026-10-02T13:46:30.215-06:00",
"cancelled_at": "2026-10-02T13:58:02.871-06:00"
}
Consultar un canje
GET/redemptions/{id}
curl https://simplepuntos.com/api/v1/redemptions/$CANJE \
-H "Authorization: Bearer $LLAVE"
Devoluciones
Cuando un cliente devuelve mercancía, revierte el movimiento de esa venta. Manda en amount el monto devuelto en pesos y
Patitas Club le quita lo que la regla le dio por él (5.00 puntos por cada $100, redondeando hacia abajo);
o manda points si tu caja calcula los puntos. Sin amount ni points la devolución es total.
Cualquier otra llave responde 400 bad_request en vez de ignorarse.
La respuesta dice cuánto pediste (requested_points) y cuánto se revirtió de verdad (reversed_points): puede ser menos
si el cliente ya canjeó esos puntos, porque el saldo nunca queda en negativo.
Un canje no se revierte: se cancela con POST /redemptions/{id}/cancel, que además borra su código de Shopify; revertir el movimiento
de un canje responde 422 not_reversible.
Buscar movimientos por referencia
GET/entries?reference={folio}
Si tu caja no guardó el id del movimiento, búscalo por el folio con el que lo mandó. Responde la lista como los
movimientos de un cliente, con next_before para paginar; vacía si nadie usó esa referencia.
curl https://simplepuntos.com/api/v1/entries?reference=riviera-3001 \
-H "Authorization: Bearer $LLAVE"
{
"entries": [
{
"id": "0199a1b5-77e0-7c21-b0f4-5d6e7f8a9b0c",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"kind": "earn",
"points": "42.50",
"balance_after": "162.50",
"source": "api",
"reference": "riviera-3001",
"description": "Venta 3001",
"metadata": {},
"reversed_entry_id": null,
"redemption_id": null,
"created_at": "2026-10-02T13:41:09.502-06:00"
}
],
"next_before": null
}
Revertir un movimiento
POST/entries/{id}/reverse
curl -X POST https://simplepuntos.com/api/v1/entries/$MOVIMIENTO/reverse \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"amount": "300.00",
"reference": "riviera-dev-3001",
"description": "Devolución parcial del ticket 3001"
}'
{
"id": "0199a1e4-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"kind": "reversal",
"points": "-15.00",
"balance_after": "147.50",
"source": "api",
"reference": "riviera-dev-3001",
"description": "Devolución parcial del ticket 3001",
"metadata": {},
"reversed_entry_id": "0199a1b5-77e0-7c21-b0f4-5d6e7f8a9b0c",
"redemption_id": null,
"created_at": "2026-10-02T14:02:51.330-06:00",
"replayed": false,
"requested_points": "15.00",
"reversed_points": "15.00",
"voided_rewards": []
}
Premios y cupones
Cada premio de la tarjeta de sellos y cada cupón de una campaña trae un código de 8 caracteres, único en el negocio, que el cliente muestra o dicta en caja.
- Al escanear la tarjeta,
GET /members/{id}/rewards da los premios y cupones que puede usar.
- Para usar uno, manda su código a
POST /rewards/{código}/use y aplica el premio en la venta.
Premios y cupones disponibles
GET/members/{id}/rewards
Solo los que todavía se pueden usar, del más nuevo al más viejo.
curl https://simplepuntos.com/api/v1/members/$CLIENTE/rewards \
-H "Authorization: Bearer $LLAVE"
{
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"rewards": [
{
"id": "0199a1d2-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"code": "K7QM3XPA",
"title": "Bebida al 50%",
"source": "campaign",
"status": "available",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"expires_on": null,
"used_at": null,
"used_by": null,
"reference": null,
"created_at": "2026-10-02T13:41:09.502-06:00"
}
]
}
Usar un premio o cupón
POST/rewards/{código}/use
El código se acepta con o sin guion y en minúsculas: k7qm-3xpa encuentra a K7QM3XPA. Cada uno se usa una sola vez.
Repetir con la misma reference responde 200 con replayed: true; si ya se había usado de otra forma responde
409 already_used, y si venció, 422 expired.
curl -X POST https://simplepuntos.com/api/v1/rewards/K7QM3XPA/use \
-H "Authorization: Bearer $LLAVE" \
-H "Content-Type: application/json" \
-d '{
"reference": "riviera-3002"
}'
{
"id": "0199a1d2-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"code": "K7QM3XPA",
"title": "Bebida al 50%",
"source": "campaign",
"status": "used",
"member_id": "0199a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
"member_identifier": "+522291234567",
"expires_on": null,
"used_at": "2026-10-02T13:52:40.118-06:00",
"used_by": "Llave «Caja Riviera»",
"reference": "riviera-3002",
"created_at": "2026-10-02T13:41:09.502-06:00",
"replayed": false
}
Reintentos seguros
Las redes fallan. Si una petición se cortó y no sabes si llegó, repítela igual, con la misma reference: si ya existía, la API responde
lo mismo que la primera vez con replayed: true (200, o 202 si es un canje que sigue pendiente), sin acumular dos veces.
Vale para acumulaciones, ajustes, canjes y reversos; manda siempre reference.
La referencia es única por tipo de movimiento: el folio del ticket sirve para acumular y para canjear en la misma venta,
pero no para dos acumulaciones. Usa el folio con un prefijo por sucursal (riviera-3001, coyol-3001) y no lo reinicies:
si tu caja reinicia folios por día o por terminal, agrégale la fecha o la caja (riviera-20261002-3001).
Si repites una referencia con otro monto, otros puntos u otro cliente, responde
409 reference_taken con cuándo y con cuánto se usó, y no mueve nada.
En canjes, la referencia queda libre cuando el canje se cancela: puedes pedir otro con el mismo folio.
Errores
Todo error responde JSON con la misma forma, también un cuerpo que no es JSON válido o una ruta que la API no tiene:
un code estable para tu programa y un message en español para mostrarle al cajero.
{
"error": {
"code": "reference_taken",
"message": "La referencia «riviera-3001» ya se usó el 02/10/2026 a las 13:41 con 42.50 puntos"
}
}