> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axelo.mx/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet Passes

> Crear, actualizar y descargar pases de Apple Wallet y Google Wallet.

## Crear pase

```
POST /v1/passes/create
```

### Headers

| Header         | Valor              |
| -------------- | ------------------ |
| `X-API-Key`    | Tu API key         |
| `Content-Type` | `application/json` |

### Apple Wallet — Event Ticket con QR

```json theme={null}
{
  "pass_type": "apple",
  "pass_data": {
    "serial_number": "EVT-2026-001",
    "organization_name": "Mi Empresa",
    "description": "Entrada al evento",
    "style": "eventTicket",
    "background_color": "rgb(30, 60, 120)",
    "foreground_color": "rgb(255, 255, 255)",
    "label_color": "rgb(200, 200, 255)",
    "logo_text": "Mi Empresa",
    "barcodes": [{
      "message": "EVT-2026-001",
      "format": "PKBarcodeFormatQR",
      "messageEncoding": "iso-8859-1"
    }],
    "header_fields": [
      {"key": "date", "label": "FECHA", "value": "15 Mayo 2026"}
    ],
    "primary_fields": [
      {"key": "event", "label": "EVENTO", "value": "Torneo MX Open"}
    ],
    "secondary_fields": [
      {"key": "loc", "label": "LUGAR", "value": "Club Deportivo"},
      {"key": "time", "label": "HORA", "value": "10:00 AM"}
    ],
    "icon_base64": "iVBORw0KGgo..."
  }
}
```

### Campos de `pass_data` (Apple)

| Campo               | Tipo   | Descripción                                                                                              |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| `serial_number`     | string | **Requerido.** Identificador único del pase                                                              |
| `organization_name` | string | **Requerido.** Nombre de la organización                                                                 |
| `description`       | string | **Requerido.** Descripción del pase                                                                      |
| `style`             | string | `eventTicket`, `boardingPass`, `coupon`, `storeCard`, `generic` (default)                                |
| `background_color`  | string | Color de fondo CSS, e.g. `"rgb(30, 60, 120)"`                                                            |
| `foreground_color`  | string | Color de texto                                                                                           |
| `label_color`       | string | Color de labels                                                                                          |
| `logo_text`         | string | Texto junto al logo                                                                                      |
| `barcodes`          | array  | Barcodes: `PKBarcodeFormatQR`, `PKBarcodeFormatPDF417`, `PKBarcodeFormatAztec`, `PKBarcodeFormatCode128` |
| `header_fields`     | array  | Campos en la parte superior                                                                              |
| `primary_fields`    | array  | Campos principales (grandes)                                                                             |
| `secondary_fields`  | array  | Campos secundarios                                                                                       |
| `auxiliary_fields`  | array  | Campos auxiliares                                                                                        |
| `back_fields`       | array  | Campos en el reverso del pase                                                                            |

### Estructura de campos (Apple)

Cada campo en `header_fields`, `primary_fields`, `secondary_fields`, `auxiliary_fields` y `back_fields` tiene esta estructura:

| Propiedad       | Tipo   | Descripción                                                                  |
| --------------- | ------ | ---------------------------------------------------------------------------- |
| `key`           | string | **Requerido.** Identificador único del campo                                 |
| `value`         | string | **Requerido.** Valor visible en el pase                                      |
| `label`         | string | Etiqueta sobre el valor                                                      |
| `changeMessage` | string | Mensaje de notificación al actualizar. Usa `%@` para insertar el nuevo valor |
| `textAlignment` | string | `PKTextAlignmentLeft`, `PKTextAlignmentCenter`, `PKTextAlignmentRight`       |

**`changeMessage` y notificaciones push:**

Para que el usuario reciba una notificación visible en la pantalla de bloqueo al actualizar un pase, el campo debe incluir `changeMessage`. Sin este atributo, el pase se actualiza silenciosamente.

```json theme={null}
{
  "key": "match",
  "label": "PRÓXIMO PARTIDO",
  "value": "Sáb 10:00am · Cancha 3 vs Borjas/Reyes",
  "changeMessage": "Partido actualizado: %@"
}
```

Cuando el valor cambia via PUT, iOS muestra un banner:

> **Pase actualizado** — Partido actualizado: Dom 2:30pm · Cancha 5 vs García/López
> \| `icon_base64` | string | Icon PNG @1x (29×29 pt) |
> \| `icon_2x_base64` | string | Icon PNG @2x (58×58 px) — recomendado |
> \| `icon_3x_base64` | string | Icon PNG @3x (87×87 px) |
> \| `logo_base64` | string | Logo PNG @1x (160×50 pt) |
> \| `logo_2x_base64` | string | Logo PNG @2x (320×100 px) — recomendado |
> \| `logo_3x_base64` | string | Logo PNG @3x (480×150 px) |
> \| `strip_base64` | string | Strip PNG @1x (375×144 pt) — banda de acento con info dinámica |
> \| `strip_2x_base64` | string | Strip PNG @2x (750×288 px) — recomendado |
> \| `strip_3x_base64` | string | Strip PNG @3x (1125×432 px) |
> \| `thumbnail_base64` | string | Thumbnail PNG (90×90 pt / 180×180 @2x) |
> \| `relevant_date` | string | Fecha W3C para relevancia |
> \| `expiration_date` | string | Fecha de expiración W3C |
> \| `locations` | array | `[{"latitude": 19.43, "longitude": -99.13, "relevantText": "Cerca del evento"}]` |

### Crear pase desde template

Si tienes un template configurado, solo necesitas `template_id` + los overrides:

```json theme={null}
{
  "template_id": "entrada-torneo",
  "pass_data": {
    "serial_number": "TP-2026-042",
    "barcodes": [{"message": "TP-2026-042", "format": "PKBarcodeFormatQR", "messageEncoding": "iso-8859-1"}],
    "header_fields": [{"key": "date", "label": "FECHA", "value": "1 Jun 2026"}],
    "primary_fields": [{"key": "event", "label": "EVENTO", "value": "Torneo Padel Verano"}],
    "secondary_fields": [
      {"key": "loc", "label": "LUGAR", "value": "Club Reforma"},
      {"key": "time", "label": "HORA", "value": "9:00 AM"}
    ],
    "auxiliary_fields": [{"key": "cat", "label": "CATEGORIA", "value": "Open"}]
  }
}
```

El `pass_type` y `style` se infieren del template. Los campos del template se mergean con los overrides (el request gana).

### Respuesta (202)

```json theme={null}
{
  "job_id": "623f7796-...",
  "status": "pending"
}
```

Consulta `GET /v1/jobs/{job_id}` para obtener el resultado:

```json theme={null}
{
  "status": "success",
  "result": {
    "pass_id": "b4eb23fa-...",
    "pkpass_base64": "UEsDBBQAAA..."
  }
}
```

### Descarga directa

```
GET /v1/passes/{pass_id}/download
```

Devuelve el `.pkpass` con `Content-Type: application/vnd.apple.pkpass`.

***

### Google Wallet

#### Paso 1: Crear clase (una por torneo/evento)

```
POST /v1/google/classes
```

Cada torneo o evento necesita su propia clase. Los datos de la clase (nombre, sede, fechas) se muestran en el frente de todos los pases de esa clase.

```json theme={null}
{
  "class_type": "eventTicket",
  "class_data": {
    "id": "ISSUER_ID.torneo-slug",
    "issuerName": "Mi Empresa",
    "reviewStatus": "UNDER_REVIEW",
    "eventName": {
      "defaultValue": {"language": "es", "value": "Nombre del Torneo"}
    },
    "venue": {
      "name": {"defaultValue": {"language": "es", "value": "Club / Sede"}},
      "address": {"defaultValue": {"language": "es", "value": "Ciudad, Estado"}}
    },
    "dateTime": {
      "start": "2026-06-22T09:00:00",
      "end": "2026-06-28T22:00:00"
    },
    "hexBackgroundColor": "#0B1F3A",
    "logo": {
      "sourceUri": {"uri": "https://url-del-logo.png"}
    },
    "heroImage": {
      "sourceUri": {"uri": "https://url-del-banner-1032x336.png"}
    }
  }
}
```

Respuesta (201):

```json theme={null}
{"status": "created", "class_id": "ISSUER_ID.torneo-slug"}
```

#### Actualizar clase

Si cambia la sede, fechas o nombre del torneo:

```
PUT /v1/google/classes/{class_id}
```

```json theme={null}
{
  "class_type": "eventTicket",
  "class_data": {
    "venue": {
      "name": {"defaultValue": {"language": "es", "value": "Nueva Sede"}}
    }
  }
}
```

Todos los pases de esa clase se actualizan automáticamente.

#### Consultar clase

```
GET /v1/google/classes/{class_id}?class_type=eventTicket
```

#### Paso 2: Crear pase (uno por jugador)

```json theme={null}
{
  "pass_type": "google",
  "pass_data": {
    "class_id": "ISSUER_ID.torneo-slug",
    "class_type": "eventTicket",
    "object_id": "ISSUER_ID.jugador-UUID",
    "object_data": {
      "id": "ISSUER_ID.jugador-UUID",
      "classId": "ISSUER_ID.torneo-slug",
      "state": "ACTIVE",
      "ticketHolderName": "Nombre del Jugador",
      "ticketType": {"defaultValue": {"language": "es", "value": "Categoría"}},
      "ticketNumber": "BIB-0241",
      "barcode": {"type": "QR_CODE", "value": "UUID-JUGADOR"},
      "textModulesData": [
        {"id": "next_match", "header": "Próximo Partido", "body": "Por definir"}
      ]
    }
  }
}
```

| Campo del objeto   | Descripción                                         |
| ------------------ | --------------------------------------------------- |
| `ticketHolderName` | Nombre del jugador (visible en el frente)           |
| `ticketType`       | Categoría del jugador                               |
| `ticketNumber`     | Dorsal o ID del jugador                             |
| `textModulesData`  | Campos dinámicos (próximo partido, resultado, etc.) |

El `eventName`, `venue` y `dateTime` del frente vienen de la **clase** (no del objeto).

Resultado:

```json theme={null}
{
  "status": "success",
  "result": {
    "pass_id": "...",
    "save_link": "https://pay.google.com/gp/v/save/<jwt>"
  }
}
```

Importante: cada `object_id` debe ser único. Si se reutiliza un ID ya creado, Google rechaza el save link.

***

## Actualizar pase

```
PUT /v1/passes/{pass_id}
```

Actualiza un pase existente. Mergea los campos nuevos sobre los existentes, re-firma el .pkpass, y envía push notification a dispositivos registrados.

### Apple Wallet

Acepta campos en `camelCase` (`auxiliaryFields`) o `snake_case` (`auxiliary_fields`). Incluir `changeMessage` para notificación visible en lock screen.

```json theme={null}
{
  "pass_data": {
    "auxiliaryFields": [
      {
        "key": "match",
        "label": "PRÓXIMO PARTIDO",
        "value": "Dom 2:30pm · Cancha 5 vs García/López",
        "changeMessage": "Partido actualizado: %@"
      }
    ]
  }
}
```

### Google Wallet

Incluir `notify_message` para enviar notificación visible al usuario.

```json theme={null}
{
  "pass_data": {
    "textModulesData": [
      {"id": "next_match", "header": "Próximo Partido", "body": "Dom 2:30pm · Cancha 5 vs García/López"}
    ],
    "notify_message": "Tu próximo partido fue actualizado"
  }
}
```

### Respuesta (202)

```json theme={null}
{
  "pass_id": "b4eb23fa-...",
  "job_id": "...",
  "status": "updating"
}
```

***

## Estado del pase

```
GET /v1/passes/{pass_id}/status
```

Consulta si un pase está instalado en el dispositivo del usuario.

### Respuesta (200) — Apple Wallet

```json theme={null}
{
  "pass_id": "e39b84be-...",
  "pass_type": "apple",
  "version": 5,
  "status": "active",
  "installed_devices": 2,
  "is_installed": true
}
```

Apple notifica a Axelo cuando un dispositivo agrega o elimina el pase. `installed_devices` muestra cuántos dispositivos lo tienen activo.

### Respuesta (200) — Google Wallet

```json theme={null}
{
  "pass_id": "a4864ef7-...",
  "pass_type": "google",
  "version": 3,
  "status": "active",
  "google_status": "ACTIVE",
  "has_users": true,
  "is_installed": true
}
```

| Campo           | Descripción                                                                                             |
| --------------- | ------------------------------------------------------------------------------------------------------- |
| `google_status` | Estado del objeto en Google: `ACTIVE`, `COMPLETED`, `EXPIRED`, `INACTIVE`, o `not_found` si fue borrado |
| `has_users`     | `true` si al menos un usuario tiene el pase guardado en Google Wallet                                   |
| `is_installed`  | Resumen: `true` si hay evidencia de que el pase está en un dispositivo                                  |

Google no envía callbacks en tiempo real cuando un usuario borra el pase — este endpoint consulta el estado directamente a la API de Google.

***

## Webhook de device registration (Apple)

Axelo puede notificar a tu backend cuando un usuario instala o elimina un pase de Apple Wallet.

### Configuración

En el dashboard admin → detalle del tenant → sección **"Wallet Webhook"**, configura la URL destino.

O via API:

```
PUT /dash/api/tenants/{id}/wallet-webhook
{"wallet_webhook_url": "https://api.tuapp.com/webhooks/wallet"}
```

### Payload que recibe tu webhook

```json theme={null}
POST https://api.tuapp.com/webhooks/wallet
Content-Type: application/json

{
  "action": "register",
  "pass_id": "e3ade6ad-1bff-4161-ae48-5d18faa6f0ee",
  "serial_number": "461911c7-86e1-40b5-9358-d2c572647f54",
  "device_id": "de95fb1794d9857bb1eb5bcf7fd72014",
  "tenant_id": "torneos-pro",
  "timestamp": "2026-04-29T05:09:38Z"
}
```

| Campo           | Descripción                                                                     |
| --------------- | ------------------------------------------------------------------------------- |
| `action`        | `"register"` cuando el usuario agrega el pase, `"unregister"` cuando lo elimina |
| `pass_id`       | ID del pase en Axelo                                                            |
| `serial_number` | Serial del pase en Apple Wallet                                                 |
| `device_id`     | Identificador del dispositivo (deviceLibraryIdentifier de Apple)                |
| `timestamp`     | Hora UTC del evento                                                             |

El webhook se dispara de forma asíncrona — no bloquea la respuesta a Apple. Si tu endpoint falla, Axelo loguea el error pero no reintenta.

Solo aplica a **Apple Wallet**. Google Wallet no notifica cuando un usuario elimina un pase — usa `GET /v1/passes/{id}/status` para consultar bajo demanda.

***

## Guía de diseño

### Colores del sistema

| Token | Hex       | Uso                                       |
| ----- | --------- | ----------------------------------------- |
| Navy  | `#0B1F3A` | `backgroundColor` base de todos los pases |
| Blue  | `#1A56DB` | CTA, links, acento Torneos Pro default    |
| Sky   | `#3B82F6` | Acento secundario, iconos                 |
| White | `#FFFFFF` | `foregroundColor` y `labelColor`          |

### Color de acento por torneo

El `labelColor` en Apple y `hexBackgroundColor` en Google se usan para el acento del torneo:

| Acento | Hex       | Uso típico                 |
| ------ | --------- | -------------------------- |
| Amber  | `#F59E0B` | Torneos de verano          |
| Green  | `#22C55E` | Corporativos               |
| Sky    | `#3B82F6` | Series oficiales (default) |
| Red    | `#EF4444` | Premium / invitacionales   |
| Purple | `#A855F7` | Femenino / alternativos    |

### Strip image (Apple)

La `strip.png` es la banda de acento que muestra la hora del próximo partido. Se genera dinámicamente con el accent del torneo y se re-envía en cada update:

* **Dimensiones**: 375×144 @1x, 750×288 @2x, 1125×432 @3x
* **Contenido**: fondo con gradient del accent + hora del partido + pista + rival
* **Enviar como**: `strip_base64` (o `strip_2x_base64` para retina)
* **Regenerar**: cada vez que cambia el próximo partido

### Catálogo de push notifications

| Evento                  | Trigger                        | Mensaje ejemplo                                             |
| ----------------------- | ------------------------------ | ----------------------------------------------------------- |
| `match.upcoming`        | 30 min antes del partido       | "Tu partido empieza en 30 min — Pista 4 · 09:30 · vs López" |
| `match.result.recorded` | Organizador registra resultado | "Resultado registrado · 6-3, 6-4 — Pasas a cuartos"         |
| `player.advanced`       | Jugador avanza de ronda        | "¡Felicitaciones! Avanzaste a semifinales"                  |
| `tournament.finished`   | Final registrada               | "Torneo finalizado · 2º lugar"                              |

Para Apple, incluir `changeMessage` en los campos dinámicos. Para Google, incluir `notify_message` en el PUT.

***

## Errores

| Código | Causa                                       |
| ------ | ------------------------------------------- |
| `400`  | `pass_type` inválido o `pass_data` faltante |
| `401`  | API key inválida                            |
| `402`  | Saldo insuficiente (1 token por pase)       |
| `403`  | Canal wallet no habilitado para tu tenant   |
| `404`  | Pase no encontrado                          |
