Loyenta English Entrar a mi panel

Guía de integración con Loyenta (API v1)

Para: equipos de desarrollo que conectan su sistema (POS, agenda, facturación, app propia) con Loyenta.

Un solo contrato para todos. Esta guía es igual para cualquier sistema, sea de BUILDEV o de terceros. Loyenta no tiene endpoints, campos ni reglas especiales para ningún sistema: cada integración se adapta a este contrato, no al revés. Si un sistema necesita algo que el contrato no cubre, se evalúa como mejora general de la API para todos. Versión del contrato: v1 del 29-10-2026.

Estado: la API v1 está implementada: llaves de prueba (loy_test_) y de producción (loy_live_) que cada partner crea en el portal de desarrolladores, inscripción, tarjeta web, Google Wallet, panel del negocio y límite de peticiones. Todavía no existen: las llaves por negocio (§3.2) y los webhooks (§9). Cualquier cambio del contrato se anotará en la sección Cambios.


1. Qué hace Loyenta y qué hace tu sistema

Loyenta Tu sistema
Guarda los programas, los clientes inscritos, los sellos y las recompensas Avisa a Loyenta cuando ocurre algo que puede dar sello: una venta pagada, un servicio realizado, una cita atendida
Decide si corresponde sello y cuántos, según las reglas que configuró el negocio No calcula sellos: solo envía la operación tal como ocurrió
Actualiza la tarjeta web y Google Wallet (Apple Wallet más adelante) Muestra al negocio el enlace o QR de inscripción y, si quiere, el progreso del cliente
Valida los canjes y evita duplicados Puede registrar canjes desde su propia interfaz

Regla de oro: Loyenta es la fuente oficial del estado de fidelización. Tu sistema nunca guarda "el saldo de sellos" como dato propio: si lo necesita, lo consulta.


2. Conceptos

Concepto Qué es
Negocio (business) Cada comercio que usa Loyenta. Sus datos están aislados de los demás negocios.
Programa (program) La tarjeta de sellos del negocio: marca, sellos requeridos, recompensa y reglas.
Cliente (customer) El cliente final del negocio. Se identifica por celular (formato E.164, por ejemplo +573001234567).
Inscripción (enrollment) Un cliente inscrito en un programa, con su tarjeta. Solo existe si el cliente aceptó las condiciones y el tratamiento de datos.
Operación (operation) Algo que ocurrió en tu sistema: una venta, un servicio o una visita. Loyenta decide si otorga sellos.
Recompensa (reward) Se desbloquea al completar la tarjeta. Queda disponible hasta que se canjea.
Partner Empresa de software que integra Loyenta para muchos negocios (por ejemplo, un POS o una app de agenda).

3. Dos formas de conectarse

3.1 Como partner (recomendado para software con muchos negocios)

Tu sistema tiene una credencial de partner y crea y administra los negocios por API. Es la forma de vender Loyenta como función de tu producto (por ejemplo, incluida en un plan de pago).

  1. Te registras tú mismo en el portal de desarrolladores (https://loyenta.buildev.co/desarrolladores) con el correo de tu empresa y un código que te llega. Ahí creas tus llaves de prueba y, cuando estés listo, de producción. No tienes que pedirle nada a BUILDEV.
  2. Cuando un usuario de tu sistema activa la fidelización, llamas a POST /v1/partner/businesses.
  3. Loyenta crea el negocio y envía al dueño una invitación por correo para configurar su marca y su programa en el panel de Loyenta.
  4. Desde ese momento, tu credencial puede operar sobre ese negocio.

3.2 Con la llave de un negocio (integraciones puntuales)

El dueño del negocio genera una llave en el panel de Loyenta (Integraciones → Nueva llave) y la pega en la configuración de tu sistema. Esa llave solo sirve para ese negocio. (Aún no disponible: por ahora, usa la forma de partner.)


4. Ambientes y autenticación

Hay una sola URL para todo: https://loyenta.buildev.co/v1. El modo lo decide la llave:

Modo Prefijo de llave Para qué
Prueba loy_test_ Desarrollar y probar. Los negocios que crea son de prueba (livemode: false) y en su panel aparece la etiqueta "Datos de prueba".
Producción loy_live_ Negocios reales (livemode: true). Sus dueños reciben correos de verdad.

La API (/v1), las páginas del cliente (/i, /t) y el panel (/panel) viven en el mismo dominio. Los enlaces que devuelve la API (enrollment_url, card.web_url, url del panel) ya vienen completos: úsalos tal cual, sin armarlos a mano.

Authorization: Bearer loy_test_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Reglas de seguridad obligatorias:


5. Flujo típico

sequenceDiagram
  participant N as Negocio (en tu sistema)
  participant S as Tu backend
  participant L as Loyenta
  participant C as Cliente final
  N->>S: Activa "Fidelización"
  S->>L: POST /v1/partner/businesses
  L-->>N: Invitación por correo (configura marca y programa)
  N->>S: Registra a un cliente o le comparte el QR del negocio
  S->>L: POST /v1/enrollment-invitations (celular del cliente)
  L-->>S: enrollment_url
  S-->>C: Envía el enlace (WhatsApp, SMS) o lo muestra como QR
  C->>L: Acepta condiciones, deja su correo y obtiene la tarjeta (web / Google Wallet)
  N->>S: Registra una venta pagada
  S->>L: POST /v1/operations (Idempotency-Key)
  L-->>S: stamps_awarded, saldo, recompensas
  L-->>C: La tarjeta se actualiza sola

6. Endpoints

Todos los POST aceptan el header Idempotency-Key (ver §7).

6.1 Partner: negocios

POST /v1/partner/businesses

Crea el negocio, o lo devuelve si ya existe con ese external_ref.

{
  "external_ref": "user_cl9x8…",
  "trade_name": "Barbería El Corte",
  "legal_name": "El Corte S.A.S.",
  "nit": "900123456-7",
  "owner": { "name": "Laura Gómez", "email": "laura@elcorte.co" },
  "business_type": "barberia"
}

Respuesta 201 (o 200 si ya existía):

{
  "id": "biz_01J…",
  "external_ref": "user_cl9x8…",
  "trade_name": "Barbería El Corte",
  "livemode": false,
  "status": "active",
  "setup": { "branding_done": false, "program_done": false },
  "panel_invitation_sent": true
}

GET /v1/partner/businesses/{id}

Devuelve el estado del negocio. Mientras setup.program_done sea false, no hay programa activo y las operaciones se rechazan con program_not_configured.

POST /v1/partner/businesses/{id}/suspend · POST /v1/partner/businesses/{id}/resume

Se usan cuando cambia la suscripción en tu sistema. Mientras el negocio está suspendido:

POST /v1/partner/businesses/{id}/panel-sessions

Devuelve un enlace para que el dueño abra el panel de su negocio desde tu sistema, sin volver a iniciar sesión. Úsalo, por ejemplo, en un botón "Personalizar mi tarjeta".

{
  "url": "https://loyenta.buildev.co/panel/acceso?token=…",
  "expires_at": "2026-11-02T15:09:05Z"
}

6.2 Programas

GET /v1/programs?business_id=biz_…

Devuelve la configuración pública del programa: nombre, sellos requeridos, recompensa, enlace y QR de inscripción del negocio. Sirve para mostrarla en tu interfaz.

{
  "data": [{
    "id": "prog_01J…",
    "name": "Tarjeta de sellos",
    "stamps_required": 10,
    "reward_description": "Un corte gratis",
    "enrollment_url": "https://loyenta.buildev.co/i/barberia-el-corte",
    "enrollment_qr_png": "https://loyenta.buildev.co/i/barberia-el-corte/qr.png"
  }]
}

6.3 Clientes e inscripciones

La inscripción la acepta el cliente, no el negocio. La Ley 1581 exige la autorización del titular, y Loyenta debe poder probarla. Hay dos caminos:

A. Invitación (recomendado). Tu sistema pide un enlace personalizado y se lo hace llegar al cliente. El cliente lo abre, ve las condiciones prellenadas con su nombre y celular, deja su correo y acepta.

POST /v1/enrollment-invitations

{
  "business_id": "biz_…",
  "program_id": "prog_…",
  "customer": { "name": "Ana Pérez", "phone": "+573001234567", "external_ref": "client_cm1…" }
}
{
  "enrollment_url": "https://loyenta.buildev.co/i/barberia-el-corte?inv=4f9c…",
  "expires_at": "2026-11-03T23:59:59-05:00",
  "already_enrolled": false
}

Para no tener que pedir una invitación por cliente, también puedes mostrar el QR del negocio (enrollment_qr_png de §6.2): el cliente lo escanea y se inscribe solo, escribiendo su nombre y celular.

B. Inscripción directa con evidencia. Solo si tu propia interfaz le muestra al cliente el texto legal de Loyenta y el cliente lo acepta allí mismo. Debes enviar la evidencia: la versión del texto, la fecha, el canal y la IP.

POST /v1/enrollments

{
  "business_id": "biz_…",
  "program_id": "prog_…",
  "customer": { "name": "Ana Pérez", "phone": "+573001234567", "email": "ana@correo.co", "external_ref": "client_cm1…" },
  "consent": {
    "policy_version": "2026-11-01",
    "accepted_at": "2026-11-02T10:15:00-05:00",
    "channel": "partner_app",
    "ip": "190.0.0.1",
    "marketing_opt_in": false
  }
}

Responde 201 con la inscripción (el mismo objeto de abajo), o 200 si ese celular ya estaba inscrito en el negocio.

GET /v1/enrollments/{id} · GET /v1/enrollments?business_id=…&phone=+57…

GET /v1/enrollments/{id} devuelve una inscripción:

{
  "id": "enr_…",
  "business_id": "biz_…",
  "program_id": "prog_…",
  "customer": { "name": "Ana Pérez", "phone": "+573001234567", "external_ref": "client_cm1…" },
  "stamps": { "current": 7, "required": 10, "carryover": 0 },
  "rewards_available": [],
  "card": {
    "web_url": "https://loyenta.buildev.co/t/…",
    "google_wallet_url": "https://pay.google.com/gp/v/save/…"
  },
  "created_at": "2026-11-02T10:15:00.000Z"
}

GET /v1/enrollments?business_id=… devuelve la lista en data ({ "data": [ … ] }). Con &phone= trae como máximo una: la de ese celular, o data vacío si no está inscrito. Es la forma de saber si un cliente ya tiene tarjeta. Recuerda codificar el + del celular en la URL (%2B57…).

6.4 Operaciones (el corazón de la integración)

POST /v1/operations

Envía lo que ocurrió, no cuántos sellos dar.

{
  "business_id": "biz_…",
  "source_ref": "txn_clx7…",
  "type": "purchase_paid",
  "occurred_at": "2026-11-02T15:04:05-05:00",
  "customer": { "phone": "+573001234567", "external_ref": "client_cm1…" },
  "amount": { "value": 45000, "currency": "COP" },
  "items": [
    { "ref": "svc_1", "name": "Corte clásico", "category": "service", "quantity": 1, "unit_price": 30000 },
    { "ref": "svc_2", "name": "Cera", "category": "product", "quantity": 1, "unit_price": 15000 }
  ],
  "metadata": { "tip": 5000 }
}

Tipos de operación (type):

Tipo Cuándo
purchase_paid Una venta cobrada.
service_completed Un servicio realizado.
visit_attended Una cita o visita atendida.

Campos:

Respuesta 201:

{
  "id": "op_…",
  "source_ref": "txn_clx7…",
  "status": "accepted",
  "result": {
    "enrollment_id": "enr_…",
    "stamps_awarded": 1,
    "stamps": { "current": 8, "required": 10, "carryover": 0 },
    "rewards_unlocked": []
  }
}

Si ya habías enviado ese source_ref, responde 200 con el resultado original (ver §7).

Una operación válida que no da sello no es un error. Responde 201 con status: "rejected" y un rejection_code:

{
  "id": "op_…",
  "source_ref": "txn_clx8…",
  "status": "rejected",
  "rejection_code": "customer_not_enrolled",
  "enrollment_url": "https://loyenta.buildev.co/i/barberia-el-corte",
  "result": { "enrollment_id": null, "stamps_awarded": 0, "stamps": null, "rewards_unlocked": [] }
}

enrollment_url solo viene con customer_not_enrolled: es la página de inscripción del negocio, para ofrecérsela al cliente.

rejection_code Qué significa Qué hacer
customer_not_enrolled El cliente no está inscrito. Esa operación no dará sellos aunque el cliente se inscriba después: los sellos cuentan desde la inscripción Mostrar u ofrecer el enrollment_url que viene en la respuesta
program_not_configured El negocio aún no configuró su programa Recordarle al negocio que termine la configuración
not_eligible No cumple las reglas (tipo, categoría, monto mínimo o días) Nada: es lo esperado
daily_limit_reached El cliente ya alcanzó el tope de sellos del día Nada
business_suspended El negocio está suspendido Revisar la suscripción

GET /v1/operations/{id} · GET /v1/operations?business_id=…&source_ref=…

Consulta el resultado de una operación, por ejemplo después de un timeout. El de {id} devuelve el mismo objeto que el POST; el listado lo devuelve en data.

POST /v1/operations/{id}/reversal

Anula una operación, por ejemplo una venta borrada o devuelta.

{ "reason": "Venta eliminada en el sistema de origen" }

Responde 200 con la operación, ahora con status: "reversed" y el detalle de la anulación:

{
  "id": "op_…",
  "source_ref": "txn_clx7…",
  "status": "reversed",
  "result": { "enrollment_id": "enr_…", "stamps_awarded": 1, "stamps": { "current": 7, "required": 10, "carryover": 0 }, "rewards_unlocked": [] },
  "reversal": { "reason": "Venta eliminada en el sistema de origen", "reversed_at": "2026-11-02T21:00:00.000Z", "stamps_removed": 1, "discrepancy": 0 }
}

stamps_removed son los sellos que se restaron de verdad; discrepancy, los que no se pudieron restar porque la recompensa ya se había canjeado.

6.5 Recompensas y canjes

GET /v1/enrollments/{id}/rewards

Lista las recompensas disponibles del cliente, en data:

{
  "data": [{
    "id": "rwd_…",
    "enrollment_id": "enr_…",
    "description": "Un corte gratis",
    "status": "available",
    "unlocked_at": "2026-11-02T20:04:06.000Z",
    "redeemed_at": null
  }]
}

POST /v1/rewards/{id}/redemptions

Canjea una recompensa. El cuerpo es opcional.

{ "source_ref": "canje_123", "redeemed_by": "Laura (caja)" }

Respuesta 201: la recompensa ya canjeada y cómo queda la tarjeta.

{
  "reward": { "id": "rwd_…", "enrollment_id": "enr_…", "description": "Un corte gratis", "status": "redeemed", "unlocked_at": "…", "redeemed_at": "2026-11-05T16:30:00.000Z" },
  "stamps": { "current": 2, "required": 10, "carryover": 0 }
}

7. Idempotencia y reintentos

Las redes fallan. Loyenta está diseñada para que reintentar sea seguro:


8. Errores

Formato application/problem+json:

{
  "type": "https://loyenta.buildev.co/errores/validation_error",
  "title": "Datos inválidos",
  "status": 400,
  "code": "validation_error",
  "detail": "customer.phone: debe estar en formato E.164 (+57…)",
  "request_id": "req_01J…"
}
HTTP code Causa
400 validation_error Faltan campos o el formato es incorrecto
401 invalid_api_key Llave inválida o revocada
403 insufficient_scope La llave no tiene ese permiso
404 not_found No existe o tu credencial no tiene acceso a ese negocio
409 reward_already_redeemed, idempotency_conflict, program_not_configured Canje repetido, la misma clave con un cuerpo distinto, o una inscripción o invitación antes de que el negocio configure su programa
429 rate_limited Demasiadas peticiones: más de 600 por minuto con la misma llave (ver los headers RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset y Retry-After), o más de 100 negocios nuevos en 24 horas por modo
5xx internal_error Error de Loyenta: reintenta

Incluye siempre el request_id cuando reportes un problema.


9. Webhooks (opcional)

Si quieres enterarte de lo que pasa sin consultar, registra una URL en el panel (Integraciones → Webhooks).

Evento Cuándo
enrollment.created Un cliente completó su inscripción (por ejemplo, desde tu invitación)
stamp.awarded Se otorgaron sellos
reward.unlocked El cliente completó su tarjeta
reward.redeemed Se canjeó una recompensa
operation.rejected / operation.reversed Una operación fue rechazada o anulada
business.suspended / business.resumed Cambió el estado del negocio

Formato:

{
  "id": "evt_…",
  "type": "reward.unlocked",
  "occurred_at": "2026-11-02T20:04:06Z",
  "business_id": "biz_…",
  "data": { "enrollment_id": "enr_…", "reward_id": "rwd_…", "customer_external_ref": "client_cm1…" }
}

Verificación de la firma:

import { createHmac, timingSafeEqual } from "node:crypto";

export function firmaValida(cuerpoCrudo: string, cabecera: string, secreto: string): boolean {
  const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=") as [string, string]));
  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) return false;
  const esperada = createHmac("sha256", secreto).update(`${partes.t}.${cuerpoCrudo}`).digest("hex");
  return partes.v1?.length === esperada.length && timingSafeEqual(Buffer.from(partes.v1), Buffer.from(esperada));
}

10. Personalización por negocio

Cada negocio configura su programa en el panel de Loyenta. El enlace llega en la invitación, y el dueño también puede entrar con su correo y un código de un solo uso. Tu sistema no necesita construir esta configuración: basta con abrir el panel con un enlace de POST /v1/partner/businesses/{id}/panel-sessions (§6.1), así el dueño no vuelve a iniciar sesión. Más adelante habrá endpoints de partner para hacerlo desde tu interfaz.

10.1 Marca

Campo Requisito Dónde se ve
Nombre comercial Máximo 20 caracteres recomendado Encabezado de la tarjeta y de la Wallet
Logo Se sube desde el panel (JPG o PNG; el panel lo recorta en cuadrado y lo deja en 660×660 px) o se da un enlace público. Google lo recorta en círculo Tarjeta web y Wallet
Color principal #RRGGBB. Si es un color claro, el texto pasa solo a oscuro para que se lea Fondo de la tarjeta y de la Wallet
Color secundario #RRGGBB Tarjeta web y página de inscripción
Ícono de los sellos Uno de un catálogo (chulo, estrella, corazón, tijeras, café, flor…) o una imagen propia subida en el panel, distinta del logo Sellos de la tarjeta web y de la Wallet
Contacto Teléfono, WhatsApp, dirección y redes Detalle de la tarjeta

Lo que el negocio NO puede cambiar en Google Wallet (lo fija Google): la disposición de la tarjeta, la tipografía, la posición del QR y del logo. Loyenta muestra los sellos como una imagen generada con el color del negocio. En la tarjeta web el diseño es más libre.

10.2 Programa y reglas

Opción Ejemplo
Nombre del programa "Club El Corte"
Sellos requeridos 10
Recompensa "Un corte gratis"
Cómo se gana 1 sello por visita, 1 por cada $20.000 o 1 por cada servicio
Qué cuenta Solo servicios, solo ciertos servicios o todo
Monto mínimo y máximo Desde $20.000; se cuenta hasta $200.000
Topes Máximo 1 sello por día por cliente; máximo 3 por venta
Días válidos De lunes a jueves
Vencimientos La recompensa vence en 30 días; los sellos, a los 180 días sin visitas
Condiciones Texto libre del negocio (se genera un borrador a partir de las reglas)

Por eso tu integración envía la operación completa: tipo, monto e ítems con su categoría. Así cualquier regla que configure el negocio funciona sin cambiar tu código.


11. Lista de verificación de una integración

12. Cambios

Fecha Cambio
2026-10-27 Primer borrador del contrato v1.
2026-10-27 Nuevo POST /v1/partner/businesses/{id}/panel-sessions: enlace de un solo uso para abrir el panel desde el sistema del partner (§6.1 y §10).
2026-10-29 Los partners se registran solos y crean sus llaves de prueba y de producción en el portal de desarrolladores (§3.1 y §4); BUILDEV ya no las entrega a mano. Llega loy_live_ y la separación de datos por modo: los negocios traen el campo nuevo livemode (§6.1). Se activa el límite de peticiones (429, §8). La guía se publica en /desarrolladores/guia.
2026-10-28 El sandbox pasa a PostgreSQL y queda listo para montarse en un servidor (§4). Se documentan respuestas que ya existían, sin cambiar el contrato: listados en data, enrollment_url en las operaciones con customer_not_enrolled, el objeto reversal, la respuesta del canje (reward y stamps) y already_enrolled en las invitaciones (§6). En el panel: logo subido, ícono o imagen propia de los sellos y contraste automático del texto (§10.1).