Gestor de PasesDISTRIBUCIÓN DE ENTRADAS · BOLSAS · WALLET & EQUITY

Bolsas corporativas de entradas: delegación, nominación y entrega en wallet.

Una orden (bolsa de entradas de un evento) se crea por API desde ticketing. El responsable la distribuye desde el portal: delega sub-bolsas o nomina entradas a titulares finales. La entrega es wallet pass (Apple/Google con QR rotativo) o Equity (la app de entradaUno). El portal te avisa todo: quién tiene cada entrada, cuándo se emitió, cuándo entró y cuándo volvió.

REST + JSON X-Api-Key + HMAC Apple / Google Wallet Equity Callbacks con reintentos ticketIds del origen Devoluciones
Base URL server-to-server: https://passes.pagouno.io · Portal del gestor: https://passes.equity.ar · Colección Postman: docs/pagouno.passes.postman_collection.json (en el repo del servicio).

Conceptos clave

📦
OrdenLa bolsa madre de un evento: sectores, cupos/butacas, branding y el email del primer responsable. La creás vos con el contrato (a).
👜
BolsaSaldo distribuible. La raíz es del manager; cada delegación crea una sub-bolsa. El saldo libre se puede reclamar hacia arriba o devolver al origen.
🎫
Pase + revisionUna entrada nominada a un email. Cada cambio de titular incrementa revision; la vieja queda SUPERSEDED (tombstone) al instante.
🔖
ticketIdEl id de la unidad en TU sistema (opcional). Viaja en la creación y vuelve en cada callback: es tu clave de reconciliación.
✉️
EntregaWALLET: pass Apple/Google con QR rotativo (TOTP), landing por mail. EQUITY: la entrada se emite en tu app vía contrato (b) y el portal manda el mail. PRINT: entrada imprimible (mail con link a una landing estilo pass + QR estático para imprimir/guardar PDF); no usa servicios externos.
🔁
OutboxTodo lo saliente ((b) y (c)) sale de un outbox con reintentos y backoff: podés estar caído y los mensajes llegan igual, en orden por pase.

Autenticación

X-Api-Key — audiences por capacidad

AudienceQuién la usaPermite
passes.ingestcada marketplace (una key POR ORIGEN)crear, ampliar y anular órdenes — contrato (a)
passes.accesscontrol de accesowebhook usada (d) + feed de credenciales (e)
Multi-marketplace ABIERTO: tu API key define tu ORIGEN — y es TODO lo que necesitás para operar. Toda orden queda atada a la key que la creó: solo VOS podés ampliarla o anularla (otro origen recibe 404) y tus callbacks (c) se firman con TU secreto HMAC (autogenerado en el primer uso; lo consultás con tu misma key, ver abajo). El destino de los callbacks es la webhookUrl que mandás EN CADA ORDEN (https + host público; sin allowlist de dominios). Las keys se dan de alta y se revocan desde el panel de PagoUno (kill-switch inmediato, sin coordinar redeploy).

Firma HMAC-SHA256

Los contratos de mayor impacto ((d), (e) y todos los salientes) van firmados con el secreto compartido de tu integración. Canonical string única:

firma = HMAC-SHA256(secreto, "<unixSeconds>." + <body crudo UTF-8>)
Header: X-Passes-Signature: t=<unixSeconds>,v1=<hex minúscula>      · tolerancia ±300 s
# En GET (feed) el body es vacío → canonical "<t>."

Tu secreto de callbacks — autoservicio

El secreto con el que firmamos TUS callbacks (c) se autogenera en tu primer uso y lo consultás cuando quieras con tu misma key de ingesta (guardalo del lado tuyo; no viaja nunca en las órdenes):

GET /api/v1/webhook-secret      · X-Api-Key: <tu key de ingesta>
→ 200 { "integration":"MKT_TUORIGEN", "hmacSecret":"…", "algorithm":"HMAC-SHA256",
        "header":"X-Passes-Signature", "canonical":"<unixSeconds>.<body crudo>", "toleranceSeconds":300 }

La regla de revision

Todo mensaje por-pase ((b) update, (c), (d), (e)) lleva revision. Aplicá el mensaje solo si revision ≥ la última vista para ese passRef (last-write-wins); a igual revision desempatá por occurredAtUtc. Los reintentos pueden llegar fuera de orden.

(a) Orden — entrante vos → portal

MétodoEndpointAuthIdempotencia
POST/api/v1/orderspasses.ingesthash byte-exacto del body por externalRef
PATCH/api/v1/orders/{externalRef}passes.ingestamendmentRef
POST/api/v1/orders/{externalRef}/cancelpasses.ingestnatural (anular dos veces = anulada)
{
  "externalRef": "E1-ORD-100001", "tenantId": "cust-3",
  "webhookUrl": "https://api.tumarketplace.com/gestor-pases/webhook",   ← opcional (v2)
  "event": { "externalRef":"EVT-555", "name":"…", "venueName":"…", "venueAddress":"…", "startsAt":"2026-08-15T20:30:00-03:00" },
  "branding": { "name":"…", "logoUrl":"…", "googleLogoUrl":"…", "stripUrl":"…",
               "backgroundColor":"#0b1030", "foregroundColor":"#ffffff", "primaryColor":"#2563eb" },
  "config": { "allowRedelegation":true, "maxDepth":3, "deliveryMethods":["WALLET","EQUITY","PRINT"],
              "defaultLocale":"es", "reminders":{ "balanceDaysBeforeEvent":[7,2], "pendingBagDays":[3,7] } },
  "sectors": [ { "externalRef":"VIP", "kind":"TICKET", "numbered":false, "qty":100, "ticketIds":["11","22","…"] },
               { "externalRef":"PLATEA", "kind":"TICKET", "numbered":true,
                 "seats":[{ "externalRef":"PL-01-001", "row":"1", "seat":"1", "ticketId":"901" }] } ],
  "manager": { "email":"agencia@x.com", "name":"Agencia X", "locale":"es" }
}
→ 201 { "orderId":"…", "portalUrl":"…" }  ·  200 replay idéntico  ·  409 mismo ref, body divergente
  • event.endsAt (opcional): cuándo TERMINA el evento. Marca el cierre de la ventana operativa: pasado ese momento no se puede delegar, nominar ni cambiar el titular de una entrada (la puerta sigue validando igual). Si no sabés el horario de fin, mandalo null o no lo mandes — el portal usa startsAt + 12 h, que es lo razonable para un show. No mandes una duración de relleno: un endsAt anterior a startsAt + 60 min se considera dato inválido, se descarta y se usa el fallback de 12 h (queda un warning en nuestro log). Un valor corto por error cerraría la ventana a los minutos de arrancar el show y tus agencias no podrían enviar entradas sobre la hora.
  • ticketIds / ticketId (v2, opcional): el id de cada unidad en TU sistema — vuelve en todos los callbacks (c) y en (b). Todo-o-nada por sector: si el sector nace con ids, todo PATCH que agregue cupo debe traerlos (400 ticket_ids_required); si nace sin ids, no puede introducirlos (400 ticket_ids_not_expected). En no numeradas count(ticketIds) == qty; únicos por orden. El portal asigna el id concreto a cada persona al nominar (FIFO): no asumas quién tiene qué id hasta recibir el callback.
  • webhookUrl (v2, opcional): destino de los callbacks (c) para ESTA orden, usado VERBATIM (no se concatena path). Validación solo técnica: https + host público (nada de IPs privadas/loopback) — sin allowlist de dominios: el destino lo decidís vos, orden por orden. La firma HMAC es siempre la de tu integración (tu secreto NUNCA viaja en la orden). Sin webhookUrl → tu BaseUrl registrada (si la acordaste) + /passes/beneficiary; sin ninguna de las dos, esa orden no recibe callbacks.
  • branding: define cómo se ve la ENTRADA en el wallet del invitado y los mails. logoUrl (PNG apaisado ~320×100 — encabezado del pass de Apple), googleLogoUrl (PNG cuadrado ~660×660 — logo de Google Wallet), stripUrl (PNG — la banda superior del pass de Apple / heroImage de Google: la "imagen" del evento; con strip, el pass de Apple no superpone el nombre del evento — el arte debe llevar el título del show, como un flyer; ideal PNG 1125×294, <500 KB; sin stripUrl la entrada sale con una banda genérica del servicio y el nombre encima), backgroundColor/foregroundColor (colores del pass, hex) y primaryColor (color de acento: los mails Y las etiquetas TITULAR/SECTOR/FECHA del pass de Apple; sin él, las etiquetas salen en el dorado default). URLs públicas (https). Sin branding la entrada sale genérica PagoUno — mandalo siempre. Los datos de la cara del pass (evento, titular, sector/butaca, fecha, lugar) los completa el portal solo.
  • El replay del POST es por hash byte-exacto del body (el orden del array ticketIds importa). No hay backfill de ids para cupo ya creado.

PATCH — ampliación aditiva (y el "deshacer" de una devolución)

  • El cupo/butacas nuevas caen en la bolsa raíz. Acepta webhookUrl (rotación del endpoint).
  • Re-enviar un ticketId/butaca DEVUELTA la reingresa a la orden — es el camino de deshacer una devolución.
  • Un ticketId ya vigente → 409 ticket_id_exists (tratalo como "ya aplicado"). Sector desconocido o repetido en el payload → 400 invalid_items.

Cancel — anulación total

202 { passesToRevoke, equityToCancel }. Revoca los wallet emitidos, anula los Equity, cierra todas las bolsas y avisa por mail a cada owner. Irreversible.

(b) Emisión Equity — saliente portal → ticketing

La implementa ticketing. Firmado HMAC, idempotente por passRef+revision.

POST {ticketing}/passes/equity/issue   { passRef, revision, orderExternalRef, sectorExternalRef, seatExternalRef, ticketId?, holder:{email,name,locale} }
  → 200 { equityRef, deepLink? }
POST {ticketing}/passes/equity/update  (mismo body + equityRef) → 200 { equityRef, deepLink? }   ← devuelve SIEMPRE el ref vigente
POST {ticketing}/passes/equity/cancel  { equityRef, passRef, reason:"ORDER_CANCELLED" }

Toda llamada a update (incluida una corrección de nombre) incrementa revision. Tras el 200 de issue, el portal manda el mail "tu pase está en la app" al titular con tu deepLink.

holder.name nunca viaja nulo: cuando la agencia nominó solo por email va el literal "S/N". En los callbacks (c) el nombre sigue siendo el real (puede ser null).

(c) Callback de beneficiario — saliente portal → marketplace

La implementa cada marketplace (destino: webhookUrl de la orden o tu BaseUrl registrada; firmado con el secreto HMAC de TU integración — cada origen valida solo lo suyo). Reintentos por outbox. Así se ve una invocación completa:

POST {webhookUrl de la orden}                  ← o {tu BaseUrl}/passes/beneficiary si la orden no trajo webhookUrl
Content-Type: application/json
X-Passes-Signature: t=1783971950,v1=9f3ab6…   ← HMAC-SHA256(tu secreto, "<t>." + body crudo UTF-8); secreto: GET /api/v1/webhook-secret

{ "eventId":"…", "type":"ASSIGNED", … }        ← body = UNO de los cinco eventos de abajo
→ tu 2xx = ack · cualquier otra cosa/timeout ⇒ reintentos con backoff (1, 5, 15, 60, 240 min) hasta morir a las 24 h

Cinco tipos de evento:

ASSIGNED | UPDATED   (al nominar / re-nominar / corregir)
{ "eventId":"…", "type":"ASSIGNED", "orderExternalRef":"…", "passRef":"…", "revision":2,
  "sectorExternalRef":"VIP", "seatExternalRef":null, "holder":{"email":"…","name":"…"},
  "deliveryMethod":"WALLET", "ticketId":"11", "barcode":"…", "chain":["agencia@x.com","sub@y.com"], "occurredAtUtc":"…" }

ISSUED   (v2 — credencial emitida; también en cada re-emisión)
{ …, "type":"ISSUED", "revision":2, "ticketId":"11", "barcode":"…",
  "walletUrl":"https://…"            /* WALLET: landing para "ver mi entrada" */,
  "equityRef":"…", "deepLink":"…"  /* EQUITY: walletUrl null */ }

CANCELLED   (v2 — pase fallido descartado por el gestor; su ticketId vuelve al pool y PUEDE reaparecer en otro passRef)
{ …, "type":"CANCELLED", "revision":1, "ticketId":"11" }

RETURNED   (v2 — el manager devolvió saldo LIBRE a tu sistema; delta acumulativo, sin passRef)
{ "eventId":"<returnRef>", "type":"RETURNED", "orderExternalRef":"…", "returnedBy":"agencia@x.com",
  "items":[ { "sectorExternalRef":"VIP", "qty":4, "ticketIds":["44","55",…], "seatExternalRefs":[],
              "remainingQty":6 } ], "occurredAtUtc":"…" }
→ 2xx = ack
  • Dedup: eventos por-pase → eventId Y (passRef, revision, type) (ISSUED es at-least-once). RETURNED → eventId (= returnRef del portal).
  • Orden: garantizado solo DENTRO del stream de un pase (ASSIGNED→ISSUED→UPDATED→ISSUED…). RETURNED es conmutativo: sumá qty/ids por sector sin asumir orden. Para el mapa ticketId→pase gana el evento con occurredAtUtc mayor; RETURNED libera incondicionalmente.
  • Máquina por método: WALLET ASSIGNED→ISSUED(→UPDATED→ISSUED)*(→CANCELLED) · EQUITY ASSIGNED→ISSUED(→UPDATED)*(→CANCELLED) · PRINT igual que WALLET pero el ISSUED trae ticketUrl (la landing imprimible) en vez de walletUrl; el QR es ESTÁTICO (la anti-duplicación depende del reporte de "usada" en la puerta, ver (d)).
  • ticketId es null si la orden no trajo ids (o el pool se agotó): qty es lo autoritativo; RETURNED lleva "los ids que haya". barcode es el QR estático de respaldo del pase: tratálo como credencial (no lo loguees ni lo muestres).
  • La devolución total NO cierra la orden: el cierre sigue siendo tuyo vía cancel (a). Los ticketIds devueltos quedan fuera de la orden hasta que los reingreses por PATCH.

(d) Webhook "usada" — entrante puerta → portal

MétodoEndpointAuthIdempotencia
POST/api/v1/webhooks/access-usedpasses.access + HMACeventId
{ "eventId":"…", "passRef":"…", "revision":2,   /* o walletSourceRef | equityRef | barcodeValue */
  "usedAtUtc":"…", "gate":"P2", "device":"ZEBRA-07" }
→ 200 { "result":"OK" }               revision vigente → USED (bloquea la edición del titular)
→ 200 { "result":"ALREADY_USED" }     el pase ya se usó → RECHAZAR el ingreso (2º intento con una copia)
→ 200 { "result":"STALE_REVISION" }   revisión supersedida presentada → NO marca, se audita
→ 200 { "result":"UNKNOWN_REF" }      ref desconocido (ack sin retry) · 5xx → reservado a fallas reales
⚠ El lector DEBE reportar cada ingreso por acá — no es opcional. Este webhook es la única defensa contra la duplicación de una entrada. Un pase de wallet se puede compartir (Apple/Google copian el .pkpass: el que lo manda y el que lo recibe quedan ambos con una copia — es nativo de la plataforma, no se puede impedir). Lo que garantiza el uso único es esto: la puerta marca el pase USED en el primer ingreso, y todo intento posterior con otra copia recibe ALREADY_USED → rechazá el ingreso.
Regla de validación en la puerta, en orden: (1) consultá el feed (e) — el pase debe estar ISSUED; cualquier otro estado (USED/SUPERSEDED/CANCELLED) se deniega. (2) Validá el código (QR/TOTP). (3) Reportá la usada por (d) de forma síncrona, idealmente antes de abrir el molinete: si dos personas presentan la misma copia casi a la vez, el segundo POST devuelve ALREADY_USED. En Apple el QR es ESTÁTICO (PassKit no tiene barcode rotativo): sin este reporte de uso, dos copias del mismo pase entrarían. En Google el QR es rotativo (TOTP), pero el reporte de uso sigue siendo obligatorio.

(e) Feed de credenciales — entrante puerta → portal

MétodoEndpointAuth
GET/api/v1/access/events/{eventExternalRef}/credentials?updatedSince=&limit=500passes.access + HMAC (body vacío)
→ { "items":[ { "passRef":"…", "revision":2, "status":"ISSUED", "sector":"VIP", "seat":null,
        "holder":{"email":"…","name":"…"}, "barcode":"token-por-revision",
        "totp":{"secretHex":"…","periodSeconds":60,"digits":8}, "updatedAtUtc":"…" },
      { "passRef":"…", "revision":1, "status":"SUPERSEDED", "updatedAtUtc":"…" }   /* tombstone */
    ], "nextSince":"…" }
  • El delta incluye todos los estados: ISSUED | SUPERSEDED | USED | CANCELLED. El validador deniega ante cualquier status ≠ ISSUED.
  • Tratá revision como monotónica (fail-closed): nunca aceptes una revision menor a la última conocida del pase.
  • Solo pases WALLET (los Equity los validás contra tu base). El código TOTP se calcula con secretHex (RFC 6238, SHA1, periodSeconds/digits).

(f) Compat IngresoUno — /Accesos puerta legacy → portal

Superficie de compatibilidad total con la API de accesos histórica (keywork/IngresoUno): mismos paths, mismos parámetros, misma salida y la misma key de siempre — un lector existente solo cambia el dominio. Detrás no hay lógica nueva: cada endpoint ejecuta los stored procedures existentes (Api_IngresoUno_*) en la base histórica de accesos, que sigue siendo la dueña de los datos y de la autorización (la key es el cApiKey dado de alta ahí, con sus funciones asociadas). Si integrás una puerta nueva, usá los contratos (d)/(e).

MétodoEndpointQué hace
GET/Accesos/Funcionesfunciones habilitadas para la key
GET/Accesos/{cFechaDesde}/{idImpresionTicketDesde}novedades desde el cursor (yyyyMMddHHmmssfff)
POST/Accesos/{cLectorCodigo}registro de usos en lote
POST/Accesos/Logeco compat (el original nunca lo implementó)

La key viaja donde siempre la puso el lector: header Authorization (el valor crudo), header apikey o query ?au= (también se acepta X-Api-Key). Recomendado: header — la query queda en logs intermedios.

GET /Accesos/Funciones                          (header apikey: <cApiKey>)
→ [ { "cComentario":"…", "cNombre":"…", "dFuncion":"…", "cJsonImagenes":"…", "idFuncion":26708 } ]

GET /Accesos/20260716000000000/0
→ [ { "idImpresionTicket":…, "idFuncion":…, "idImpresionTicketEstado":201, "cCodigoImpresion":"…",
      "dUso":null, "dModificacion":"…", "cClaveTOTP":"JBSWY3DP…" /* Base32, como siempre */, … } ]

POST /Accesos/MOLINETE-N1
[ { "cCodigoImpresion":"…" } ]
→ { "r":[ { "nCantidadRegistrosModificados":1 } ], "e":[] }
  • Las columnas de salida son las que devuelven los SP de la base histórica, reflejadas tal cual (con la transformación de siempre: cClaveTOTP en Base32 y sin columnas _INTERNO_). Nada que parsear distinto.
  • Endurecimientos (no cambian el happy path): sin key → 401 {"message":"unauthorized"} sin tocar la base (el viejo devolvía 200 []); errores internos → 500 {"message":"Database Error"} sin detalle (el viejo filtraba ex.Message); rate limiting por IP (HTTP 429 con Retry-After: 60 — esperá y reintentá; el mismo lote reintentado después SÍ entra); body máximo 4 MB (el techo del sistema viejo — un lote grande de usos encolados NO se rechaza). Tratá cualquier no-200 como transitorio y reintentá: el registro de usos es idempotente.
  • Una key presente pero inválida se comporta como siempre: los SP no le devuelven filas (200 []).

(g) Validación de pago — entrante marketplace → portal

Recordatorios automáticos y progresivos al comprador que tiene un pago pendiente de validación antes de la fecha del evento. Vos ingestás la compra pendiente y avisás cuando el pago se validó (o se anuló); el portal se encarga de todo el ciclo de emails con tu branding, en el idioma del comprador. Universo independiente de las órdenes del contrato (a): no toca bolsas ni pases.

MétodoEndpointQué hace
POST/api/v1/payment-validationsalta/actualización (idempotente por externalRef)
POST/api/v1/payment-validations/{externalRef}/validatedpago validado → frena recordatorios + mail de confirmación (una sola vez)
POST/api/v1/payment-validations/{externalRef}/cancelanulación → frena recordatorios, sin mail
GET/api/v1/payment-validations/{externalRef}estado + historial de envíos (soporte)

Auth: tu misma key de ingesta (passes.ingest, header X-Api-Key) — el origen sale de la key, multi-marketplace desde el día uno.

POST /api/v1/payment-validations
{ "externalRef":"OP-778821",
  "buyer":{ "email":"juan@cliente.com", "name":"Juan Pérez", "phone":"+54911…", "locale":"es" },
  "event":{ "name":"Concierto Primavera", "startsAt":"2026-10-20T21:00:00-03:00", "venueName":"Estadio Único" },
  "amountDue":45000.00, "currency":"ARS", "ticketCount":2, "operationRef":"778821",
  "validationUrl":"https://tumarketplace.com/pagos/OP-778821",   ← el botón "Validar Pago Ahora"
  "purchasedAt":"2026-07-17T14:30:00-03:00",
  "branding":{ "name":"Tu Marca", "logoUrl":"https://…", "primaryColor":"#2563eb" } }
→ 200 { "externalRef":"OP-778821", "status":"PENDING", "isNew":true }

Cadencia de recordatorios (la proximidad al evento siempre manda):

  • Día 0: en el alta nueva sale el mail passes.payment.created ("registramos tu compra, pago pendiente" con el botón de validación). Ese día la cadencia no manda otro. Los re-mandos del alta y las reactivaciones no lo repiten.
  • Más de 30 días al evento → hitos únicos a los 7, 30, 60, 90… días de la compra (cada 30).
  • Últimos 30 días → semanal · últimos 7 días → diario hasta el día del evento.
  • Anti-spam: máximo 1 email por día por compra; cada hito se manda una sola vez.
  • Evento pasado sin validar → EXPIRED en silencio (no se notifica más).
  • Re-mandar el alta actualiza monto/fecha/URL y nunca vuelve a PENDING un pago ya validado (los replays fuera de orden no re-spamean); una compra CANCELLED/EXPIRED sí se reactiva.

Los emails (passes.payment.created, passes.payment.reminder y passes.payment.validated, es/en) salen del motor de notificaciones central: las plantillas se editan desde el Backoffice (pantalla de plantillas, por evento y alcance) sin tocar código ni redeployar.

Ciclo de vida — el viaje completo de una orden y sus pases

Desde el POST /api/v1/orders hasta la puerta, cada paso con quién lo dispara y qué interacción genera hacia tu sistema (callbacks/llamadas) o hacia las personas (mails).

1
Creación de la orden — ticketing hace POST /orders (a). El portal crea la orden, la bolsa raíz y el pool de ticketIds.
✉️ Mail passes.bag.invitation al manager con el link al portal. Todavía no existe ningún pase.
2
Distribución interna (delegación / reclaim) — el manager delega sub-bolsas por email (✉️ passes.bag.invitation al delegado, en su idioma) y puede reclamar el saldo libre de una sub-bolsa (✉️ passes.bag.reclaimed). Es pura contabilidad de saldos: no te llega ningún callback — los ticketIds siguen libres en el pool.
3
Nominación — un gestor asigna N entradas a un email. El portal reserva el cupo en el momento (sincrónico) y materializa los pases en lotes (asíncrono): cada pase nace QUEUED con revision 1, un barcode propio y — si hay pool — su ticketId (FIFO; en numeradas, el de la butaca).
📡 Callback (c) ASSIGNED por cada pase, con titular, cadena de delegación, ticketId y barcode.
4
Emisión WALLET — un worker toma los pases QUEUED (lease multi-réplica) y llama a PagoUno Wallet (POST /passes, QR rotativo TOTP — el secreto queda guardado para la puerta). El pase pasa a ISSUED.
📡 Callback (c) ISSUED con walletUrl (la landing "ver mi entrada").
✉️ Mail passes.pass.wallet al titular con esa landing (Apple/Google según el teléfono).
5
Emisión EQUITY — el pase pasa a PENDING_EXTERNAL y el portal llama a tu ticketing: (b) equity/issue. Con tu 200 {equityRef, deepLink} pasa a ISSUED.
📡 Callback (c) ISSUED con equityRef + deepLink (walletUrl null).
✉️ Mail passes.pass.equity al titular con tu deepLink.
6
Falla de emisión — si el wallet o tu equity/issue fallan (tras los reintentos), el pase queda FAILED: visible para el gestor, que puede reintentar (vuelve a QUEUED, repite el paso 4/5) o descartar. Al completarse el lote (con o sin errores):
✉️ Mail passes.batch.completed al gestor con el conteo (total / con error).
7
Corrección del titular (mismo email, cambia nombre/observaciones) — NO cambia la revision en WALLET: el portal sincroniza el pass en el teléfono (wallet /passes/sync). En EQUITY sí incrementa revision y va (b) equity/update.
📡 Callback (c) UPDATED.
8
Re-nominación (cambia el email) — revision + 1. La revisión vieja pasa a SUPERSEDED al instante (tombstone en el feed (e): la puerta la rechaza aunque el pass viejo siga en un teléfono). El portal revoca la credencial vieja (wallet /passes/revoke) y re-emite para el titular nuevo (repite el paso 4/5).
📡 Callbacks (c) UPDATED (rev N) y luego ISSUED (rev N, credencial nueva).
✉️ Mail al titular NUEVO con su credencial. Editable hasta que el pase se usa en puerta.
9
Puerta — tu control de acceso sincroniza el feed (e) (barcodes + secretos TOTP por revisión + tombstones) y valida offline. Cuando alguien entra, te avisa con el webhook (d): el pase pasa a USED, se bloquea la edición del titular y el uso aparece en los reportes del portal. Una revisión supersedida en puerta → STALE_REVISION (no entra, queda auditado).
10
Descarte de un FAILED — el gestor lo descarta: el pase pasa a CANCELLED, el cupo se libera y su ticketId vuelve al pool (puede reaparecer en un pase futuro).
📡 Callback (c) CANCELLED con el ticketId liberado.
11
Devolución al origen — el manager raíz devuelve saldo LIBRE (nunca pases nominados) desde el portal, con confirmación e idempotencia por returnRef.
📡 Callback (c) RETURNED con los ticketIds devueltos (FIFO) y remainingQty por sector.
✉️ Mail-recibo passes.bag.returned al manager. El "deshacer" es tuyo: PATCH (a) re-enviando esos ids.
12
Anulación de la orden — ticketing hace POST …/cancel (a): todos los pases pasan a CANCELLED, el portal revoca cada wallet emitido (wallet /passes/revoke) y anula cada Equity ((b) equity/cancel), y cierra todas las bolsas.
✉️ Mail passes.order.cancelled a cada owner de bolsa.
13
Recordatorios automáticos (worker diario, cadencia por orden) — ✉️ passes.reminder.pending-bag (bolsa nunca abierta a los N días) y ✉️ passes.reminder.balance (saldo sin nominar a N días del evento). No generan callbacks.

Estados del pase

                 reserva de cupo        emisión (worker)
nominación ────▶ QUEUED ────▶ ISSUING ──┬──▶ ISSUED ─────────────▶ USED      (webhook (d); bloquea edición)
                                        │       │
                                        │       │ re-nominación: la rev vigente pasa a SUPERSEDED
                                        │       ▼ (tombstone en (e)) + revoke wallet + re-emisión rev N+1 → QUEUED
                                        │
                                        ├──▶ FAILED ──▶ retry → QUEUED
                                        │       └─────▶ discard → CANCELLED  (+ (c) CANCELLED, ticket → pool)
                                        │
EQUITY:                                 └──▶ PENDING_EXTERNAL ──▶ ISSUED     ((b) issue → tu 200)

anulación de orden: cualquier estado ──▶ CANCELLED   (revoke wallet / (b) equity-cancel masivos)

Matriz de interacciones — quién llama a quién en cada momento

MomentoLo disparaEstado del paseLlamadas del portalMails (motor de notificaciones)
Creación de ordenticketing (a)— (no hay pases)—bag.invitation → manager
Delegacióngestor (portal)——bag.invitation → delegado
Reclaim de sub-bolsagestor (portal)——bag.reclaimed → delegado
Nominación (materialización)gestor (portal)nace QUEUED + ticketId FIFO(c) ASSIGNED → marketplace—
Emisión WALLETworkerISSUING → ISSUEDwallet POST /passes · (c) ISSUEDpass.wallet → titular
Emisión EQUITYworkerPENDING_EXTERNAL → ISSUED(b) equity/issue → ticketing · (c) ISSUEDpass.equity → titular
Falla de emisiónworkerFAILED——
Lote completoworker——batch.completed → gestor
Corrección (mismo email)gestor (portal)ISSUED (WALLET: misma rev · EQUITY: rev+1)wallet /passes/sync o (b) equity/update · (c) UPDATED—
Re-nominación (otro email)gestor (portal)rev N → SUPERSEDED · nueva rev QUEUED → ISSUEDwallet /passes/revoke + re-emisión · (c) UPDATED + ISSUEDpass.wallet/equity → titular nuevo
Reenvío del mailgestor o titular (portal)——pass.wallet/equity → titular
Validación en puertacontrol de acceso (d)ISSUED → USED——
Sincronización de puertacontrol de acceso (e)lee ISSUED + tombstones——
Descarte de FAILEDgestor (portal)FAILED → CANCELLED (ticket → pool)(c) CANCELLED—
Devolución al origenmanager raíz (portal)— (solo saldo libre)(c) RETURNEDbag.returned → manager
Reingreso de devueltosticketing (a) PATCH———
Anulación de ordenticketing (a) canceltodos → CANCELLEDwallet revoke × N · (b) equity/cancel × Norder.cancelled → todos los owners
Recordatoriosworker diario——reminder.pending-bag / reminder.balance
Los mails salen del motor de notificaciones de PagoUno (plantillas passes.*, es/en, con el branding del evento). Todas las llamadas salientes van por outbox: si tu endpoint está caído, se reintenta con backoff (1 m → 4 h) hasta 24 h.

Errores y reintentos

CódigoCuándoQué hacer
400 invalid_ticket_idscount≠qty, duplicados, mezcla con/sin ids en el payloadLa respuesta trae detail[] — corregí y reintentá
400 ticket_ids_required / ticket_ids_not_expectedPATCH que rompe el todo-o-nada del sectorRespetá cómo nació el sector
400 invalid_webhook_urlwebhookUrl no-https o host privado/loopbackUsá una URL https pública
409 order_conflictMismo externalRef con body distintoEl replay debe ser byte-exacto
409 ticket_id_existsPATCH con un ticketId ya vigenteTratalo como "ya aplicado"
401API key inválida o HMAC que no verificaRevisá key/secreto/reloj (tolerancia ±300 s)

Salientes con backoff exponencial (1 m → 4 h), DEAD a las 24 h con alerta. Todos los contratos son idempotentes: reintentá con seguridad usando la misma clave de negocio (externalRef, amendmentRef, eventId, passRef+revision, returnRef).

Checklist de go-live

① API keysPedí tus keys: passes.ingest (ticketing) y passes.access (control de acceso).
② Secretos HMACIntercambiá el secreto de tu integración (firma de (b)/(c)/(d)/(e)).
③ Creá una ordenProbá el contrato (a) con la colección Postman (incluye el body v2 con ticketIds + webhookUrl y los casos 400/409).
④ Recibí callbacksImplementá el endpoint (c): dedup por eventId + (passRef, revision, type), last-write-wins por revision.
⑤ Equity (si aplica)Implementá (b) issue/update/cancel — idempotente por passRef+revision.
⑥ PuertaCableá el feed (e) + webhook (d) en tu control de acceso (revision monotónica fail-closed).

Changelog

  • v5 (2026-07-17) — (g) validación de pago: ingesta de compras con pago pendiente + recordatorios automáticos progresivos (hitos 7/30/60/90…, semanal en el último mes, diario en la última semana) + confirmación al validar. Plantillas editables desde el BO.
  • v4 (2026-07-16) — compat IngresoUno: superficie /Accesos con el contrato histórico (funciones, novedades, registro de uso) para los molinetes existentes — proxy endurecido a los SP de la base histórica, solo cambia el dominio.
  • v3 (2026-07-13) — multi-marketplace ABIERTO: opera cualquier marketplace con su key de ingesta, sin alta de dominios; los callbacks (c) van a la webhookUrl de cada orden (validación solo técnica: https + host público); secreto HMAC por origen autogenerado + autoservicio GET /api/v1/webhook-secret.
  • v2 (2026-07-12) — ticketIds del sistema origen (pool por sector, todo-o-nada), webhookUrl por orden, devoluciones al origen (evento RETURNED con eventId=returnRef), eventos ISSUED y CANCELLED en (c), ticketId en (b), PATCH que reingresa tickets devueltos.
  • v1 — contratos (a)–(e), wallet + Equity, QR rotativo con tombstones por revisión.