reservaqui
v0.3.2
Published
Reserva Aqui embed SDK — renders an interactive seat map inside an iframe and communicates via postMessage
Maintainers
Readme
reservaqui
SDK para embedar o mapa de assentos Reserva Aqui em qualquer página web.
npm install reservaquiVia CDN:
<script src="https://cdn.jsdelivr.net/npm/reservaqui/dist/reservaqui.umd.js"></script>Guia de integração
Pré-requisitos — o que você precisa ter em mãos
| Dado | O que é | Exemplo |
|---|---|---|
| workspaceKey | Chave pública do workspace | pub_abc123 |
| event | ID numérico do evento na tabela events | "42" |
| baseUrl | URL base do seu servidor Reserva Aqui | https://tickets.myapp.com |
Atenção:
eventdeve ser o ID numérico (42), não um slug ("meu-show") nem um UUID. Se passar errado, o backend rejeita todas as requisições com 4xx.
Passo 1 — Renderizar o mapa
<div id="seat-map"></div>
<script type="module">
import { SeatingChart } from 'reservaqui';
const chart = new SeatingChart({
divId: 'seat-map',
baseUrl: 'https://tickets.myapp.com',
workspaceKey: 'pub_abc123',
event: '42', // ID numérico do evento
mode: 'simplified', // ou 'manager'
onReady(eventId, objectKeys) {
console.log('Mapa pronto', eventId);
},
onSelectionChanged(seatIds, ticketTypes, objectKeys, items, pricingSelection) {
console.log('Seleção atual:', seatIds);
},
onError(action, message) {
console.error('Erro Reserva Aqui:', action, message);
},
}).render();
</script>Passo 2 — Criar session token (obrigatório antes de qualquer hold)
O SDK cria e gerencia o token automaticamente. Basta chamar antes de fazer o hold:
// O SDK verifica se o token ainda é válido; recria se estiver perto de expirar.
const { session_token, expires_at } = await chart.ensureValidSession();Ou se quiser criar explicitamente na primeira vez:
const { session_token, expires_at } = await chart.createSessionToken();O SDK salva o token internamente e o renova automaticamente antes de expirar. Você não precisa gerenciar o timer manualmente.
Passo 3 — Fazer hold por label
// label é o campo "label" do inventário (ex: "A-1", "Mesa 3")
// NÃO é o objectId do builder
const result = await chart.holdByLabel('A-1');
// result: { label: 'A-1', status: 'held', created: true }Para liberar:
await chart.releaseByLabel('A-1');Como obter o label correto? Chame
getInventory()e use o campolabelde cada item:
const inventory = await chart.getInventory();
// inventory.items[n].label ← use este valor no holdByLabel()Passo 4 — Receber o evento de hold criado
Quando o mapa interno cria um hold, o SDK dispara onHoldCreated:
const chart = new SeatingChart({
// ...
onHoldCreated(holdId, holdToken, expiresAt, seatIds, ticketTypes, objectKeys, items) {
// holdId === holdToken === sessionToken (após o refactor de session-token)
// Envie para o seu backend finalizar o pedido
fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionToken: holdId, seatIds, ticketTypes }),
});
},
}).render();Fluxo completo em código
import { SeatingChart } from 'reservaqui';
const chart = new SeatingChart({
divId: 'seat-map',
baseUrl: 'https://tickets.myapp.com',
workspaceKey: 'pub_abc123',
event: '42',
onReady: () => console.log('Mapa pronto'),
onError: (action, msg) => console.error(action, msg),
}).render();
// Quando o usuário clicar em "Reservar":
async function reservar(label) {
try {
await chart.ensureValidSession(); // 1. garante token válido
const result = await chart.holdByLabel(label); // 2. faz o hold
console.log('Hold criado:', result);
} catch (err) {
console.error('Falha no hold:', err.status, err.code, err.message);
}
}Headers enviados automaticamente
O SDK injeta esses headers em todas as requisições protegidas:
| Header | Valor |
|---|---|
| X-SeatHold-Event-Id | config.event (ID numérico) |
| X-SeatHold-Public-Key | config.workspaceKey |
| X-SeatHold-Session-Token | token criado em /api/session-tokens |
Todas as requisições também enviam credentials: 'include' para suportar cookies CSRF em ambientes browser.
Erros comuns e o que fazer
| Status | Código | Causa | Solução |
|---|---|---|---|
| 4xx | — | event não é numérico | Usar o ID numérico do evento, não slug |
| 401/403 | invalid_or_expired_session_token | Token expirou | Chamar ensureValidSession() antes do hold — o SDK renova automaticamente |
| 404 | — | Label errado no path | Confirmar que o label vem de getInventory(), não do objectId do builder |
| 419 | — | Cookie CSRF ausente | SDK já envia credentials: 'include' desde a v0.1.17 |
| 4xx | workspace_key_required | Header público ausente | Verificar que workspaceKey está correto |
Multiprice (opcional)
As categorias/setores são cadastrados e enviados exclusivamente pelo Reserva Aqui
no evento seathold:ready. O app que incorpora o mapa fornece apenas os tipos de
ingresso e seus preços em centavos. Configure o pricing depois que o mapa estiver
pronto, usando o id recebido nas categorias:
const chart = new SeatingChart({
divId: 'seat-map',
baseUrl: 'https://tickets.myapp.com',
workspaceKey: 'pub_abc123',
event: '42',
onReady(eventId, objectKeys, categories) {
// categories vem do evento seathold:ready
// [{ id: 'pista-1', label: 'Pista', key: 'pista' }]
chart.setPricing([
{
categoryId: categories[0].id,
ticketTypes: [
{ id: 'inteira', label: 'Inteira', price: 10000, currency: 'BRL', color: '#2563eb' },
{ id: 'meia', label: 'Meia', price: 5000, currency: 'BRL', color: '#16a34a' },
],
},
]);
},
}).render();Também é possível passar pricing na configuração inicial. O SDK aguarda o
seathold:ready, valida cada categoryId contra categories[].id e então envia
automaticamente a configuração ao iframe.
Se setPricing() for chamado antes do iframe estar pronto, o SDK guarda a
configuração e envia automaticamente após seathold:ready. As categorias também
podem ser consultadas com chart.getCategories().
O preço é sempre enviado em centavos: 10000 = R$ 100,00 e 5000 = R$ 50,00.
Um categoryId que não exista nas categorias recebidas é rejeitado com um erro
claro via onError e exceção do método setPricing.
Captura de session token do iframe
Se o próprio embed criar a sessão internamente (modo padrão sem createSessionToken() explícito):
const chart = new SeatingChart({
// ...
onSessionCreated(sessionToken, expiresAt) {
// primeira sessão criada pelo iframe
salvarNoBackend(sessionToken, expiresAt);
},
onSessionUpdated(sessionToken, expiresAt) {
// sessão renovada pelo iframe
salvarNoBackend(sessionToken, expiresAt);
},
}).render();Referência rápida de métodos
chart.render() // monta o iframe
chart.destroy() // remove o iframe e listeners
await chart.createSessionToken() // POST /api/session-tokens (primeira vez)
await chart.refreshSessionToken() // força renovação imediata
await chart.ensureValidSession() // reusa ou renova se próximo de expirar
await chart.getBuilder() // GET /api/render-map/builder
await chart.getInventory() // GET /api/render-map/inventory
await chart.holdByLabel('A-1') // POST /api/inventory/A-1/hold
await chart.releaseByLabel('A-1') // POST /api/inventory/A-1/release
chart.setSelectedSeats([1, 2, 3]) // seleciona assentos programaticamente
chart.holdCreated(token, expiresAt) // notifica o iframe de um hold externo
chart.releaseHold() // libera o hold atual no iframe
chart.updateSession(token, expiresAt) // atualiza token no iframe
chart.requestState() // solicita snapshot do estado atual
chart.setPricing(rules) // atualiza pricing em tempo real (categoryId)Referência de tipos
type SeatingChartConfig = {
divId: string;
baseUrl: string;
workspaceKey: string;
event: string; // ID numérico como string
mode?: 'manager' | 'simplified';
environment?: 'production' | 'sandbox';
sessionToken?: string;
sessionExpiresAt?: string | null;
sessionRefreshBufferMs?: number; // padrão: 30000ms
pricing?: PricingRule[];
height?: number | string; // padrão: 600px
width?: number | string; // padrão: 100%
onReady?: (eventId: string, objectKeys?: string[], categories?: SeatingCategory[]) => void;
onSelectionChanged?: (seatIds, ticketTypes, objectKeys, items, pricingSelection) => void;
onObjectClicked?: (objectId, objectType, objectKey?, categoryKey?) => void;
onCategoryChanged?: (categoryKey: string | null) => void;
onViewChanged?: (zoom: number, position: { x: number; y: number }) => void;
onHoldCreated?: (holdId, holdToken, expiresAt, seatIds, ticketTypes, objectKeys, items) => void;
onHoldReleased?: () => void;
onState?: (state: SessionState) => void;
onSessionCreated?: (sessionToken: string, expiresAt: string) => void;
onSessionUpdated?: (sessionToken: string | null, expiresAt: number | null) => void;
onError?: (action: string, message: string) => void;
};
type PricingRule = {
categoryId: string | number; // deve existir em seathold:ready.categories[].id
ticketTypes: TicketType[];
};
type SeatingCategory = {
id: string | number;
label: string;
key?: string | null;
};
type TicketType = {
id: string;
label: string;
color?: string | null;
price: number; // centavos: 10000 = R$ 100,00
currency: string;
};
type InventoryStatusResponse = {
label: string;
status: 'available' | 'held' | string;
created?: boolean;
};
type SessionTokenResponse = {
session_token: string;
expires_at: string;
};