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.
Un pase puede morir de tres maneras: anulado ahora mismo (voided: true en Apple, state: EXPIRED en Google), expirado por fecha (expirationDate en Apple, validTimeInterval.end en Google) o borrado por el titular. Las dos primeras las controlas tú, pero entran distinto a cada wallet: en Apple las claves viajan dentro del .pkpass y el dispositivo las evalúa solo —también sin red—; en Google son estado del objeto en los servidores de Google, cambian únicamente con un PATCH, y la expiración por intervalo llega con hasta 24 horas de latencia declarada. Y la verdad del offline: el titular ve el último contenido que su teléfono descargó — en Apple por diseño del mecanismo; en Google, hasta donde llega su documentación.
Revocar un pase a distancia cuenta qué pasa operativamente cuando un pase debe morir. Este artículo baja a los campos: cuál usar en cada caso, qué hace cada wallet con ellos según su documentación viva a agosto de 2026, y qué ve el titular cuando no hay conexión.
Apple: voided y expirationDate viven dentro del pase
La referencia del objeto Pass define dos claves de nivel raíz:
voided— «un booleano que indica que el pase es nulo, como un cupón de un solo uso ya canjeado». Por defecto,false.expirationDate— «la fecha y hora en que el pase expira»; el valor tiene que ser una fecha completa con horas y minutos, opcionalmente segundos.
Apple no describe qué dibuja Wallet con cada una; lo que sigue es observación en dispositivo: ambas grisan el pase y lo mueven a la sección de pases vencidos, y voided además desactiva el código de barras. Lo que sí se deduce de la referencia es lo importante: las dos viajan dentro del paquete firmado, así que el dispositivo las aplica sin hablar con nadie. Un pase cuya expirationDate ya pasó expira también en un teléfono sin conexión.
Esa propiedad nos arregló un problema que arrastramos semanas con las entradas de evento. Al principio, que una entrada dejara de verse «vigente» dependía de una cadena: detectar que la función terminó, enviar el push silencioso, esperar a que el dispositivo descargara la versión anulada. En la práctica eso era más de un día de entrada «vigente» después del espectáculo — y para siempre en un teléfono que no se conectó. La solución fue poner expirationDate al fin de la función más seis horas de margen: el dispositivo la expira solo, sin red y sin costo, y el push de anulación queda como refuerzo, no como única vía.
Cuándo usamos cada una: voided para lo que ya no vale ahora —entrada usada o reembolsada, pase revocado, registro borrado por un administrador (anularlo es mejor que dejarlo pareciendo vivo)—; expirationDate para lo que tiene fecha de muerte conocida: una vigencia de membresía, el fin de un evento. Y un matiz de producto: una factura puntual se anula al pagarse o cancelarse, pero un pase de cuenta persistente no muere por un pago — representa la relación, no el cobro.
La entrega del void en Apple: nunca respondas 304 a un pase anulado
Como el void viaja en la siguiente versión del pase, su entrega depende del flujo de actualización: el dispositivo pide la lista de seriales que cambiaron desde su última marca, y luego cada pase con If-Modified-Since. Dos trampas que nos costaron un void que nunca llegó:
- A un pase anulado no se le responde 304. Si tu comparación de fechas coincide con el segundo en que anulaste, el dispositivo recibe «sin cambios» y se queda con la versión viva. Un pase anulado siempre se sirve completo.
- La anulación tiene que mover la marca de actualización. Si borras el registro sin actualizar su fecha de cambio, el serial nunca aparece en la lista de «cambiados desde», el dispositivo nunca lo pide y el
.pkpassanulado no sale jamás del servidor. Lo vimos en el diagnóstico de APNs: el push llegaba y el estado no convergía.
Google: validTimeInterval y state son estado del servidor
La referencia de Google define, en el objeto de entrada de evento, validTimeInterval como «el período en que el objeto estará activo y podrá usarse; el estado del objeto cambiará a expirado cuando ese período haya pasado», y en el objeto genérico como el período en que «se considerará válido o usable; pasado el período, el objeto se considerará expirado, lo que afectará el render en los dispositivos». state —obligatorio en los objetos nativos; opcional en el genérico, que asume ACTIVE— «se usa para determinar cómo se muestra el objeto en la app: por ejemplo, un objeto inactivo se mueve a la sección Pases vencidos»; el valor EXPIRED significa «el objeto ya no es válido (pasó validTimeInterval)».
La página de pases expirados añade lo operativo: un pase se mueve a «Pases vencidos» cuando pasó validTimeInterval.end.date —«en cualquier momento dentro de las 24 horas siguientes»— o cuando su state está marcado como Expired, Inactive o Completed (así los escribe esa página; en la API son EXPIRED, INACTIVE, COMPLETED); el ejemplo de código de esa misma página expira un objeto fijando state en EXPIRED, y para esa vía Google no declara latencia — las 24 horas están escritas solo para el intervalo. En entradas de evento hay una tercera condición: entre 72 y 96 horas después de dateTime.end de la clase (o de start si no hay end). Por eso revocar en Google es un PATCH de state: EXPIRED: la vía por estado, sin la latencia que Google sí declara para el intervalo.
Y la consecuencia que hay que internalizar: cambiar el intervalo es cambiar el objeto. No hay otro mecanismo que un PATCH al objeto (la API lo llama patch semantics); si tu sistema ya tiene la fecha nueva pero no la empujó, el pase sigue expirando en la vieja. El PATCH es silencioso —no consume la cuota de mensajes— así que no hay razón para no hacerlo en el mismo momento en que cambia la fecha.
Tres cicatrices del intervalo, todas en producción:
- Semántica de parche. Para borrar una vigencia hay que enviar
validTimeInterval: nullexplícito; omitir el campo lo deja como estaba. Tuvimos objetos expirando en una fecha que nadie quería porque elendviejo quedó persistido en Google — y reinstalar el pase no lo limpia. - Zona horaria. Un
endescrito como medianoche UTC expira el pase a las siete de la tarde del día anterior en Bogotá. El fin de vigencia se escribe como fin de día local, no como la fecha desnuda. - Anclar el fin a la fecha de pago. Archivar un pase de factura al vencer la fecha de pago lo manda a «Pases vencidos» exactamente cuando el titular más necesita ver la factura pendiente; y poner un intervalo a un documento de identidad —un carné— atado a un vencimiento de pago archivó el carné de quien ya había pagado. Regla: el intervalo solo representa vigencia real del documento, nunca el estado de un cobro; el fin de una factura viva se mueve mientras sea accionable y se fija al saldarse.
La verdad del offline
Sin red no hay consulta ni descarga: el teléfono conserva la última versión que bajó. Apple no lo escribe como frase; es la consecuencia del mecanismo, que su documentación describe como «un esfuerzo cooperativo entre el dispositivo del usuario, los servidores de Apple y tu servidor», en cinco pasos: el usuario instala un pase que admite actualizaciones; el dispositivo lo registra en tu servidor con un identificador y un token de push; la información cambia y tu servidor envía un push; el dispositivo recibe el push y consulta qué pases cambiaron; el dispositivo pide cada pase que cambió. Todo lo que viene después del push es el dispositivo yendo a buscar.
De ahí dos reglas que no dependen de la plataforma:
- Un pase revocado puede verse vivo en un teléfono sin conexión hasta que vuelva a conectarse. Por eso la puerta verifica contra tu sistema, no contra la pantalla del titular, y por eso revocar es un cambio de estado en tu registro antes que un cambio en el teléfono.
- Lo único que se cumple sin red es lo que viaja dentro del pase. En Apple,
expirationDateyvoided; úsalas como respaldo de todo lo que dependa de un push.
En Google la documentación no hace una declaración general sobre el offline: lo más concreto es que el enlace a un boarding pass «funciona sin conexión si el usuario tiene la app instalada». La expiración por intervalo la resuelve Google con la latencia declarada de hasta 24 horas, y el resto es comportamiento de la app que conviene comprobar en tu propio dispositivo antes de prometerlo.
Qué elegir para cada caso
- Entrada usada, reembolsada o pase revocado →
voided: trueahora (Apple) ystate: EXPIRED(Google);expirationDatecomo respaldo offline. - Membresía con vigencia →
expirationDateyvalidTimeInterval.endcon el mismo instante, escrito en hora local; el wallet solo renderiza la vigencia, nunca derives estado de ella. - Documento de identidad (carné, credencial) → sin expiración atada a pagos; si no hay vigencia declarada, no hay intervalo.
- Factura puntual → se anula al pagarse o cancelarse; pase de cuenta → nunca se anula por un pago.
- Relevancia ≠ expiración → cuándo el pase se ofrece en la pantalla de bloqueo es otro juego de claves: relevantDates, locations y geofencing.
Preguntas frecuentes
¿Un pase instalado sigue funcionando sin conexión?
Sí, con su último contenido. En Apple el pase es un archivo firmado que vive en el teléfono y cada actualización es un pase nuevo que el dispositivo va a buscar cuando le avisan; sin red no hay descarga y el teléfono muestra la última versión que bajó, aplicando por su cuenta las claves voided y expirationDate que viajan dentro. Google no publica una declaración general —documenta el acceso sin conexión para boarding passes a través de su app— y la expiración por validTimeInterval la resuelve Google con una latencia declarada de hasta 24 horas.
¿Cuál es la diferencia entre voided y expirationDate en Apple?
voided marca el pase como sin validez desde ya —Apple pone el ejemplo del cupón de un solo uso ya canjeado—; expirationDate fija el instante a partir del cual el pase expira. El primero es un estado, el segundo una fecha que el dispositivo evalúa solo, sin red. En nuestra experiencia en dispositivo ambos grisan el pase y lo mueven a la sección de pases vencidos; voided además desactiva el código.
¿Cómo expira un pase en Google Wallet?
De dos formas, según la documentación: cuando pasa validTimeInterval.end, Google lo considera expirado y lo mueve a «Pases vencidos» en cualquier momento dentro de las 24 horas siguientes; o cuando tú mismo pones state en EXPIRED por la API — para esa vía Google no declara latencia; las 24 horas están escritas solo para el intervalo. En entradas de evento hay una tercera: entre 72 y 96 horas después del fin declarado en la clase.
Si cambio la fecha de vencimiento, ¿tengo que volver a enviar el pase?
Sí, en ambas plataformas, y sin que el titular haga nada. En Apple el cambio viaja en la próxima versión del pase que el dispositivo descarga tras el push silencioso. En Google, validTimeInterval es estado del objeto en los servidores de Google y solo cambia con un PATCH al objeto — si no lo envías, el pase sigue expirando en la fecha vieja aunque tu sistema ya tenga la nueva. Ese PATCH es silencioso y no consume la cuota de mensajes.
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.
Apple Wallet: relevantDates, locations y geofencing — cuándo un pase aparece solo en la pantalla de bloqueo
Relevante por fecha (relevantDates), por lugar (locations, hasta 10) o ambos: así aparece un pase en la pantalla de bloqueo. Y el geofencing de Google.