POST /v1/partner/tickets/bulk
Upsert de hasta 1000 boletos por request, idempotente por externalRef.
Reenviar la misma fila actualiza en lugar de duplicar; puedes reenviar tu inventario
completo con seguridad (por ejemplo, un sync cada hora). Es también el único endpoint
para transferencias y para cambiar maxUses.
| Método y ruta | POST /v1/partner/tickets/bulk |
| Auth | Authorization: Bearer agk_… |
| Content-Type | application/json |
| Rate limit | 60 req/min por llave |
| Campo | Tipo | Obligatorio | Límite | Descripción |
|---|---|---|---|---|
experienceId | string (UUID) | Sí | Experiencia (evento) enlazada a tu proveedor. | |
rows | TicketImportRow[] | Sí | 1 – 1000 | Filas a upsertar. |
TicketImportRow
Sección titulada «TicketImportRow»| Campo | Tipo | Obligatorio | Límite | Descripción |
|---|---|---|---|---|
qrContent | string | Sí | 1 – 500 chars | Contenido del QR tal cual está en tu boleto. Se normaliza en el servidor. |
externalRef | string | No (recomendado) | ≤ 200 chars | Tu ID estable del boleto: clave de idempotencia y de void/consulta. Sin él, el boleto se identifica solo por qrContent. |
status | "valid" | "void" | No | default valid | redeemed no se acepta: solo lo produce la puerta. |
maxUses | integer | No | 1 – 1000, default 1 | Entradas que admite el boleto. Ver Multi-usos. |
holderRef | string | No (recomendado) | ≤ 200 chars | ID estable del titular actual en tu sistema (userId, teléfono…). Base de transferencias y del sello multi-uso. |
holderName | string | No | ≤ 200 chars | Nombre del titular para mostrar al operador en puerta. |
metadata | object (JSON libre) | No | Se muestra al operador (tipo de entrada, asiento, zona…). No lo uses para datos sensibles. |
Las llaves son camelCase exactamente como aparecen; las desconocidas se ignoran.
holderRef, holderName y externalRef se recortan (trim); un valor vacío se
trata como ausente.
Semántica del upsert
Sección titulada «Semántica del upsert»Para cada fila, el servidor busca el boleto en este orden:
- Si la fila trae
externalRef: por(tu proveedor, externalRef).- Existe en esta experiencia con el mismo QR →
updated. - Existe en esta experiencia con otro QR →
reissued(reemisión; ver Transferencias). - Existe en otra experiencia → rechazo
EXPERIENCE_MISMATCH.
- Existe en esta experiencia con el mismo QR →
- Si no se encontró por
externalRef(o la fila no lo trae): por(experiencia, qrContent).- Existe →
updated(y se le asigna elexternalRefsi venía). - No existe →
inserted.
- Existe →
En un update los campos ausentes conservan su valor anterior (externalRef,
metadata, maxUses, holderRef, holderName). Hay una excepción importante:
Un boleto ya redimido nunca se pisa desde el bulk (REDEEMED_CONFLICT).
Normalización de qrContent
Sección titulada «Normalización de qrContent»Antes de guardar (y antes de comparar en puerta) aplicamos la misma función a cada código:
trimde espacios.- Si el resultado es una URL
http(s)://…con parámetro de querychlno vacío (QRs estilo Google Charts), el código real es el valor dechl(también recortado). Si la URL no traechl, se conserva la URL completa.
qrContent enviado | Guardado |
|---|---|
" ABC.DEF " | ABC.DEF |
"https://chart.googleapis.com/chart?cht=qr&chl=GHI.JKL" | GHI.JKL |
"https://tickets.example.com/t/9f3a" | la URL completa |
Envía el contenido tal cual está en tu QR; no lo pre-proceses.
Respuesta 200
Sección titulada «Respuesta 200»{ "inserted": 2, "updated": 0, "reissued": 0, "rejected": [ { "index": 2, "reason": "REDEEMED_CONFLICT" } ]}| Campo | Tipo | Descripción |
|---|---|---|
inserted | integer | Boletos nuevos en el espejo. |
updated | integer | Boletos existentes actualizados (mismo externalRef, mismo QR). No incluye reemisiones. |
reissued | integer | Boletos cuyo QR cambió (mismo externalRef, qrContent nuevo). Opcional: si falta, es 0. |
rejected | array | Filas rechazadas: index es el índice 0-based dentro de rows, reason un código estable. |
Las filas rechazadas no afectan al resto del request (cada fila se procesa en su
propia subtransacción): revisa rejected[].index, corrige solo esas y reenvía.
Reason codes
Sección titulada «Reason codes»reason | Qué pasó | Qué hacer |
|---|---|---|
EMPTY_CODE | qrContent quedó vacío tras la normalización. | Revisa el contenido del QR que envías. |
DUPLICATE_IN_BATCH | Otra fila anterior del mismo request tiene el mismo qrContent (normalizado). Gana la primera aparición. | Deduplica antes de enviar. |
INVALID_STATUS | status distinto de valid | void. | En la práctica el API lo detecta antes con 400 VALIDATION_FAILED. |
INVALID_MAX_USES | maxUses no es un entero entre 1 y 1000. | Ídem: normalmente 400 VALIDATION_FAILED. |
USES_CONFLICT | El maxUses nuevo es menor que los usos ya consumidos por el boleto. | No reduzcas el cupo por debajo de lo consumido. Ver Multi-usos. |
PROVIDER_MISMATCH | Ese qrContent ya existe en la experiencia en el espejo de otro proveedor. | Contacta a alpadevs: el evento tiene inventario de otra ticketera con ese código. |
EXPERIENCE_MISMATCH | Ese externalRef ya existe en otra experiencia tuya. Un boleto no cambia de evento. | Usa un externalRef distinto por evento. |
REDEEMED_CONFLICT | El boleto ya fue redimido en puerta: no se modifica, anula, transfiere ni reemite. | Nada que corregir: la fila se ignora. |
EXTERNAL_REF_TAKEN | Choque de unicidad: el externalRef ya está en uso por otro boleto del proveedor (por ejemplo, un import concurrente en otra experiencia). | Reintenta la fila; si persiste, revisa tu mapeo de referencias. |
QR_TAKEN | Al reemitir, el qrContent nuevo ya pertenece a otro boleto (otro externalRef) de la experiencia. | Emite otro QR o revisa tu mapeo. |
Trata cualquier reason desconocido como un rechazo genérico: podemos añadir motivos
nuevos sin romper el contrato.
Errores
Sección titulada «Errores»| HTTP | code | Cuándo |
|---|---|---|
| 400 | VALIDATION_FAILED | Body fuera de schema: más de 1000 filas, qrContent vacío o > 500, maxUses fuera de rango, status inválido, UUID malformado… |
| 401 | KEY_INVALID | Llave ausente, malformada, revocada o proveedor desactivado. |
| 403 | EXPERIENCE_NOT_LINKED | La experiencia no está enlazada a tu proveedor (o no existe). |
| 429 | RATE_LIMITED | Más de 60 requests por minuto en esta ruta. |
| 500 | INTERNAL | Error nuestro; reintenta con backoff (el request es idempotente). |
Formato y detalle en Errores comunes.
Ejemplo
Sección titulada «Ejemplo»curl -s -X POST "https://gate-partners.alpadevs.com/v1/partner/tickets/bulk" \ -H "Authorization: Bearer $GATE_KEY" \ -H "Content-Type: application/json" \ -d '{ "experienceId": "22222222-2222-4222-8222-222222222222", "rows": [ { "qrContent": "ABC.DEF", "externalRef": "TG-1001", "holderRef": "user-42", "holderName": "Ana Pérez", "metadata": { "priceName": "VIP", "experienceName": "Main Event" } }, { "qrContent": "https://chart.googleapis.com/chart?cht=qr&chl=GHI.JKL", "externalRef": "TG-1002", "maxUses": 3, "holderRef": "+5215512345678", "holderName": "Carlos Ruiz" }, { "qrContent": "MNO.PQR", "externalRef": "TG-1003", "status": "void" } ] }'Primera carga:
{ "inserted": 3, "updated": 0, "reissued": 0, "rejected": [] }Mismo request repetido después de que TG-1001 entró por la puerta:
{ "inserted": 0, "updated": 2, "reissued": 0, "rejected": [{ "index": 0, "reason": "REDEEMED_CONFLICT" }]}