Entradas de cortesía con priceOverride 0
Gira y llévate una de las 3 entradas del evento seed como cortesía. Al caer el premio se crea una CheckoutSession real con priceOverride: 0 y se abre el widget de verdad.
Vende, regala y valida entradas desde tu web, tu app o tu backend. API REST con CORS abierto, un widget de checkout que se incrusta con una línea de JavaScript y webhooks firmados para enterarte de todo en tiempo real.
Todo lo que ves en Tiketa — órdenes, QRs, emails, check-in y métricas — está disponible para tu propio producto:
Tiketa.mount().order.created, order.paid y ticket.checked_in firmados con HMAC llegan a tu servidor.Base URL local: http://localhost:3001. Todos los endpoints responden JSON con Access-Control-Allow-Origin: * y aceptan preflight OPTIONS: puedes llamarlos desde cualquier origen (la key, siempre desde tu servidor).
Cada partner tiene una key tk_live_… que se genera (y se muestra una sola vez) en el panel de Tiketa. Va en el header de cada request:
Authorization: Bearer {apiKey}Este entorno de desarrollo incluye la key demo tk_demo_123 (integración "Integración demo" del seed) — todos los ejemplos de esta página funcionan tal cual. En producción, guarda tu key como variable de entorno y úsala solo server-side.
curl http://localhost:3001/api/v1/events \
-H "Authorization: Bearer tk_demo_123"Todo error responde el mismo shape — un code estable para tu lógica y un message en español para humanos:
{
"error": {
"code": "rate_limited",
"message": "Superaste el límite de 100 solicitudes por minuto. Espera unos segundos y reintenta."
}
}| Campo | Tipo | Descripción |
|---|---|---|
missing_api_key | 401 | Falta el header Authorization. |
invalid_api_key | 401 | La API key no existe. |
partner_inactive | 403 | La key está desactivada. |
rate_limited | 429 | Más de 100 req/min con la misma key. |
invalid_request | 400 | Body o parámetros inválidos (el mensaje dice exactamente qué). |
not_found / ticket_type_not_found / ticket_not_found | 404 | El recurso no existe. |
sold_out | 409 | La entrada no tiene cupo disponible. |
event_unpublished | 409 | El evento de la entrada no está publicado. |
session_expired | 410 | La CheckoutSession pasó su TTL de 30 minutos. |
session_completed | 409 | La sesión ya emitió su orden. |
El límite es 100 solicitudes por minuto por API key. Cada respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (epoch en segundos). Al superarlo: 429 + Retry-After.
/api/v1/events/api/v1/events/{slug}El listado trae los eventos publicados con sus entradas visibles (precio, cupo restante, highlight). El detalle por slug añade descriptionHtml. Los ticketTypes[].id son los que usarás para vender.
curl http://localhost:3001/api/v1/events/peru-blockchain-conference-2026 \
-H "Authorization: Bearer tk_demo_123"{
"events": [
{
"id": "cmev8xk2l0000tiketa01",
"slug": "peru-blockchain-conference-2026",
"name": "Perú Blockchain Conference 2026",
"country": "PE",
"city": "Lima",
"venue": "JW Marriott Larcomar",
"startDate": "2026-07-11T14:00:00.000Z",
"endDate": "2026-07-12T03:00:00.000Z",
"logoUrl": "http://localhost:3001/uploads/logo-pbconf26.png",
"coverUrl": "http://localhost:3001/uploads/pbconf26-cover.jpg",
"url": "http://localhost:3001/e/peru-blockchain-conference-2026",
"ticketTypes": [
{
"id": "cmev8xk2l0003tiketa01",
"name": "VIP",
"price": 249,
"compareAtPrice": 399,
"currency": "USD",
"featuresHtml": "<ul><li>Asientos preferenciales…</li></ul>",
"highlight": true,
"soldOut": false,
"remaining": 269
}
]
}
]
}/api/v1/checkout-sessionsUna sesión define cómo se venderá una entrada: datos precargados, campos bloqueados, precio impuesto y adónde volver. Devuelve un id y una url lista para el widget, un iframe o un link directo. Expira a los 30 minutos.
| Campo | Tipo | Descripción |
|---|---|---|
ticketTypeId | string | La entrada a vender (de GET /api/v1/events). |
prefill | object | Datos ya conocidos: { name?, email?, role?, company? }. |
lockedFields | string[] | Campos que el comprador NO puede editar (se ocultan). El servidor los impone: el cliente no puede falsearlos. |
priceOverride | number ≥ 0 | Precio impuesto. 0 = cortesía: el checkout no pide pago. |
metadata | object | string | Datos libres tuyos (máx 4 KB). Vuelven en órdenes y webhooks. |
redirectUrl | string (URL) | Al terminar, el checkout ofrece “Volver al sitio” hacia esta URL. |
Ruleta / regalos
priceOverride: 0 → "Entrada cortesía · $0", sin paso de pago.
Datos ya conocidos
prefill + lockedFields → el checkout solo pide lo que falta, o nada.
Venta directa
Sesión simple (o solo ticketTypeId en el widget) → flujo completo.
curl -X POST http://localhost:3001/api/v1/checkout-sessions \
-H "Authorization: Bearer tk_demo_123" \
-H "Content-Type: application/json" \
-d '{
"ticketTypeId": "TICKET_TYPE_ID",
"prefill": {
"name": "Camila Rojas",
"email": "camila@tuapp.com",
"role": "Head of Growth"
},
"lockedFields": ["name", "email"],
"metadata": { "userId": "u_8123", "campaign": "black-friday" },
"redirectUrl": "https://tuapp.com/gracias",
"theme": { "accent": "#2E78FF", "onAccent": "#FFFFFF" }
}'{
"id": "cmses9dk10000tiketa9",
"url": "http://localhost:3001/embed/checkout/cmses9dk10000tiketa9",
"status": "open",
"expiresAt": "2026-07-17T16:34:00.000Z",
"ticketType": { "id": "TICKET_TYPE_ID", "name": "GENERAL", "price": 29, "currency": "USD" },
"priceOverride": null,
"theme": {
"accent": "#2e78ff",
"onAccent": "#FFFFFF",
"accentSoft": "#6ba1ff",
"accentDeep": "#2662d1"
}
}/api/v1/orders/{id | código}/api/v1/orders?email={email}&limit={n≤50}Ideal para dar acceso a otro servicio: "¿este correo tiene una orden paid del evento X?". Acepta el id interno o el código legible del ticket (K7M-4P2, con o sin guion).
# Por id de orden o por código legible (K7M-4P2)
curl http://localhost:3001/api/v1/orders/K7M-4P2 \
-H "Authorization: Bearer tk_demo_123"curl "http://localhost:3001/api/v1/orders?email=ana@empresa.com" \
-H "Authorization: Bearer tk_demo_123"{
"order": {
"id": "cmord2xk10000tiketa5",
"code": "K7M-4P2",
"status": "paid",
"source": "embed",
"amount": 29,
"currency": "USD",
"paymentMethod": "stripe",
"paymentRef": "pi_3PqX…",
"buyer": {
"name": "Ana Rodríguez",
"email": "ana@empresa.com",
"role": "Backend Developer",
"company": "Yape"
},
"event": {
"id": "cmev8xk2l0000tiketa01",
"slug": "peru-blockchain-conference-2026",
"name": "Perú Blockchain Conference 2026",
"startDate": "2026-07-11T14:00:00.000Z"
},
"ticketType": { "id": "cmev8xk2l0002tiketa01", "name": "GENERAL", "price": 29 },
"checkedInAt": null,
"metadata": { "userId": "u_8123" },
"ticketUrl": "http://localhost:3001/ticket/K7M-4P2",
"createdAt": "2026-07-17T15:02:11.000Z"
}
}/api/v1/tickets/validateEnvía el code, el qrToken o directamente el JSON que escaneaste del QR. Estados: valid used pending cancelled. Con checkIn: true además registra el ingreso (marca la hora y dispara el webhook ticket.checked_in).
# Solo consultar el estado
curl -X POST http://localhost:3001/api/v1/tickets/validate \
-H "Authorization: Bearer tk_demo_123" \
-H "Content-Type: application/json" \
-d '{ "code": "K7M-4P2" }'
# Registrar el ingreso de verdad (marca check-in + webhook ticket.checked_in)
curl -X POST http://localhost:3001/api/v1/tickets/validate \
-H "Authorization: Bearer tk_demo_123" \
-H "Content-Type: application/json" \
-d '{ "qrToken": "3f9a1b…", "checkIn": true }'{
"ticket": {
"status": "valid",
"checkedIn": true,
"checkedInAt": "2026-07-11T14:22:40.000Z",
"code": "K7M-4P2",
"orderId": "cmord2xk10000tiketa5",
"buyer": { "name": "Ana Rodríguez", "email": "ana@empresa.com", "role": "Backend Developer", "company": "Yape" },
"event": { "slug": "peru-blockchain-conference-2026", "name": "Perú Blockchain Conference 2026", "startDate": "2026-07-11T14:00:00.000Z" },
"ticketType": { "id": "cmev8xk2l0002tiketa01", "name": "GENERAL" }
}
}/api/public/events/{slug}Para pintar tus tiers desde el navegador sin exponer ninguna key: devuelve el evento publicado con sus entradas visibles, con CORS abierto. Es el endpoint que usa la web del conference para renderizar sus precios en vivo — combínalo con el widget y tienes venta completa sin backend propio.
# Sin API key — pensado para pintar tiers desde el navegador (CORS *)
curl http://localhost:3001/api/public/events/peru-blockchain-conference-2026/embed.js es vanilla JS (~4 KB, sin dependencias, compatible ES5). Crea un overlay con el checkout completo en un iframe con fondo transparente — pagos, QR y correo incluidos. La página /embed/checkout/* se sirve con frame-ancestors * para poder incrustarse en cualquier dominio (en producción puedes restringirla a los dominios de tus partners).
<!-- 1) Carga el script una sola vez -->
<script src="http://localhost:3001/embed.js" async></script>
<!-- 2) Abre el checkout donde quieras -->
<button id="comprar-vip">Comprar VIP</button>
<script>
document.getElementById("comprar-vip").addEventListener("click", function () {
Tiketa.open({
ticketTypeId: "TICKET_TYPE_ID",
onSuccess: function (data) {
// data = { orderId, code }
console.log("Entrada vendida:", data.code);
},
onClose: function () {
console.log("Checkout cerrado");
},
});
});
</script>Para cortesías, prefill o precios especiales, crea primero la sesión desde tu servidor y pásala al widget:
// 1) SERVER-SIDE: crea la sesión con tu API key (nunca la expongas en el navegador)
const res = await fetch("http://localhost:3001/api/v1/checkout-sessions", {
method: "POST",
headers: {
Authorization: "Bearer tk_demo_123",
"Content-Type": "application/json",
},
body: JSON.stringify({
ticketTypeId: "TICKET_TYPE_ID",
prefill: { name: "Camila Rojas", email: "camila@tuapp.com", role: "Head of Growth" },
lockedFields: ["name", "email", "role"],
priceOverride: 0, // 0 = entrada cortesía (regalos, ruletas, ganadores)
redirectUrl: "https://tuapp.com/gracias",
// Los botones del checkout se pintan con TU acento. Solo `accent` es
// obligatorio: el texto de encima se elige por contraste y las variantes
// clara/oscura se derivan, así que con un color basta.
theme: { accent: "#2E78FF", onAccent: "#FFFFFF" },
}),
});
const session = await res.json(); // { id, url, expiresAt, ... }
// 2) CLIENT-SIDE: abre el widget con esa sesión
Tiketa.open({ session: session.id, baseUrl: "http://localhost:3001" });// Modo inline: el checkout vive dentro de tu página (sin overlay).
// El iframe ajusta su altura solo con los mensajes tiketa:resize.
const handle = Tiketa.mount(document.getElementById("checkout"), {
ticketTypeId: "TICKET_TYPE_ID",
onSuccess: (data) => console.log("Orden", data.orderId),
});
// Al desmontar tu vista:
handle.destroy();| Campo | Tipo | Descripción |
|---|---|---|
session | string | id de una CheckoutSession. Tiene prioridad sobre ticketTypeId. |
ticketTypeId | string | Venta directa de una entrada, sin sesión previa. |
baseUrl | string | Origen de Tiketa. Por defecto se infiere del src del script. |
onSuccess | función | Recibe { orderId, code } al confirmarse la orden. |
onClose | función | El usuario cerró el checkout (X, ESC o clic fuera). |
Protocolo postMessage (iframe → tu página), por si prefieres escucharlo tú:
| Campo | Tipo | Descripción |
|---|---|---|
tiketa:ready | — | El checkout terminó de cargar; el widget oculta su spinner. |
tiketa:resize | { height } | Altura del contenido — en modo mount el iframe se ajusta solo. |
tiketa:success | { orderId, code } | Orden confirmada. Dispara tu callback onSuccess. |
tiketa:close | — | El usuario cerró el checkout. Dispara onClose. |
tiketa:redirect | { url } | El usuario pulsó “Volver al sitio” — el widget navega a redirectUrl. |
Configura uno o varios destinos en Configuración API y Tiketa enviará a cada uno un POST independiente por cada order.created, order.paid y ticket.checked_in. Si tu servidor no responde 2xx, reintentamos 3 veces con backoff (1s → 10s → 60s) y cada entrega queda registrada con botón de reenvío en el admin.
POST https://tuapp.com/webhooks/tiketa
Content-Type: application/json
User-Agent: Tiketa-Webhooks/1.0
X-Tiketa-Event: order.paid
X-Tiketa-Delivery: cmdel8xk20000tiketa7
X-Tiketa-Signature: t=1789000000,v1=5f8a2c41d9…{
"id": "cmdel8xk20000tiketa7",
"type": "order.paid",
"createdAt": "2026-07-17T15:04:05.000Z",
"data": {
"orderId": "cmord2xk10000tiketa5",
"code": "K7M-4P2",
"status": "paid",
"paymentMethod": "stripe",
"paymentRef": "pi_3PqX…",
"eventSlug": "peru-blockchain-conference-2026",
"ticketType": "GENERAL",
"amount": 29,
"currency": "USD",
"buyer": { "name": "Ana Rodríguez", "email": "ana@empresa.com" },
"metadata": "{\"userId\":\"u_8123\"}"
}
}Verifica SIEMPRE la firma. v1 es el HMAC-SHA256 de {timestamp}.{rawBody} con tu whsec_…. Usa el body crudo, compara en tiempo constante y rechaza timestamps de más de 5 minutos.
const crypto = require("crypto");
const express = require("express");
/**
* Verifica el header X-Tiketa-Signature: "t={timestamp},v1={hmac}".
* v1 = HMAC-SHA256 de "{timestamp}.{rawBody}" con tu webhookSecret (whsec_…).
* Tolerancia de 5 minutos contra replay. Comparación en tiempo constante.
*/
function verifyTiketaSignature(rawBody, signatureHeader, webhookSecret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("="))
);
const timestamp = Number(parts.t);
if (!timestamp || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// IMPORTANTE: usa el body CRUDO para verificar — nunca el JSON re-serializado.
app.post(
"/webhooks/tiketa",
express.raw({ type: "application/json" }),
(req, res) => {
const ok = verifyTiketaSignature(
req.body.toString("utf8"),
req.get("X-Tiketa-Signature") || "",
process.env.TIKETA_WEBHOOK_SECRET
);
if (!ok) return res.status(401).send("firma inválida");
const evento = JSON.parse(req.body);
switch (evento.type) {
case "order.paid":
// Entrega el acceso, actualiza tu CRM, emite la factura…
break;
case "order.created":
case "ticket.checked_in":
break;
}
// Responde 2xx rápido: si no, Tiketa reintenta 3 veces (1s / 10s / 60s).
res.sendStatus(200);
}
);
app.listen(4000);Los tres casos de negocio funcionando contra esta misma base de datos, con el /embed.js real. Cada compra crea órdenes de verdad: revísalas después en las métricas del admin y en el outbox de correos.
ticketTypeId reales del seed — para tus curl
cmrpvftko0001tbkgcyus9230cmrpvftko0002tbkgqierdekfcmrpvftko0003tbkgn0vfe2gdcmrpvftko0004tbkgshc9420eGira y llévate una de las 3 entradas del evento seed como cortesía. Al caer el premio se crea una CheckoutSession real con priceOverride: 0 y se abre el widget de verdad.
Estas cards llaman Tiketa.open({ ticketTypeId }) sin tocar ningún backend.
GENERAL
$29$59
VIP
Más popular$249$399
EXPERIENCE
$499$999
Tu plataforma · usuario registrado
Estos datos ya viven en tu sistema: viajan como prefill bloqueado — el widget no los vuelve a pedir.
Vende la entrada GENERAL ($29) a este usuario sin hacerle repetir formularios: el checkout abre mostrando solo el campo empresa (opcional) y va directo al pago.
Pide tu API key al equipo de Latam Blockchain Events, apunta tus requests al dominio real y configura tus destinos de webhook. Nada más cambia: mismos endpoints, mismo widget, misma firma.