Developersv1

Integra Tiketa en tu producto

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.

API REST v1Widget embebibleWebhooks firmadosDemos en vivo abajo
Empezar01

Tres formas de integrar Tiketa

Todo lo que ves en Tiketa — órdenes, QRs, emails, check-in y métricas — está disponible para tu propio producto:

  • API REST v1 — lee eventos, crea sesiones de checkout, consulta órdenes y valida tickets desde tu backend.
  • Widget embebible — un script de ~4 KB abre el checkout completo (pagos incluidos) sobre tu página, o inline con Tiketa.mount().
  • Webhooks 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).

Auth02

Autenticación

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:

GETAuthorization: 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.

Tu primer request
curl http://localhost:3001/api/v1/events \
  -H "Authorization: Bearer tk_demo_123"
Contrato03

Errores y límites de uso

Todo error responde el mismo shape — un code estable para tu lógica y un message en español para humanos:

Shape de error
{
  "error": {
    "code": "rate_limited",
    "message": "Superaste el límite de 100 solicitudes por minuto. Espera unos segundos y reintenta."
  }
}
Códigos de error de la API v1
CampoTipoDescripción
missing_api_key401Falta el header Authorization.
invalid_api_key401La API key no existe.
partner_inactive403La key está desactivada.
rate_limited429Más de 100 req/min con la misma key.
invalid_request400Body o parámetros inválidos (el mensaje dice exactamente qué).
not_found / ticket_type_not_found / ticket_not_found404El recurso no existe.
sold_out409La entrada no tiene cupo disponible.
event_unpublished409El evento de la entrada no está publicado.
session_expired410La CheckoutSession pasó su TTL de 30 minutos.
session_completed409La 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.

Referencia04

Eventos publicados

GET/api/v1/events
GET/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.

Request
curl http://localhost:3001/api/v1/events/peru-blockchain-conference-2026 \
  -H "Authorization: Bearer tk_demo_123"
200 OK (abreviado)
{
  "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
        }
      ]
    }
  ]
}
Referencia05

Checkout Sessions

POST/api/v1/checkout-sessions

Una 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.

Body de POST /api/v1/checkout-sessions
CampoTipoDescripción
ticketTypeIdstringLa entrada a vender (de GET /api/v1/events).
prefillobjectDatos ya conocidos: { name?, email?, role?, company? }.
lockedFieldsstring[]Campos que el comprador NO puede editar (se ocultan). El servidor los impone: el cliente no puede falsearlos.
priceOverridenumber ≥ 0Precio impuesto. 0 = cortesía: el checkout no pide pago.
metadataobject | stringDatos libres tuyos (máx 4 KB). Vuelven en órdenes y webhooks.
redirectUrlstring (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.

Request
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" }
  }'
201 Created
{
  "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"
  }
}
Referencia06

Consultar órdenes

GET/api/v1/orders/{id | código}
GET/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).

Request
# 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"
Por correo
curl "http://localhost:3001/api/v1/orders?email=ana@empresa.com" \
  -H "Authorization: Bearer tk_demo_123"
200 OK
{
  "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"
  }
}
Referencia07

Validar tickets

POST/api/v1/tickets/validate

Enví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).

Request
# 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 }'
200 OK
{
  "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" }
  }
}
Referencia08

API pública sin key

GET/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.

Request
# Sin API key — pensado para pintar tiers desde el navegador (CORS *)
curl http://localhost:3001/api/public/events/peru-blockchain-conference-2026
Embeds09

Widget embebible

/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).

Venta directa en 10 líneas
<!-- 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:

Con CheckoutSession
// 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 (mount)
// 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();
Opciones de Tiketa.open() y Tiketa.mount()
CampoTipoDescripción
sessionstringid de una CheckoutSession. Tiene prioridad sobre ticketTypeId.
ticketTypeIdstringVenta directa de una entrada, sin sesión previa.
baseUrlstringOrigen de Tiketa. Por defecto se infiere del src del script.
onSuccessfunciónRecibe { orderId, code } al confirmarse la orden.
onClosefunciónEl usuario cerró el checkout (X, ESC o clic fuera).

Protocolo postMessage (iframe → tu página), por si prefieres escucharlo tú:

Eventos postMessage del widget
CampoTipoDescripción
tiketa:readyEl 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:closeEl usuario cerró el checkout. Dispara onClose.
tiketa:redirect{ url }El usuario pulsó “Volver al sitio” — el widget navega a redirectUrl.
Eventos salientes10

Webhooks firmados

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.

Headers de la entrega
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…
Body
{
  "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.

Verificación completa en Node
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);
Pruébalo ya11

Demos en vivo

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

  • FREEcmrpvftko0001tbkgcyus9230
  • GENERALcmrpvftko0002tbkgqierdekf
  • VIPcmrpvftko0003tbkgn0vfe2gd
  • EXPERIENCEcmrpvftko0004tbkgshc9420e
Demo · Ruleta de la fortunaa

Entradas de cortesía con priceOverride 0

FREEGENERALVIPFREEGENERALVIP

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.

FREEGENERALVIP
Demo · Venta directab

Un botón, checkout completo

Estas cards llaman Tiketa.open({ ticketTypeId }) sin tocar ningún backend.

GENERAL

$29$59

  • Acceso presencial a todas las charlas del día
  • Zona expo con protocolos y fintechs
  • Coffee breaks incluidos

VIP

Más popular

$249$399

  • Todo lo de GENERAL
  • Asientos preferenciales en primeras filas
  • Almuerzo VIP con speakers

EXPERIENCE

$499$999

  • Todo lo de VIP
  • Cena privada con speakers internacionales
  • Meet & greet y foto oficial
Demo · Prefill + lockedFieldsc

El widget solo pide lo que falta

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.

¿Listo para integrar en producción?

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.

Developers — API pública y widget · Tiketa