PagoUno WalletCREDENCIALES DIGITALES · APPLE & GOOGLE WALLET

Tus entradas y credenciales, vivas en el teléfono de tu cliente.

PagoUno Wallet emite passes de Apple Wallet y Google Wallet con tu marca y los mantiene actualizados en vivo: cambia la butaca, la función o los puntos, y la credencial se actualiza sola en el teléfono — con notificación. Una sola integración REST para ambos mundos.

REST + JSON OAuth2 (client_credentials) Apple Wallet Google Wallet Actualización en vivo White-label

Conceptos clave

🎫
PassUna credencial en el teléfono. Tipos: event-ticket (entrada de evento) y club-membership (credencial de socio).
🔑
sourceRefEl id del pase en TU sistema. Es la clave de idempotencia: re-emitir con el mismo sourceRef refresca el pass sin duplicarlo (los links ya enviados siguen vivos).
🗂️
groupRefAgrupa passes que comparten branding y datos: para entradas es el id del evento/función; para clubes, el club. Define la "clase" de Google.
🎨
BrandColores, logos e imagen principal de TU marca, por pase. El mismo servicio sirve a N marketplaces white-label.
✉️
LandingLa url que devuelve la emisión es pública y viaja en tu mail: detecta el dispositivo y ofrece el botón correcto (Apple/Google).
⚡
En vivoCada sync empuja el cambio a los teléfonos: push APNs en iPhone, actualización + notificación en Android.

Quickstart — integrá en 4 pasos

1
Pedí un token de servicio (M2M) con scope wallet.issue. Ver Autenticación.
2
Emití el pass con POST /passes al confirmar la venta/alta — te devuelve la url de la landing.
3
Mandá esa url en tu mail de confirmación ("Agregá tu entrada al teléfono"). El cliente toca y la guarda en su wallet.
4
Sincronizá cuando algo cambie (butaca, horario, puntos) con POST /passes/sync — el teléfono se actualiza solo.
Base URL de la API: https://wallet.pagouno.io · Emisor de tokens (IdP): https://auth.pagouno.io · Colección Postman lista para importar: docs/pagouno.wallet.postman_collection.json (en el repo del servicio).

Autenticación

Todo el ABM de passes se protege con OAuth2. El backend de tu marketplace usa client_credentials (M2M) con el scope wallet.issue. 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=wallet.issue"

# → { "access_token": "eyJ...", "expires_in": 3600, "token_type": "Bearer" }
Cacheá el token y renovalo ~60s antes del expires_in. Nunca expongas el client_secret en el front-end. Las URLs de landing/descarga que devuelve el servicio son públicas a propósito (capability URLs para tu mail).

Scopes

ScopeQuién lo usaPara qué
wallet.issueBackend del marketplaceEmitir, modificar y dar de baja passes (los 3 endpoints).

Emitir un pass

MétodoEndpointScope
POST/passeswallet.issue

Idempotente por (passType, sourceRef): re-emitir refresca datos y branding, conserva el mismo passId y revive un pass revocado. Respuesta:

{ "passId": "…", "created": true,
  "url": "https://wallet.pagouno.io/p/{passId}",          ← la landing (mandá ESTE link)
  "appleUrl": "…/p/{passId}/apple.pkpass", "googleUrl": "…/p/{passId}/google" }

La validación es estricta: si falta un dato requerido la API devuelve 400 con la lista missing exacta — un pase a medias se ve roto en el teléfono.

Entradas de evento (event-ticket)

CampoReq.Descripción
passTypeSÍ"event-ticket"
sourceRefSÍId de la entrada en TU sistema (máx. 80)
groupRefSÍId del EVENTO/función — agrupa las entradas que comparten nombre, venue, fecha y branding
brand.nameSÍMarca del emisor (ver Branding)
accountNameSÍTitular de la entrada
accountIdSÍNúmero de entrada (visible)
barcode.messageSÍContenido del QR (tu código de control de acceso). altText opcional
event.eventNameSÍNombre del espectáculo/función
event.venueNameSÍSala/estadio
event.venueAddressSÍDirección de la sala (Google exige nombre y dirección juntos)
event.startsAtSÍInicio ISO 8601 con el offset LOCAL del venue (2026-08-15T20:30:00-03:00) — la tarjeta muestra la hora tal cual viene. En iOS activa la sugerencia del lock screen a la hora de la función
event.endsAtnoFin de la función (mismo formato)
event.section / row / seat / gatenoSector / fila / butaca / puerta. Omitir en entrada general
fields.*noEtiquetas visibles en Apple (header/primary/secondary/auxiliary/back), cada una {key,label,value,changeMessage?}. En Google van como filas de texto
personId / tenantIdnoPersona canónica de PagoUno (si la tenés) · tenant del marketplace
curl -X POST https://wallet.pagouno.io/passes \
  -H "Authorization: Bearer eyJ..." -H "Content-Type: application/json" \
  -d '{
    "passType": "event-ticket",
    "sourceRef": "TCK-2026-000123",
    "groupRef": "EVT-tempestad-2026-08-15-2030",
    "brand": {
      "name": "TNC Club", "subtitle": "Teatro Nacional Cervantes",
      "backgroundColor": "#0a1423", "labelColor": "#daac59",
      "logoUrl": "https://…/pass/apple/logo@2x-v1.png",
      "googleLogoUrl": "https://…/pass/google/logo-660-v1.png",
      "stripUrl": "https://…/pass/apple/strip@2x-v1.png"
    },
    "accountName": "Carolina Zuzunegui", "accountId": "000123",
    "barcode": { "message": "TCK-2026-000123|c0d1g0", "altText": "000123" },
    "event": {
      "eventName": "La Tempestad", "venueName": "Teatro Nacional Cervantes",
      "venueAddress": "Libertad 815, CABA",
      "startsAt": "2026-08-15T20:30:00-03:00", "endsAt": "2026-08-15T22:30:00-03:00",
      "section": "Platea", "row": "7", "seat": "12", "gate": "Puerta 2"
    },
    "fields": {
      "primary":   [{ "key": "event", "label": "FUNCIÓN", "value": "La Tempestad" }],
      "secondary": [{ "key": "date", "label": "FECHA", "value": "15/08 20:30",
                      "changeMessage": "Tu función cambió: %@" },
                    { "key": "seat", "label": "UBICACIÓN", "value": "Platea F7 B12" }],
      "back":      [{ "key": "terms", "label": "Condiciones", "value": "Presentate 30 min antes…" }]
    }
  }'

Credencial de club (club-membership)

Mismo contrato con personId REQUERIDO (la credencial de socio se ancla en la Persona canónica de PagoUno), groupRef = ClubId, y points/pointsLabel para el saldo (en Google se muestra como loyaltyPoints). La integración de los clubes de afinidad ya la hace PagoUno Affinity — emití directo solo si tenés un programa propio.

Modificar (sync)

MétodoEndpointScope
POST/passes/syncwallet.issue

Body: { passType, sourceRef } + solo lo que cambia (merge server-side). Dentro de event el merge también es campo a campo: mandar solo la butaca no pisa el resto. Para limpiar un campo (p. ej. sacar la butaca), re-emití con POST /passes.

curl -X POST https://wallet.pagouno.io/passes/sync \
  -H "Authorization: Bearer eyJ..." -H "Content-Type: application/json" \
  -d '{
    "passType": "event-ticket", "sourceRef": "TCK-2026-000123",
    "event": { "seat": "14" },
    "fields": { "secondary": [{ "key": "seat", "label": "UBICACIÓN",
      "value": "Platea F7 B14", "changeMessage": "Tu ubicación cambió: %@" }] }
  }'

# → { "synced": true, "applePushed": true, "googleSynced": true }
404 = nunca emitiste ese (passType, sourceRef). Sobre un pass revocado el sync responde 200 (el pass sigue existiendo, solo está de baja).

Baja (revoke)

MétodoEndpointScope
POST/passes/revokewallet.issue

Body: { passType, sourceRef }. El pass queda voided en Apple (tachado), INACTIVE en Google (expirado) y la landing devuelve 404. Re-emitir con el mismo sourceRef lo reactiva con los mismos links.

Landing pública

MétodoEndpointAuth
GET/p/{passId}Pública (capability URL)
GET/p/{passId}/apple.pkpassPública — descarga el pass firmado
GET/p/{passId}/googlePública — redirect al "Guardar en Google Wallet"

La landing se muestra con tu branding y detecta el dispositivo: en Android el botón principal es Google Wallet, en iPhone el de Apple (el otro queda como alternativa chica). En desktop se muestran los dos.

Branding white-label (imágenes por marca)

ClaveUsoEspecificación
brand.logoUrlLogo del pass de Apple (header)PNG apaisado ~320×100, https público
brand.googleLogoUrlLogo de la tarjeta de GooglePNG cuadrado ~660×660 (Google lo recorta en círculo)
brand.stripUrlImagen principal (strip de Apple + hero de Google)PNG máx. 2 MB — clubes: 750×246 · entradas: ideal 750×196 (el strip de eventTicket es más chato; iOS recorta si no coincide). En event-ticket con strip presente, el pass de Apple omite los fields.primary (iOS los superpone a la imagen): el arte debe llevar el nombre del show. Sin stripUrl, las entradas salen con una banda genérica del servicio (escenario abstracto) y el título del evento dibujado encima
brand.backgroundColor / labelColor / foregroundColorColoresHex #rrggbb
brand.subtitleNombre legal del emisorSale en las notificaciones de iOS y como issuerName de Google
⚠ Versioná el NOMBRE del archivo (strip@2x-v1.png → -v2.png) al cambiar una imagen — hay caché por URL en el servicio y en los CDN: pisar la misma URL no refresca nada. Todas las imágenes: https, host público, PNG real. Un cambio de branding llega a los passes YA emitidos en el próximo sync.

QR rotativo (anti-screenshot)

Opcional y a pedido en la creación: agregá rotatingBarcode al body de POST /passes. El QR cambia solo cada periodSeconds por TOTP (RFC 6238) — un screenshot deja de servir enseguida. El scanner de tu control de acceso valida el mismo TOTP con el secreto compartido.

CampoReq.Descripción
secretHexnoSecreto TOTP en hex (el que usa TU validador). Si lo omitís, el servicio genera uno de 20 bytes y te lo devuelve en rotatingBarcodeSecret
periodSecondsnoCada cuánto rota (default 60; 1–3600)
valueLengthnoDígitos del código (6–8, default 8)
valuePatternnoPatrón Google (default {totp_value_0})
altTextnoTexto alternativo bajo el QR
# Agregado al body de POST /passes (reemplaza al barcode estático)
"rotatingBarcode": { "periodSeconds": 60, "valueLength": 8 }

# Respuesta incluye el secreto para tu validador:
# { "passId":"…", "rotatingBarcodeSecret":"a1b2c3…(hex)" }
Google: soporte nativo (el QR anima con un shimmer de seguridad). Apple: iOS no tiene barcode rotativo — el pass de Apple muestra el QR estático del campo barcode (usá NFC o QR rotativo solo-Google si necesitás anti-screenshot fuerte en iPhone).
El secreto no rota en los sync — si actualizás el pass sin mandar secretHex, se conserva el vigente (si rotara, tu validador dejaría de coincidir).

NFC / tap

Opcional: agregá nfc al body para habilitar la redención por acercamiento del teléfono al lector.

"nfc": { "message": "TCK-2026-000123", "requiresAuthentication": false }
Google (Smart Tap): requiere onboarding de Smart Tap con Google — una vez habilitado, el tap funciona en Android. El Collector ID y las claves del lector se configuran en tus terminales (ej. Zebra), no acá. Apple: el NFC en Apple Wallet requiere un entitlement especial de Apple (partners de acceso/fidelización); sin él, el pass de Apple sale sin NFC (no rompe nada). Pedinos habilitar el canal antes de usar nfc.

Actualización en vivo — cómo llega el cambio al teléfono

📱
iPhone (Apple Wallet)Cada sync dispara un push APNs → el iPhone re-baja el pass. Si el campo que cambió tenía changeMessage, iOS muestra la notificación en el lock screen (%@ = valor nuevo).
🤖
Android (Google Wallet)El objeto se actualiza por API (silencioso) y, si cambian puntos o datos del evento, va un mensaje TEXT_AND_NOTIFY que sí notifica (Google lo limita a ~3 por tarjeta por día).
🕗
Lock screenEn entradas, event.startsAt hace que iOS sugiera el pass en el lock screen cuando se acerca la función.

Errores

CódigoCuándoQué hacer
400Faltan datos requeridos al emitirLa respuesta trae missing[] con cada campo faltante — completá y reintentá
401Token vencido o sin scope wallet.issueRenová el token / pedí el scope para tu cliente
404Sync/revoke de un (passType, sourceRef) nunca emitido; landing de un pass revocadoEmití primero / verificá el sourceRef

Checklist de go-live

① Cliente M2MPedí tu client_id/client_secret con scope wallet.issue.
② ImágenesSubí logo/strip de tu marca a un CDN https con nombre versionado (-v1).
③ EmitíProbá con la colección Postman: entrada demo → landing → guardala en tu teléfono.
④ MailSumá la url de la landing a tu mail de confirmación de compra.
⑤ SyncCableá el sync a tus cambios (butaca, horario, reprogramación).
⑥ BajaCableá el revoke a la anulación/devolución de la entrada.