API de Doop
Conecta tu punto de venta o tu sistema con Doop: busca clientes, da sellos al cobrar, entrega premios, aplica promociones, manda avisos y recibe un aviso en tu servidor cada vez que pasa algo.
Para empezar
- Entra a tu panel en negocio.doop.club → Más → API y webhooks. Necesitas un plan con acceso a la API.
- Crea una llave y elige qué puede hacer. Cópiala: solo se muestra una vez.
- Haz tus llamadas a
https://api.doop.club/v1con la llave en el encabezadoAuthorization.
curl "https://api.doop.club/v1/promotions" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"Llaves y permisos
Cada llave empieza con pp_live_ y pertenece a un solo negocio: solo ve y cambia datos de ese negocio. Al crearla eliges sus permisos; si una llave llama a algo que no tiene permitido, recibe 403.
| Permiso | Para qué sirve |
|---|---|
| customers:read | Buscar clientes y ver su tarjeta de sellos |
| stamps:write | Dar sellos |
| rewards:write | Canjear premios |
| promotions:read | Ver promociones vigentes |
| promotions:write | Canjear promociones |
| notifications:write | Enviar avisos |
Reglas generales
- Todo se envía y se recibe en JSON. Las respuestas buenas vienen en
{ data }y los errores en{ error: { code, message } };messageestá en español y lo puedes mostrar tal cual. - Todas las llamadas POST llevan el encabezado
Idempotency-Key(de 8 a 100 caracteres, por ejemplo el folio de la venta). Si se corta la conexión y repites la llamada con la misma llave, Doop no la hace dos veces: devuelve la misma respuesta y el encabezadoIdempotent-Replayed: true. - Máximo 300 solicitudes por minuto por llave.
- Las fechas van en formato ISO 8601 (
2026-10-16T19:12:00Z) y los montos en centavos (8500= $85.00).
Buscar un cliente
GET/customers/lookup· permisocustomers:read
Encuentra a un cliente de tu negocio por el contenido de su QR, su código corto, su correo o su teléfono. Si buscas por QR o por código y la persona aún no era cliente de tu negocio, se une en ese momento.
| Dato | Dónde | Qué es |
|---|---|---|
| tokentexto | query | Lo que lee tu lector al escanear el QR del cliente. |
| codetexto | query | Código corto que el cliente ve debajo de su QR (ej. K7M2-QX9P). |
| emailtexto | query | Correo del cliente. |
| phonetexto | query | Teléfono a 10 dígitos o con +52. |
- Usa solo uno de los cuatro parámetros por llamada.
curl "https://api.doop.club/v1/customers/lookup?code=K7M2-QX9P" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"{
"data": {
"id": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"fullName": "Ana Torres",
"email": "ana@correo.mx",
"phone": "+525512345678",
"joinedAt": "2026-09-02T18:24:00.000Z",
"lastVisitAt": "2026-10-14T19:05:00.000Z",
"visits": 7
}
}Ver la tarjeta de sellos
GET/customers/{customerId}/card· permisocustomers:read
Sellos que lleva el cliente y los premios que tiene listos para canjear. Devuelve null si tu negocio no tiene tarjeta de sellos activa.
| Dato | Dónde | Qué es |
|---|---|---|
| customerIduuid · obligatorio | ruta | El id que devuelve Buscar un cliente. |
curl "https://api.doop.club/v1/customers/8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10/card" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"{
"data": {
"stamps": 4,
"stampsRequired": 5,
"rewardTitle": "Café gratis",
"rewards": [
{
"id": "c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a",
"title": "Café gratis",
"expiresAt": "2026-12-14T06:00:00.000Z"
}
]
}
}Dar un sello
POST/stamps· permisostamps:write
Suma un sello cuando se cobra una venta en tu punto de venta. Se aplican las mismas reglas que en la caja de Doop (sellos por día, compra mínima). Al completar la tarjeta, el premio se crea solo.
| Dato | Dónde | Qué es |
|---|---|---|
| branchIduuid · obligatorio | cuerpo | Sucursal donde se hizo la venta. |
| externalIdtexto · obligatorio | cuerpo | Id de la venta en tu sistema. Si lo repites, no se duplica el sello. |
| customerTokentexto | cuerpo | Contenido del QR del cliente (o usa customerId). |
| customerIduuid | cuerpo | Id del cliente (o usa customerToken). |
| amountCentsentero | cuerpo | Monto de la compra en centavos. Obligatorio si tu tarjeta pide compra mínima. |
| occurredAtfecha ISO | cuerpo | Cuándo fue la venta. Si no la mandas, se usa la hora actual. |
- Un sello que no cumple las reglas no es error: responde 200 con status "rejected", el motivo en reason (daily_limit, below_min_amount, no_program, plan_inactive, too_old, future_date) y un message listo para mostrar.
- Cuando el cliente completa la tarjeta, stamps vuelve a 0 y rewardEarned trae el premio.
curl -X POST "https://api.doop.club/v1/stamps" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: venta-10293" \
-d '{"customerId":"8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10","branchId":"2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42","externalId":"VENTA-10293","amountCents":8500}'{
"data": {
"clientEventId": "api:VENTA-10293",
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"status": "accepted",
"stamps": 5,
"stampsRequired": 5,
"rewardEarned": {
"id": "c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a",
"title": "Café gratis"
}
}
}Canjear un premio
POST/rewards/{rewardId}/redeem· permisorewards:write
Marca como entregado un premio de la tarjeta de sellos. Un premio solo se puede canjear una vez.
| Dato | Dónde | Qué es |
|---|---|---|
| rewardIduuid · obligatorio | ruta | Id del premio (viene en Ver la tarjeta de sellos). |
| branchIduuid | cuerpo | Sucursal donde se entregó. |
curl -X POST "https://api.doop.club/v1/rewards/c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a/redeem" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: premio-c4e8f1a2" \
-d '{"branchId":"2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42"}'{
"data": {
"id": "c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a",
"status": "redeemed",
"redeemedAt": "2026-10-16T19:12:00.000Z"
}
}Ver promociones vigentes
GET/promotions· permisopromotions:read
Promociones activas de tu negocio. Si mandas customerId, available te dice si ese cliente todavía puede usar cada una.
| Dato | Dónde | Qué es |
|---|---|---|
| customerIduuid | query | Para saber si el cliente aún puede usarla. |
| branchIduuid | query | Solo las que aplican en esa sucursal. |
- kind: "percent" (value en %), "fixed" (value en centavos), "freebie" (regalo o 2x1) o "info" (solo aviso, no se canjea).
curl "https://api.doop.club/v1/promotions?customerId=8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"{
"data": [
{
"id": "a7d2e9c4-1b5f-4a3e-9c8d-6e0f1a2b3c4d",
"title": "2x1 los martes",
"description": "Todos los martes.",
"kind": "freebie",
"value": null,
"endsAt": null,
"available": true
},
{
"id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"title": "10% en tu cuenta",
"description": null,
"kind": "percent",
"value": 10,
"endsAt": "2026-10-31T06:00:00.000Z",
"available": false
}
]
}Canjear una promoción
POST/promotions/{promotionId}/redeem· permisopromotions:write
Registra que el cliente usó una promoción. Respeta los límites por persona y totales que configuraste.
| Dato | Dónde | Qué es |
|---|---|---|
| promotionIduuid · obligatorio | ruta | Id de la promoción. |
| customerIduuid · obligatorio | cuerpo | Id del cliente. |
| externalIdtexto · obligatorio | cuerpo | Id de la venta en tu sistema. Si lo repites, no se duplica. |
| branchIduuid | cuerpo | Sucursal donde se usó. |
curl -X POST "https://api.doop.club/v1/promotions/a7d2e9c4-1b5f-4a3e-9c8d-6e0f1a2b3c4d/redeem" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: promo-venta-10293" \
-d '{"customerId":"8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10","branchId":"2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42","externalId":"VENTA-10293"}'{
"data": {
"id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b",
"duplicate": false
}
}Enviar un aviso
POST/notifications· permisonotifications:write
Manda una notificación al celular de tus clientes, ahora o programada. Usa uno de los avisos del mes de tu plan.
| Dato | Dónde | Qué es |
|---|---|---|
| titletexto (60) · obligatorio | cuerpo | Título de la notificación. |
| bodytexto (180) · obligatorio | cuerpo | Texto de la notificación. |
| audienceobjeto | cuerpo | A quién: {"type":"all"}, {"type":"branch","branchId":"…"} o {"type":"inactive","days":30}. |
| urltexto | cuerpo | A dónde lleva al tocarla (por defecto, la página de tu negocio). |
| scheduledAtfecha ISO | cuerpo | Para programarla. Sin esto se envía en ese momento. |
curl -X POST "https://api.doop.club/v1/notifications" \
-H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: aviso-2026-10-16" \
-d '{"title":"Hoy 2x1 en tacos","body":"Solo hoy de 5 a 8 pm. Muestra tu QR en caja.","audience":{"type":"all"}}'{
"data": {
"id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"status": "sending",
"scheduledAt": null
}
}Webhooks
En vez de preguntarle a Doop cada rato, Doop te avisa: das de alta una dirección de tu servidor (https) en tu panel, eliges los eventos y cada vez que ocurre uno te mandamos un POST con este formato.
customer.joinedUn cliente se unió a tu negocio.
{
"id": "3b9e1c7a-5d2f-4a8e-b6c1-9f0e2d4a7b3c",
"type": "customer.joined",
"createdAt": "2026-10-16T19:12:00.000Z",
"brandId": "…",
"data": {
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"branchId": "2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42"
}
}stamp.addedSe sumó un sello (en caja, con el QR de la tablet o por la API).
{
"id": "3b9e1c7a-5d2f-4a8e-b6c1-9f0e2d4a7b3c",
"type": "stamp.added",
"createdAt": "2026-10-16T19:12:00.000Z",
"brandId": "…",
"data": {
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"branchId": "2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42",
"stamps": 4,
"stampsRequired": 5,
"source": "cashier"
}
}reward.earnedEl cliente completó su tarjeta y ganó un premio.
{
"id": "3b9e1c7a-5d2f-4a8e-b6c1-9f0e2d4a7b3c",
"type": "reward.earned",
"createdAt": "2026-10-16T19:12:00.000Z",
"brandId": "…",
"data": {
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"rewardId": "c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a",
"title": "Café gratis"
}
}reward.redeemedSe entregó un premio.
{
"id": "3b9e1c7a-5d2f-4a8e-b6c1-9f0e2d4a7b3c",
"type": "reward.redeemed",
"createdAt": "2026-10-16T19:12:00.000Z",
"brandId": "…",
"data": {
"rewardId": "c4e8f1a2-9b3d-4c6e-8f7a-0d1b2c3e4f5a",
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"branchId": "2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42"
}
}promotion.redeemedSe usó una promoción.
{
"id": "3b9e1c7a-5d2f-4a8e-b6c1-9f0e2d4a7b3c",
"type": "promotion.redeemed",
"createdAt": "2026-10-16T19:12:00.000Z",
"brandId": "…",
"data": {
"redemptionId": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b",
"promotionId": "a7d2e9c4-1b5f-4a3e-9c8d-6e0f1a2b3c4d",
"customerId": "8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10",
"branchId": "2f6b9c1e-7d4a-4e3b-8c5f-1a9e0d7b6c42"
}
}Verificar que viene de Doop
Cada envío trae el encabezado PP-Signature: t=1760641920,v1=…. v1 es un HMAC-SHA256 de t + "." + cuerpo con el secreto de tu webhook. Calcúlalo tú y compáralo; si no coincide, ignora el envío. También llega PP-Event con el nombre del evento.
import crypto from "node:crypto";
// secret: el que te dio Doop al crear el webhook (whsec_...)
// rawBody: el cuerpo tal cual llegó, como texto (antes de convertirlo a JSON)
function verificar(rawBody, firma, secret) {
const partes = Object.fromEntries(firma.split(",").map((p) => p.split("=")));
const esperado = crypto
.createHmac("sha256", secret)
.update(`${partes.t}.${rawBody}`)
.digest("hex");
const vigente = Math.abs(Date.now() / 1000 - Number(partes.t)) < 300; // 5 minutos
return vigente && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
}
// En tu servidor:
// verificar(rawBody, req.headers["pp-signature"], process.env.DOOP_WEBHOOK_SECRET)Reintentos
Responde con un código 2xx en menos de 10 segundos. Si tu servidor falla o tarda, Doop reintenta hasta 8 veces, esperando cada vez el doble (1, 2, 4, 8… minutos). Un mismo evento puede llegar más de una vez: usa su id para no procesarlo dos veces.
Errores
| HTTP | code | Qué pasó |
|---|---|---|
| 401 | unauthorized | Falta la llave o no es válida. |
| 403 | forbidden | La llave no tiene el permiso (scope) que pide este endpoint. |
| 404 | not_found | No existe o no es de tu negocio. |
| 409 | conflict | Ya se hizo (por ejemplo, un premio ya canjeado). |
| 402 | limit_reached / plan_required / brand_suspended | Tu plan no lo permite, llegaste al límite o la cuenta está suspendida. |
| 422 | invalid_input | Faltan datos o no son válidos. message dice cuál. |
| 429 | rate_limited | Más de 300 solicitudes por minuto con la misma llave. |
{
"error": {
"code": "invalid_input",
"message": "Falta el header Idempotency-Key (8 a 100 caracteres)."
}
}¿Usas herramientas que leen OpenAPI? La especificación está en https://api.doop.club/v1/openapi.json.