SimplePuntos

Desarrolladores / Referencia

Referencia de la API

Los ejemplos usan un negocio de muestra. Ya conectado, el panel de cada negocio muestra esta misma referencia con sus reglas.

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.

  1. El lector de código de barras o QR funciona como un teclado: escribe el identificador y manda Enter.
  2. 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ó.
  3. La respuesta trae el id del cliente: con él van todas las demás rutas (/members/{id}/earnings, /members/{id}/redemptions…).
  4. 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.

  1. El cliente quiere usar sus puntos. La caja llama POST /members/{id}/redemptions con cuántos.
  2. 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.
  3. 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.
  4. ¿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.

  1. Al escanear la tarjeta, GET /members/{id}/rewards da los premios y cupones que puede usar.
  2. 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
}
CódigoHTTPCuándo
not_found 404 No hay ningún premio o cupón con ese código en tu negocio.
already_used 409 El premio o cupón ya se usó, con otra referencia o sin ella.
expired 422 Pasó su fecha de vencimiento.

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"
  }
}
CódigoHTTPCuándo
bad_request 400 Falta un parámetro obligatorio, el cuerpo no es JSON válido, mandaste amount y points juntos o un reverso trae una llave que no conoce.
unauthorized 401 Falta la llave o fue revocada.
not_found 404 El cliente, movimiento o canje no existe en tu negocio, la ruta no existe en la API o el cursor before no está en la lista.
not_pending 409 El canje ya no está pendiente para lo que pediste: confirmar o reenviar uno cancelado, reenviar uno confirmado, o cancelar uno de Shopify cuyo código ya se usó o no se pudo borrar.
reference_taken 409 La referencia ya se usó en ese tipo de movimiento con otros puntos u otro cliente; el mensaje dice cuándo y con cuánto.
invalid 422 Los datos tienen errores; details trae los mensajes por campo. También un canje mayor al saldo.
contact_locked 422 Intentaste cambiar el identificador, el teléfono o el correo; eso solo se hace en el panel.
insufficient_balance 422 El movimiento dejaría el saldo en negativo.
invalid_code 422 Código incorrecto, vencido o bloqueado; ya se enviaron 3 códigos en 10 minutos; el cliente no tiene teléfono ni correo, o el programa en periodo de prueba llegó a su tope de códigos del día.
not_reversible 422 El movimiento ya no tiene puntos por revertir (ya se revirtió o el cliente ya los gastó), o es el de un canje: cancélalo.
rate_limited 429 Más de 300 peticiones por minuto con la misma llave.
¿Dudas? Escríbenos