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

  1. Entra a tu panel en negocio.doop.club → Más → API y webhooks. Necesitas un plan con acceso a la API.
  2. Crea una llave y elige qué puede hacer. Cópiala: solo se muestra una vez.
  3. Haz tus llamadas a https://api.doop.club/v1 con la llave en el encabezado Authorization.
Tu primera llamada
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.

PermisoPara qué sirve
customers:readBuscar clientes y ver su tarjeta de sellos
stamps:writeDar sellos
rewards:writeCanjear premios
promotions:readVer promociones vigentes
promotions:writeCanjear promociones
notifications:writeEnviar 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 } }; message está 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 encabezado Idempotent-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.

DatoDóndeQué es
tokentextoqueryLo que lee tu lector al escanear el QR del cliente.
codetextoqueryCódigo corto que el cliente ve debajo de su QR (ej. K7M2-QX9P).
emailtextoqueryCorreo del cliente.
phonetextoqueryTeléfono a 10 dígitos o con +52.
  • Usa solo uno de los cuatro parámetros por llamada.
Ejemplo
curl "https://api.doop.club/v1/customers/lookup?code=K7M2-QX9P" \
  -H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"
Respuesta
{
  "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.

DatoDóndeQué es
customerIduuid · obligatoriorutaEl id que devuelve Buscar un cliente.
Ejemplo
curl "https://api.doop.club/v1/customers/8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10/card" \
  -H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"
Respuesta
{
  "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.

DatoDóndeQué es
branchIduuid · obligatoriocuerpoSucursal donde se hizo la venta.
externalIdtexto · obligatoriocuerpoId de la venta en tu sistema. Si lo repites, no se duplica el sello.
customerTokentextocuerpoContenido del QR del cliente (o usa customerId).
customerIduuidcuerpoId del cliente (o usa customerToken).
amountCentsenterocuerpoMonto de la compra en centavos. Obligatorio si tu tarjeta pide compra mínima.
occurredAtfecha ISOcuerpoCuá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.
Ejemplo
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}'
Respuesta
{
  "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.

DatoDóndeQué es
rewardIduuid · obligatoriorutaId del premio (viene en Ver la tarjeta de sellos).
branchIduuidcuerpoSucursal donde se entregó.
Ejemplo
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"}'
Respuesta
{
  "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.

DatoDóndeQué es
customerIduuidqueryPara saber si el cliente aún puede usarla.
branchIduuidquerySolo 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).
Ejemplo
curl "https://api.doop.club/v1/promotions?customerId=8d1c7a52-3f0e-4b8a-9b2d-6a4f1e2c9d10" \
  -H "Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx"
Respuesta
{
  "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.

DatoDóndeQué es
promotionIduuid · obligatoriorutaId de la promoción.
customerIduuid · obligatoriocuerpoId del cliente.
externalIdtexto · obligatoriocuerpoId de la venta en tu sistema. Si lo repites, no se duplica.
branchIduuidcuerpoSucursal donde se usó.
Ejemplo
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"}'
Respuesta
{
  "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.

DatoDóndeQué es
titletexto (60) · obligatoriocuerpoTítulo de la notificación.
bodytexto (180) · obligatoriocuerpoTexto de la notificación.
audienceobjetocuerpoA quién: {"type":"all"}, {"type":"branch","branchId":"…"} o {"type":"inactive","days":30}.
urltextocuerpoA dónde lleva al tocarla (por defecto, la página de tu negocio).
scheduledAtfecha ISOcuerpoPara programarla. Sin esto se envía en ese momento.
Ejemplo
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"}}'
Respuesta
{
  "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.
Cuerpo que recibes
{
  "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).
Cuerpo que recibes
{
  "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.
Cuerpo que recibes
{
  "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.
Cuerpo que recibes
{
  "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.
Cuerpo que recibes
{
  "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.

Node.js
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

HTTPcodeQué pasó
401unauthorizedFalta la llave o no es válida.
403forbiddenLa llave no tiene el permiso (scope) que pide este endpoint.
404not_foundNo existe o no es de tu negocio.
409conflictYa se hizo (por ejemplo, un premio ya canjeado).
402limit_reached / plan_required / brand_suspendedTu plan no lo permite, llegaste al límite o la cuenta está suspendida.
422invalid_inputFaltan datos o no son válidos. message dice cuál.
429rate_limitedMás de 300 solicitudes por minuto con la misma llave.
Ejemplo de error
{
  "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.