Google Wallet: Class y Object explicados con casos reales
La Class guarda lo común del grupo (evento, marca, lugar); el Object, lo del titular. Y cada tipo tiene su recurso: usar el path equivocado responde 404.
En Google Wallet todo pase son dos piezas: una Class con los datos comunes del grupo — marca, evento, lugar, colores — y un Object con los datos del titular — nombre, asiento, código, estado. La Class se crea una vez por grupo; el Object, una vez por pase emitido.
Qué dato vive en cuál pieza no es un detalle de modelado: define cómo se actualiza cada cosa, cuánto cuesta hacerlo y qué endpoint tienes que llamar. Esto es lo que aprendimos operándolo en producción, incluida la trampa del 404 que más tiempo nos costó.
Qué vive en la Class y qué en el Object
La regla corta: si el dato es igual para todos los pases del grupo, vive en la Class; si es de un titular, vive en el Object.
Con entradas de evento, el reparto real se ve así:
- EventTicketClass: el nombre del evento, el lugar, la fecha y hora de la función, el logo y los colores del emisor. Una clase por función — si el mismo evento tiene dos fechas, son dos clases, porque la fecha es un dato de la clase. Las clases nativas nacen en revisión, y emitir al público depende del publishing access de la cuenta de emisor, no de un visto bueno por clase.
- EventTicketObject: el nombre del asistente, su localidad y asiento, el código que la puerta escanea y el estado de la entrada. Un objeto por entrada vendida.
El mismo reparto aplica a lealtad: la clase lleva el nombre del programa y su identidad visual; el objeto lleva al miembro, su nivel y su saldo.
Actualizar la Class es un broadcast
Respuesta directa: un PATCH a la Class propaga el cambio a todos los pases instalados de esa clase, del lado de Google, sin que toques objeto por objeto — y, cuando viaja sin notifyPreference (el default, y el modo en que operamos los rebrandings), sin consumir cuota de mensajes.
El caso real: el organizador renombra el evento o cambia el lugar. Con el reparto bien hecho, eso es un solo PATCH a la clase de cada función afectada; miles de entradas instaladas amanecen actualizadas. Si hubieras horneado el nombre del evento en cada objeto, el mismo cambio sería una actualización por entrada — mil llamadas donde había una.
El broadcast sin notifyPreference es silencioso: los titulares ven el contenido nuevo en su siguiente sincronización, sin notificación. Para avisar del cambio hay dos vías, ambas con la misma cuota de 3 avisos por pase cada 24 horas: el mensaje con aviso visible, o pedir el aviso en el propio UPDATE (notifyPreference: notifyOnUpdate, disponible para campos específicos de entradas — nombre del evento, lugar, fecha, asiento — y solo cuando la función empieza en 3 horas o menos).
Cada tipo tiene su propio recurso — y el path equivocado responde 404
Aquí está la trampa que motiva este artículo. genericObject, eventTicketObject y loyaltyObject no son variantes de un mismo endpoint: son recursos distintos de la API de Google Wallet, cada uno con sus rutas. Un objeto nativo consultado, parcheado o notificado a través del recurso genérico responde 404 — indistinguible de «este pase no existe».
El síntoma en producción es desconcertante: el pase está instalado, se ve en el teléfono, y tu servidor jura que no existe. Nada está roto del lado de Google — tu código está preguntando en la dirección equivocada.
Nuestra solución, que recomendamos como regla: el tipo de objeto se persiste junto al pase en tu base de datos, en el momento de la emisión, y viaja explícitamente en cada actualización y cada mensaje. Inferirlo del tipo de operación es fabricar un bug latente, porque las flotas reales son mixtas: los pases emitidos antes de adoptar tipos nativos siguen siendo genéricos para siempre, y conviven con los nativos nuevos en la misma operación. Un campo persistido no se equivoca; una inferencia, sí.
Los dos 404 que tienes que distinguir
Porque hay un 404 que sí es legítimo y frecuente:
- 404 por recurso equivocado — el de arriba. Es un bug tuyo: no se reintenta, se corrige el path.
- 404 benigno por objeto inexistente — el identificador del objeto suele persistirse de forma optimista al generar el enlace de instalación, pero el objeto solo existe en Google cuando el titular efectivamente guarda el pase. Un titular que recibió el enlace y nunca lo usó, o que eliminó el pase después, produce 404 legítimos ante cualquier actualización.
El segundo caso es terminal, no transitorio: reintentar no va a crear un objeto que el titular nunca guardó. Clasifícalo como no-reintentable en tus jobs, o vas a acumular colas de reintentos infinitos contra pases que no existen — nos pasó antes de tipificarlo, con jobs dando vueltas contra objetos fantasma.
Y una sutileza medida en agosto de 2026: la sonda de existencia correcta es un GET. Un PATCH con body efectivamente vacío no distingue existencia — responde 400 tanto para objetos reales como inexistentes («Patch request was empty.» en los tipos nativos; en el genérico el mismo 400 llega con un engañoso «Missing resource with ID» aunque el objeto exista) — y Google además descarta los campos que el recurso no conoce antes de evaluar el body, así que un PATCH con campos de otro tipo de objeto también cae en ese 400.
Cuántas Classes crear — y por qué versionarlas
Dos reglas que nos han funcionado:
Una clase por invariante. Todo lo que la clase declara es igual para todos sus objetos; si un dato varía entre grupos — la fecha entre funciones, el programa entre membresías — cada valor es su propia clase. Un identificador de clase determinístico (por ejemplo, derivado de la operación y la función) evita mantener un registro aparte.
Versiona el identificador desde el día uno. Algunos campos de una clase son, en la práctica, de nacimiento: con pases ya guardados, Google rechaza el cambio. Lo medimos en julio de 2026 con multipleDevicesAndHoldersAllowedStatus: el PATCH completo vuelve con 400 — «Cannot update value … as users have already saved your passes … Please create a new class if necessary» — y ese rechazo arrastra todos los demás campos del mismo body, incluidos los inocentes. La salida es la que el propio error sugiere: una clase nueva. Con el identificador versionado (un sufijo -v1, -v2), las emisiones nuevas apuntan a la clase nueva y las viejas siguen vivas sobre la suya, sin migración forzada.
Cuándo ves el cambio en el teléfono
A diferencia de Apple Wallet — donde el dispositivo re-descarga el pase tras un push —, en Google el dispositivo no consulta por su cuenta: tu servidor PATCHea el objeto o la clase y el resultado aparece cuando la vista se sincroniza.
Un detalle verificado que ahorra confusión en pruebas: una vista que ya está abierta en pantalla no se refresca sola — cerrar y reabrir el detalle del pase sí muestra el contenido nuevo. Si estás probando una actualización y el pase «no cambia», cierra el detalle y vuelve a entrar antes de abrir un ticket contra tu propio código.
Preguntas frecuentes
¿Un PATCH a la Class consume la cuota de mensajes?
No cuando viaja sin notifyPreference — el default: el cambio llega a todos los pases instalados en silencio, sin consumir cuota. Si el UPDATE pide aviso con notifyPreference: notifyOnUpdate (campos específicos de entradas, con la función a 3 horas o menos), ese aviso sí comparte el techo de 3 por pase cada 24 horas.
¿Cómo confirmo que un objeto existe realmente en Google?
Con un GET al recurso correcto del objeto. Un PATCH con body vacío no sirve como sonda: responde 400 sin distinguir si el objeto existe. El GET responde 404 limpio cuando el objeto no está — el titular nunca guardó el pase o lo eliminó.
¿El tipo genérico también tiene Class?
Sí: genericClass y genericObject funcionan con la misma mecánica de dos piezas. La diferencia con los tipos nativos no es estructural sino semántica: el genérico renderiza filas de texto que tú defines, sin el layout ni el comportamiento de categoría. La decisión entre uno y otro está en pase genérico vs tipos nativos.
¿Puedo distinguir por la respuesta un 404 de recurso equivocado de un 404 de objeto inexistente?
No: ambos responden 404. Por eso el tipo de objeto se persiste junto al pase en tu base de datos y viaja en cada llamada — inferirlo o adivinarlo convierte un error de programación en un síntoma idéntico a un pase que no existe.
Sigue leyendo
Todas las guíasEl certificado WWDR de Apple: el intermedio que viaja dentro de cada .pkpass
El WWDR es el intermedio público de Apple que va dentro de la firma de cada .pkpass. Qué generación usar (G4), cuándo vence y cómo falla cuando falta.
Expirar un pase: voided, expirationDate, validTimeInterval — y qué ve el titular sin conexión
Apple evalúa voided y expirationDate dentro del pase; Google expira por validTimeInterval o state vía PATCH. Sin red, el titular ve lo último descargado.