PagoUno AffinityAPI DE CLUBES DE AFINIDAD · WHITE-LABEL

Sumá un club de afinidad a tu marketplace en horas, no meses.

PagoUno Affinity es la capa de fidelización + confianza con tu marca. Vos seguís siendo el dueño de la relación con tu cliente; nosotros ponemos los puntos, los niveles, el canje, el widget y la inteligencia antifraude. Una sola integración por API.

REST + JSON OAuth2 (client_credentials) Widget white-label Señal antifraude incluida Multi-marketplace

Conceptos clave

🧑
PersonaUn humano = una identidad única (la PersonId). El socio es la Persona, no la cuenta. Tu marketplace referencia siempre la PersonId.
🎟️
ClubTu club, con tu marca, tu modo de enrolamiento, tus niveles y tus reglas de puntos. Un club por marketplace (o más).
⭐
MembresíaLa relación Persona↔Club: estado, nivel (tier), saldo de puntos e historial (ledger).
💳
EarnReportás la compra; el motor aplica tu regla, acumula puntos y recalcula el nivel.
🎁
CanjeEl socio canjea por un voucher en TU checkout o por un premio del catálogo. Vos definís el catálogo.
🛡️
ConfianzaEl historial de socio se vuelve una señal antifraude 0–100, agregada de forma segura entre marketplaces.

Quickstart — integrá en 4 pasos

1
Pedí un token de servicio (M2M) con scope affinity.marketplace. Ver Autenticación.
2
Configurá tu club (nombre, marca, modo de enrolamiento, reglas y niveles) — desde el panel de PagoUno o por API. Te quedás con el ClubId.
3
Reportá cada compra con POST /admin/clubs/{clubId}/earn: el socio acumula puntos automáticamente (y se enrola solo si tu club es AUTO).
4
Mostrá el widget en tu sitio (tarjeta de socio, puntos, catálogo y canje) — ya tematizado con tu marca. Ver Widget.
Base URL de la API: https://affinity.pagouno.io · Emisor de tokens (IdP): https://auth.pagouno.io

Autenticación

Toda la API se protege con OAuth2. El backend de tu marketplace usa client_credentials (M2M) con el scope affinity.admin. Te entregamos client_id y client_secret.

# 1) Obtené un access token (válido ~1h)
curl -X POST https://auth.pagouno.io/connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=TU_CLIENT_ID" \
  -d "client_secret=TU_CLIENT_SECRET" \
  -d "scope=affinity.admin"

# → { "access_token": "eyJ...", "expires_in": 3600, "token_type": "Bearer" }

# 2) Usalo como Bearer en cada request
curl https://affinity.pagouno.io/admin/clubs \
  -H "Authorization: Bearer eyJ..."
Cacheá el token y renovalo ~60s antes del expires_in. Nunca expongas el client_secret en el front-end.

Scopes

ScopeQuién lo usaPara qué
affinity.marketplaceBackend del marketplaceReportar compras (earn), validar socio por DNI y operar vouchers en el checkout. El scope recomendado para integraciones — no administra clubes.
affinity.adminBO / operaciones PagoUnoConfigurar clubes, tiers, reglas, planes y socios (superset: también pasa por los endpoints de marketplace).
affinity.memberEl socio (token CIAM)Ver su membresía/puntos, el catálogo y canjear. Lo usa el widget.
affinity.trust.readMotor antifraudeLeer la señal de confianza del socio (uso interno de PagoUno).

Acumular puntos (earn)

Es el endpoint que más vas a usar: reportá cada compra confirmada y el motor acumula puntos según tu regla. Si tu club es AUTO, el socio se enrola solo en su primera compra.

MétodoEndpointScope
POST/admin/clubs/{clubId}/earnaffinity.admin
curl -X POST https://affinity.pagouno.io/admin/clubs/{clubId}/earn \
  -H "Authorization: Bearer eyJ..." -H "Content-Type: application/json" \
  -d '{
    "personId": "7bd04518-47e5-4907-9df5-ee8e4fbe429a",
    "amount": 12500.00,
    "refId": "order_98412",
    "autoEnroll": true
  }'

# → { "Result":"OK", "MembershipId":"...", "PointsEarned":125, "BalanceAfter":1250, "LifetimePoints":4300 }
Idempotencia: enviá tu refId (id de la orden) para poder conciliar. Result puede ser OK, NOT_MEMBER, NOT_ACTIVE o CLUB_NOT_FOUND.
Vencimiento automático: los puntos caducan según expiryMonths del club (default 12 meses, FIFO). Es automático — no requiere integración de tu parte.

Membresía & catálogo (socio)

Endpoints que consume el front del socio (token affinity.member). El widget ya los usa por vos.

MétodoEndpointDescripción
GET/member/persons/{personId}/membershipsTodas las membresías del socio (cross-club).
GET/member/memberships/{id}Detalle: tier, saldo, antigüedad.
GET/member/memberships/{id}/ledgerHistorial de puntos.
GET/member/clubs/{clubId}/catalogRecompensas activas para canjear.
POST/member/clubs/{clubId}/opt-inEl socio se suma (clubes OPT_IN).
GET/public/clubs/{slug}Sin token: marca del club (para tematizar).
Propiedad del dato: el access token del socio trae el claim person_id (lo emite el IdP). La API coteja que el personId/membershipId operado sea el del token — pedir datos de otra persona devuelve 403. Usá siempre el personId del socio logueado (el widget ya lo hace por vos).

Canjear (redeem)

El socio canjea puntos por una recompensa. El motor valida saldo y stock, descuenta los puntos y emite el voucher.

MétodoEndpointScope
POST/member/memberships/{id}/redeemaffinity.member
curl -X POST https://affinity.pagouno.io/member/memberships/{id}/redeem \
  -H "Authorization: Bearer SOCIO_TOKEN" -H "Content-Type: application/json" \
  -d '{ "rewardId": "a1b2c3d4-..." }'

# → { "Result":"OK", "VoucherCode":"7F3A9C21B0E4", "PointsSpent":500, "BalanceAfter":750 }
Si la recompensa es voucher de marketplace, recibís un VoucherCode para aplicar el descuento en TU checkout. Si es catálogo PagoUno, el canje queda en REQUESTED y nosotros gestionamos el fulfillment. Errores posibles (HTTP 422): INSUFFICIENT_POINTS, NO_STOCK, REWARD_INACTIVE.

Señal de confianza (antifraude)

Cada socio acumula un score de confianza 0–100 a partir de su antigüedad, historial de compras limpio, nivel y amplitud entre clubes. La gracia: como la Persona es única en todo PagoUno, alguien con años de buen historial en otro club entra como bajo riesgo en tu marketplace aunque sea su primera compra.

MétodoEndpointScope
GET/trust/persons/{personId}affinity.trust.read
# Respuesta — solo el derivado normalizado, nunca PII ni "está en el club X"
{
  "PersonId": "7bd04518-...",
  "TrustScore": 82,
  "TenureDays": 540, "MaxTierRank": 3,
  "CleanPurchases": 36, "Clubs": 2,
  "ConsentShared": true
}
Privacidad por diseño. Nunca se expone información de otro marketplace: solo un número normalizado, y el cruce entre clubes se gobierna por el consentimiento del socio (ConsentShared). Anti-gaming: un socio recién creado pesa ≈ 0.

Admin de clubes (referencia)

Podés administrar todo desde el panel de PagoUno; también está disponible por API (scope affinity.admin).

MétodoEndpointDescripción
GET/admin/clubsListar clubes (filtrá por ?tenantId=).
POST/admin/clubsCrear un club (nombre, slug, modo, branding, config de puntos/canje).
PUT/admin/clubs/{clubId}Editar el club.
POST/admin/clubs/{clubId}/tiersCrear/editar niveles (umbral + perks).
POST/admin/clubs/{clubId}/rulesReglas de acumulación (pts por $, multiplicador).
POST/admin/clubs/{clubId}/rewardsCatálogo de recompensas.
GET/admin/clubs/{clubId}/membersListar socios (búsqueda + paginado).
GET/admin/clubs/{clubId}/members/validate/{dni}Validar un socio por DNI (puerta/POS/beneficios): devuelve estado, nro. de socio, nivel, saldo, nombre, email y flags de verificación (DNI y email) — o 404 si ese DNI no es socio del club. La validez la decidís vos con status/isActive.
POST/admin/members/{id}/statusActivar/suspender un socio.
POST/admin/redemptions/{id}/statusMarcar un canje como entregado.

Widget embebible (white-label)

La forma más rápida de mostrarle el club al socio: tarjeta con su nivel, puntos, catálogo y canje — ya con tu marca (se tematiza solo desde la config del club). Pegá esto donde quieras que aparezca:

<div id="mi-club"></div>
<script src="https://affinity.pagouno.io/widget/affinity-widget.js"></script>
<script>
  AffinityWidget.mount(document.getElementById('mi-club'), {
    apiBase:     'https://affinity.pagouno.io',
    clubSlug:    'club-mimarca',
    personId:    socio.personId,        // la PersonId del socio logueado
    memberToken: socio.accessToken      // token CIAM con scope affinity.member
  });
</script>
¿Tenés tu propio front? Usá la API del socio directamente. El widget es opcional: la plataforma es API-first.

Inscripción pública (widget de alta)

Para que una persona nueva se sume al club desde tu sitio (a diferencia del widget de socio, que es para alguien ya logueado). Flujo: pide el email → manda un código OTP → al validarlo muestra el formulario → da de alta al socio. La estética sale de una plantilla HTML/CSS editable (la hospedás vos).

👉 Ejemplo completo funcionando: /widget/inscripcion-demo.html — un mock de la página del sitio con el widget incrustado (mirá el HTML para copiar la integración).

1) Embebé el plugin

<div id="club-tnc"></div>
<script src="https://affinity.pagouno.io/widget/affinity-enroll.js"></script>
<script>
  AffinityEnroll.mount(document.getElementById('club-tnc'), {
    apiBase:     'https://affinity.pagouno.io',
    publicKey:   'pk_xxxxxxxx',                          // la public key del club
    templateUrl: 'https://tusitio.com/plantilla-club.html' // tu plantilla (estética libre)
  });
</script>
El .js lo podés servir desde un CDN (S3 + CloudFront) y pinnear una versión: affinity-enroll-1.0.0.js (cachea inmutable). El apiBase siempre apunta a affinity.
¿De dónde sale la publicKey? Del Backoffice → Clubes → panel “Plugin de inscripción” (ahí también se ven/editan los orígenes permitidos y el kill-switch), o por API con POST /admin/clubs/{clubId}/enrollment (scope affinity.admin). Se genera una sola vez y los updates no la rotan. No es secreta — puede ir en el HTML y en tu repo: el anti-abuso real es OTP + CAPTCHA + rate-limit + orígenes permitidos.

2) La plantilla (HTML/CSS libre + ganchos data-affinity-*)

La plantilla define el look; el plugin se engancha por atributos (no cambies sus nombres). Hay una de ejemplo en /widget/plantilla-club.example.html.

AtributoPara qué
data-affinity-step="email|otp|form|done|disabled"Contenedor de cada paso (el plugin muestra/oculta)
data-affinity-field="email|otp|displayName|dni|phone|hp"Inputs base (hp = honeypot anti-bot, dejalo oculto)
data-affinity-extra="clave"Campos extra del club (los que quieras): cualquier input / select / radio / checkbox. Se guardan como JSON en el perfil del socio. Ver abajo 👇
data-affinity-action="send-otp|verify-otp|resend|enroll|restart"Botones de cada acción
data-affinity-captchaContenedor del CAPTCHA (Cloudflare Turnstile)
data-affinity-msg · data-affinity-club-nameZona de mensajes · nombre del club (se rellena solo)

3) Campos extra del formulario (perfil del socio)

Además del email (verificado) y el DNI, tu club seguramente pide más datos en la inscripción (apellido, nombre, fecha de nacimiento, provincia, intereses, etc.). Agregalos en tu plantilla con data-affinity-extra="clave" y el plugin los junta automáticamente y los guarda como un JSON en el perfil del socio (ProfileJson), para que después se los puedas reportar/exportar al club.

<!-- en el paso data-affinity-step="form" -->
<input type="text"   data-affinity-extra="apellido">
<input type="text"   data-affinity-extra="nombre">
<input type="date"   data-affinity-extra="fechaNacimiento">
<select data-affinity-extra="provincia">…</select>
<!-- radio: el plugin toma la opción marcada -->
<input type="radio" name="interesEmail" value="Sí" data-affinity-extra="interesEmail">
<input type="radio" name="interesEmail" value="No" data-affinity-extra="interesEmail">

Eso queda guardado así en el perfil del socio:

{
  "apellido": "Pérez",
  "nombre": "Juan",
  "fechaNacimiento": "1990-05-12",
  "provincia": "Buenos Aires",
  "interesEmail": "Sí"
}
  • Soporta input (text/email/date/tel…), select, radio (toma el marcado) y checkbox.
  • Si no ponés un data-affinity-field="displayName" pero sí los extra nombre y/o apellido, el plugin arma el nombre con esos (así podés tener Nombre/Apellido separados).
  • Límites por inscripción: hasta 30 campos, valor de hasta 500 caracteres; los vacíos se descartan.
  • Plantilla de ejemplo con el formulario completo del TNC: /widget/plantilla-club.example.html.

4) Credenciales y anti-abuso

  • Public key por club: identifica el club (va en el JS, no es secreta). Se provisiona con POST /admin/clubs/{clubId}/enrollment (orígenes permitidos + on/off) y permite cortar un solo club ante abuso (kill-switch).
  • CAPTCHA (Turnstile): obligatorio en producción — sin él, el endpoint público de OTP es abusable. El plugin lo renderiza solo si está configurado.
  • Rate-limit por IP/email/club + OTP obligatorio (sin código válido no hay alta) + respuestas uniformes (no filtran si un email existe).
El templateUrl tiene que permitir CORS (o estar en el mismo origen que tu página). El <script> del plugin no necesita CORS.

5) Credencial digital (Apple Wallet / Google Wallet)

Al enrolar, el socio recibe una credencial digital con el nombre, DNI, número de socio, puntos y QR — brandeada con los colores del club — que puede guardar en Apple Wallet o Google Wallet. Los puntos se actualizan solos en el teléfono cuando el socio acumula o canjea.

  • El mail de bienvenida incluye el botón “Agregar a Apple / Google Wallet” automáticamente.
  • La respuesta del alta (POST /public/enroll/{publicKey}/submit) ahora trae walletUrl (además de status: "enrolled"): mostrala en tu paso done si querés ofrecer el botón ahí mismo. Si la credencial no está disponible, viene null — el alta funciona igual.
  • El branding sale del BrandingJson del club: primaryColor (fondo), accentColor (etiquetas), logoUrl y stripUrl (imagen superior, PNG por HTTPS).

Cobro recurrente (webhook)

Para las membresías pagas, PagoUno orquesta el ciclo (agenda, factura, reintentos) y tu sitio ejecuta el cobro. Cuando vence un período, affinity hace un POST firmado a tu webhook de cobro; vos cobrás contra tu PSP y respondés el resultado. Configurás la URL y el secreto por club en el panel (Clubes → Cobro).

# Lo que affinity POSTea a tu webhook (header de firma incluido):
POST https://tusitio.com/affinity/charge
X-Affinity-Signature: <HMAC-SHA256 hex del body crudo, con el secreto del club>
{
  "invoiceId": "...", "subscriptionId": "...",
  "clubId": "...", "personId": "...",
  "amount": 1500.00, "currency": "ARS",
  "paymentRef": "tok_visa_4242",
  "periodStart": "2026-07-01T00:00:00Z", "periodEnd": "2026-08-01T00:00:00Z"
}

# Tu respuesta (sincrónica):
{ "result": "PAID", "chargeRef": "ch_abc123" }
# o, si no pudiste cobrar:
{ "result": "FAILED", "reason": "card_declined" }

Verificá la firma (referencia, Node)

app.post('/affinity/charge', (req, res) => {
  const sig = crypto.createHmac('sha256', CLUB_SECRET)
                   .update(req.rawBody).digest('hex');
  if (sig !== req.headers['x-affinity-signature']) return res.status(401).end();

  const r = await psp.charge(req.body.paymentRef, req.body.amount, req.body.currency);
  res.json(r.ok ? { result: 'PAID', chargeRef: r.id }
                : { result: 'FAILED', reason: r.error });
});
¿Cobro asíncrono? Respondé el webhook y, cuando tengas el resultado, hacé POST /billing/callback con { invoiceId, result, chargeRef?, reason? } y la misma firma X-Affinity-Signature sobre el body.
Dunning automático: ante un FAILED, affinity reintenta según el plan (reintentos + días de gracia configurables); agotados, suspende la membresía. No tenés que manejar reintentos.

Canje en tu checkout (vouchers)

Cuando un socio canjea una recompensa de tipo voucher de marketplace, recibe un VoucherCode. En tu checkout: validás el voucher, aplicás el beneficio y lo consumís para que no se reuse. (M2M, scope affinity.admin.)

MétodoEndpointDescripción
GET/vouchers/{code}Validar: devuelve Valid, VoucherKind (PERCENT/FIXED/ITEM), VoucherValue y estado.
POST/vouchers/{code}/consumeMarcar usado al aplicarlo. Idempotente (ALREADY_USED si ya se usó).
# 1) validar al ingresar el código en el checkout
curl https://affinity.pagouno.io/vouchers/7F3A9C21B0E4 \
  -H "Authorization: Bearer eyJ..."
# → { "Valid": true, "VoucherKind": "PERCENT", "VoucherValue": 20, "RewardName": "20% OFF", "Status": "ISSUED" }

# 2) aplicás el 20% en tu checkout y lo consumís
curl -X POST https://affinity.pagouno.io/vouchers/7F3A9C21B0E4/consume \
  -H "Authorization: Bearer eyJ..." -H "Content-Type: application/json" \
  -d '{ "fulfillmentJson": "{\"orderId\":\"98412\"}" }'
# → { "Result": "OK", "Status": "FULFILLED" }
Validá siempre con Valid: true antes de aplicar, y consumí después de confirmar la compra. Si consume devuelve ALREADY_USED, no apliques el beneficio.

Errores

CódigoSignificado
200OK. Las operaciones de negocio devuelven además un campo Result.
401Token ausente, vencido o inválido. Pedí uno nuevo.
403El token no tiene el scope necesario.
404El recurso (club, membresía, recompensa) no existe.
422Regla de negocio: saldo insuficiente, sin stock, recompensa inactiva.
5xxError nuestro. Reintentá con backoff; nunca lo trates como rechazo de negocio.

Checklist de go-live

① CredencialesRecibís client_id/secret (M2M) y tu ClubId.
② Earn en el checkoutLlamás /earn al confirmar cada compra.
③ CatálogoCargás recompensas y reglas de puntos en el panel.
④ Widget o APIMostrás el club al socio.
⑤ CanjeAplicás el VoucherCode en tu checkout.
⑥ ListoTu marketplace ya tiene club de afinidad + antifraude.