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ó.
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
revision; la vieja queda SUPERSEDED (tombstone) al instante.WALLET: 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.Autenticación
X-Api-Key — audiences por capacidad
| Audience | Quién la usa | Permite |
|---|---|---|
| passes.ingest | cada marketplace (una key POR ORIGEN) | crear, ampliar y anular órdenes — contrato (a) |
| passes.access | control de acceso | webhook usada (d) + feed de credenciales (e) |
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
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étodo | Endpoint | Auth | Idempotencia |
|---|---|---|---|
| POST | /api/v1/orders | passes.ingest | hash byte-exacto del body por externalRef |
| PATCH | /api/v1/orders/{externalRef} | passes.ingest | amendmentRef |
| POST | /api/v1/orders/{externalRef}/cancel | passes.ingest | natural (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 divergenteevent.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, mandalonullo no lo mandes — el portal usastartsAt + 12 h, que es lo razonable para un show. No mandes una duración de relleno: unendsAtanterior astartsAt + 60 minse 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 numeradascount(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; sinstripUrlla entrada sale con una banda genérica del servicio y el nombre encima),backgroundColor/foregroundColor(colores del pass, hex) yprimaryColor(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
ticketIdsimporta). 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 →
eventIdY(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
occurredAtUtcmayor; RETURNED libera incondicionalmente. - Máquina por método: WALLET
ASSIGNED→ISSUED(→UPDATED→ISSUED)*(→CANCELLED)· EQUITYASSIGNED→ISSUED(→UPDATED)*(→CANCELLED)· PRINT igual que WALLET pero el ISSUED traeticketUrl(la landing imprimible) en vez dewalletUrl; el QR es ESTÁTICO (la anti-duplicación depende del reporte de "usada" en la puerta, ver (d)). ticketIdes null si la orden no trajo ids (o el pool se agotó):qtyes lo autoritativo; RETURNED lleva "los ids que haya".barcodees 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étodo | Endpoint | Auth | Idempotencia |
|---|---|---|---|
| POST | /api/v1/webhooks/access-used | passes.access + HMAC | eventId |
{ "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.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.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étodo | Endpoint | Auth |
|---|---|---|
| GET | /api/v1/access/events/{eventExternalRef}/credentials?updatedSince=&limit=500 | passes.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á
revisioncomo 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 consecretHex(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étodo | Endpoint | Qué hace |
|---|---|---|
| GET | /Accesos/Funciones | funciones habilitadas para la key |
| GET | /Accesos/{cFechaDesde}/{idImpresionTicketDesde} | novedades desde el cursor (yyyyMMddHHmmssfff) |
| POST | /Accesos/{cLectorCodigo} | registro de usos en lote |
| POST | /Accesos/Log | eco 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:
cClaveTOTPen 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 filtrabaex.Message); rate limiting por IP (HTTP 429 conRetry-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étodo | Endpoint | Qué hace |
|---|---|---|
| POST | /api/v1/payment-validations | alta/actualización (idempotente por externalRef) |
| POST | /api/v1/payment-validations/{externalRef}/validated | pago validado → frena recordatorios + mail de confirmación (una sola vez) |
| POST | /api/v1/payment-validations/{externalRef}/cancel | anulació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 →
EXPIREDen 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).
POST /orders (a).
El portal crea la orden, la bolsa raíz y el pool de ticketIds.
passes.bag.invitation al manager con el link al portal. Todavía no existe ningún pase.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.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.
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").
passes.pass.wallet al titular con esa landing (Apple/Google según el teléfono).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).
passes.pass.equity al titular con tu deepLink.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):
passes.batch.completed al gestor con el conteo (total / con error)./passes/sync).
En EQUITY sí incrementa revision y va (b) equity/update.
📡 Callback (c) UPDATED.
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.
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).CANCELLED, el cupo se libera y su ticketId vuelve al pool (puede reaparecer en un pase futuro).
📡 Callback (c) CANCELLED con el ticketId liberado.
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.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.
passes.order.cancelled a cada owner de bolsa.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
| Momento | Lo dispara | Estado del pase | Llamadas del portal | Mails (motor de notificaciones) |
|---|---|---|---|---|
| Creación de orden | ticketing (a) | — (no hay pases) | — | bag.invitation → manager |
| Delegación | gestor (portal) | — | — | bag.invitation → delegado |
| Reclaim de sub-bolsa | gestor (portal) | — | — | bag.reclaimed → delegado |
| Nominación (materialización) | gestor (portal) | nace QUEUED + ticketId FIFO | (c) ASSIGNED → marketplace | — |
| Emisión WALLET | worker | ISSUING → ISSUED | wallet POST /passes · (c) ISSUED | pass.wallet → titular |
| Emisión EQUITY | worker | PENDING_EXTERNAL → ISSUED | (b) equity/issue → ticketing · (c) ISSUED | pass.equity → titular |
| Falla de emisión | worker | FAILED | — | — |
| Lote completo | worker | — | — | 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 → ISSUED | wallet /passes/revoke + re-emisión · (c) UPDATED + ISSUED | pass.wallet/equity → titular nuevo |
| Reenvío del mail | gestor o titular (portal) | — | — | pass.wallet/equity → titular |
| Validación en puerta | control de acceso (d) | ISSUED → USED | — | — |
| Sincronización de puerta | control de acceso (e) | lee ISSUED + tombstones | — | — |
| Descarte de FAILED | gestor (portal) | FAILED → CANCELLED (ticket → pool) | (c) CANCELLED | — |
| Devolución al origen | manager raíz (portal) | — (solo saldo libre) | (c) RETURNED | bag.returned → manager |
| Reingreso de devueltos | ticketing (a) PATCH | — | — | — |
| Anulación de orden | ticketing (a) cancel | todos → CANCELLED | wallet revoke × N · (b) equity/cancel × N | order.cancelled → todos los owners |
| Recordatorios | worker diario | — | — | reminder.pending-bag / reminder.balance |
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ódigo | Cuándo | Qué hacer |
|---|---|---|
400 invalid_ticket_ids | count≠qty, duplicados, mezcla con/sin ids en el payload | La respuesta trae detail[] — corregí y reintentá |
400 ticket_ids_required / ticket_ids_not_expected | PATCH que rompe el todo-o-nada del sector | Respetá cómo nació el sector |
400 invalid_webhook_url | webhookUrl no-https o host privado/loopback | Usá una URL https pública |
409 order_conflict | Mismo externalRef con body distinto | El replay debe ser byte-exacto |
409 ticket_id_exists | PATCH con un ticketId ya vigente | Tratalo como "ya aplicado" |
401 | API key inválida o HMAC que no verifica | Revisá 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
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.