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.
Conceptos clave
event-ticket (entrada de evento) y club-membership (credencial de socio).sourceRef refresca el pass sin duplicarlo (los links ya enviados siguen vivos).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).sync empuja el cambio a los teléfonos: push APNs en iPhone, actualización + notificación en Android.Quickstart — integrá en 4 pasos
POST /passes al confirmar la venta/alta — te devuelve la url de la landing.url en tu mail de confirmación ("Agregá tu entrada al teléfono"). El cliente toca y la guarda en su wallet.POST /passes/sync — el teléfono se actualiza solo.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" }
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
| Scope | Quién lo usa | Para qué |
|---|---|---|
| wallet.issue | Backend del marketplace | Emitir, modificar y dar de baja passes (los 3 endpoints). |
Emitir un pass
| Método | Endpoint | Scope |
|---|---|---|
| POST | /passes | wallet.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)
| Campo | Req. | Descripción |
|---|---|---|
passType | SÍ | "event-ticket" |
sourceRef | SÍ | Id de la entrada en TU sistema (máx. 80) |
groupRef | SÍ | Id del EVENTO/función — agrupa las entradas que comparten nombre, venue, fecha y branding |
brand.name | SÍ | Marca del emisor (ver Branding) |
accountName | SÍ | Titular de la entrada |
accountId | SÍ | Número de entrada (visible) |
barcode.message | SÍ | Contenido del QR (tu código de control de acceso). altText opcional |
event.eventName | SÍ | Nombre del espectáculo/función |
event.venueName | SÍ | Sala/estadio |
event.venueAddress | SÍ | Dirección de la sala (Google exige nombre y dirección juntos) |
event.startsAt | SÍ | 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.endsAt | no | Fin de la función (mismo formato) |
event.section / row / seat / gate | no | Sector / fila / butaca / puerta. Omitir en entrada general |
fields.* | no | Etiquetas visibles en Apple (header/primary/secondary/auxiliary/back), cada una {key,label,value,changeMessage?}. En Google van como filas de texto |
personId / tenantId | no | Persona 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étodo | Endpoint | Scope |
|---|---|---|
| POST | /passes/sync | wallet.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 }
(passType, sourceRef). Sobre un pass revocado el sync responde 200
(el pass sigue existiendo, solo está de baja).Baja (revoke)
| Método | Endpoint | Scope |
|---|---|---|
| POST | /passes/revoke | wallet.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étodo | Endpoint | Auth |
|---|---|---|
| GET | /p/{passId} | Pública (capability URL) |
| GET | /p/{passId}/apple.pkpass | Pública — descarga el pass firmado |
| GET | /p/{passId}/google | Pú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)
| Clave | Uso | Especificación |
|---|---|---|
brand.logoUrl | Logo del pass de Apple (header) | PNG apaisado ~320×100, https público |
brand.googleLogoUrl | Logo de la tarjeta de Google | PNG cuadrado ~660×660 (Google lo recorta en círculo) |
brand.stripUrl | Imagen 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 / foregroundColor | Colores | Hex #rrggbb |
brand.subtitle | Nombre legal del emisor | Sale en las notificaciones de iOS y como issuerName de Google |
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.
| Campo | Req. | Descripción |
|---|---|---|
secretHex | no | Secreto 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 |
periodSeconds | no | Cada cuánto rota (default 60; 1–3600) |
valueLength | no | Dígitos del código (6–8, default 8) |
valuePattern | no | Patrón Google (default {totp_value_0}) |
altText | no | Texto 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)" }
barcode
(usá NFC o QR rotativo solo-Google si necesitás anti-screenshot fuerte en iPhone).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 }
nfc.Actualización en vivo — cómo llega el cambio al teléfono
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).event.startsAt hace que iOS sugiera el pass en el lock screen cuando se acerca la función.Errores
| Código | Cuándo | Qué hacer |
|---|---|---|
400 | Faltan datos requeridos al emitir | La respuesta trae missing[] con cada campo faltante — completá y reintentá |
401 | Token vencido o sin scope wallet.issue | Renová el token / pedí el scope para tu cliente |
404 | Sync/revoke de un (passType, sourceRef) nunca emitido; landing de un pass revocado | Emití primero / verificá el sourceRef |
Checklist de go-live
client_id/client_secret con scope wallet.issue.-v1).url de la landing a tu mail de confirmación de compra.sync a tus cambios (butaca, horario, reprogramación).revoke a la anulación/devolución de la entrada.