Agenda embutível & Widget de checkout
Duas formas de o organizador exibir a agenda de eventos e vender ingressos no próprio site, sem sair do domínio dele:
-
Camada 1 — Widget declarativo (
/widget/v1.js): cola uma tag<script>e alguns atributosdata-*; o script renderiza a agenda como DOM nativo e abre o checkout num overlay. Zero JavaScript escrito pelo organizador. -
Camada 2 — Headless (esta página): para quem tem
desenvolvedor próprio e quer montar a vitrine do zero — consome o
JSON de
GET /agendadiretamente e chamawindow.NewPass.openCheckout()para abrir o checkout, sem usar nenhum atributodata-*.
As duas camadas são complementares e podem ser combinadas na mesma
página — o widget nunca interfere em elementos que não tenham os
atributos data-newpass-*.
/o/{tenant}) e que
preferem montar a vitrine com o próprio design system.
Pré-requisito de configuração
Antes de qualquer código funcionar, o organizador precisa habilitar
dois campos no próprio painel /admin — nenhum deles tem
efeito por API, só pelo painel:
-
Por evento — na edição do evento, aba "Detalhes",
ativar o toggle "Disponibilizar na agenda pública
(embed)" (
embed_enabled). É independente do toggle "Exibir no marketplace da plataforma" — um evento pode estar em qualquer combinação dos dois. Sem esse toggle, o evento nunca aparece no JSON de/agenda, mesmo publicado. -
Por organizador — em
Configurações → "Agenda e Embed", cadastrar pelo
menos um domínio (o domínio de onde o widget/headless vai rodar,
ex.:
ingressos.meusite.com.br, semhttps://nem barra final). O formulário do evento já bloqueia ativarembed_enabledsem nenhum domínio cadastrado.
/agenda funciona independente disso (CORS já é
aberto para qualquer origem nesse endpoint), mas o
checkout embutido em iframe é bloqueado pelo
navegador (Content-Security-Policy frame-ancestors) se o
domínio do site não estiver cadastrado. Ver
Troubleshooting.
Referência do endpoint
GET
/api/v1/public/{tenantSlug}/agenda
Público, sem autenticação, sem cookie/sessão. CORS liberado
(Access-Control-Allow-Origin: *) só para esta rota —
nenhuma outra rota da API tem CORS habilitado.
Sem rate limit dedicado nesta rota (mesmo precedente de
GET /api/v1/marketplace/events).
Resposta
Sempre um envelope {"data": [...]} — nunca um array cru.
A lista vem ordenada por starts_at crescente e inclui
apenas eventos publicados, com
embed_enabled = true, cujo access_protection_level
não seja total, e que ainda não tenham encerrado (fim do
evento no futuro, ou fim das vendas no futuro).
| Campo | Tipo | Descrição |
|---|---|---|
id | string (UUID) | ID do evento |
name | string | Nome do evento |
slug | string | Slug do evento (único por organizador) |
banner_url | string | null | URL da imagem de capa |
starts_at | string (ISO 8601) | null | Início do evento |
ends_at | string (ISO 8601) | null | Fim do evento |
venue | object | null | { name, city, state } — nunca o endereço completo |
lowest_price | integer (centavos) | null | Menor preço entre os produtos visíveis; null se não houver produto com preço |
event_url | string (URL absoluta) | Link da página pública do evento (/o/{tenant}/{evento}) |
checkout_url | string (URL absoluta) | Link pronto do checkout — usar direto em openCheckout({ event: checkout_url }) |
products | array | Produtos visíveis: [{ id, name, price }] — id é o batch_type_id, usado em ?products[]=/data-products |
access_protection_level = "products")
Nome, capa e data continuam públicos, mas products vem
[] e lowest_price vem null — o
catálogo fica atrás da senha, mesma fronteira da página pública do
evento. Não há como diferenciar isso de um evento sem nenhum produto
cadastrado a partir só do JSON — é intencional: este endpoint nunca
revela se um evento está protegido. Eventos com
access_protection_level = "total" nem aparecem na lista.
Exemplo de resposta
{
"data": [
{
"id": "0191a1a0-1234-7abc-9def-000000000001",
"name": "Festival de Jazz 2026",
"slug": "festival-jazz-2026",
"banner_url": "https://newpass.com.br/storage/events/festival-jazz.webp",
"starts_at": "2026-11-14T20:00:00-03:00",
"ends_at": "2026-11-15T02:00:00-03:00",
"venue": { "name": "Arena Central", "city": "Florianópolis", "state": "SC" },
"lowest_price": 12000,
"event_url": "https://newpass.com.br/o/org-slug/festival-jazz-2026",
"checkout_url": "https://newpass.com.br/checkout/org-slug/festival-jazz-2026",
"products": [
{ "id": "0191a1a0-...-batchtype-1", "name": "Inteira", "price": 12000 },
{ "id": "0191a1a0-...-batchtype-2", "name": "Meia Entrada", "price": 6000 }
]
},
{
"id": "0191a1a0-1234-7abc-9def-000000000002",
"name": "Retiro Espiritual — acesso restrito",
"slug": "retiro-2026",
"banner_url": null,
"starts_at": "2026-12-01T08:00:00-03:00",
"ends_at": "2026-12-03T18:00:00-03:00",
"venue": null,
"lowest_price": null,
"event_url": "https://newpass.com.br/o/org-slug/retiro-2026",
"checkout_url": "https://newpass.com.br/checkout/org-slug/retiro-2026",
"products": []
}
]
}
API JS — window.NewPass
Disponível assim que a tag <script src="/widget/v1.js">
(ou embed.js) termina de executar — chamável de imediato
mesmo antes do core (newpass-core.js) terminar de
carregar (as primeiras chamadas ficam numa fila interna e são
processadas assim que o core carrega, tipicamente poucos
milissegundos, cacheado após a primeira vez).
window.NewPass.render(options)
Renderiza a agenda de um tenant dentro de um container — versão programática do modo declarativo.
window.NewPass.render({
tenant: 'org-slug', // obrigatório se o <script> não tiver data-tenant
container: '#minha-agenda', // seletor CSS (string) ou elemento DOM
limit: 6, // opcional — nº máximo de eventos
mode: 'checkout', // 'link' (padrão) ou 'checkout'
});
// Retorna uma Promise<void> resolvida quando a renderização termina.
Gera a mesma estrutura DOM e as mesmas classes .np-* do
modo declarativo (ver Atributos data-*) —
útil quando o container não deve ser auto-inicializado por
[data-newpass-agenda] (ex.: SPA que controla a própria
montagem).
window.NewPass.openCheckout(options)
Abre o checkout em overlay (desktop) ou tela cheia (mobile), via iframe.
window.NewPass.openCheckout({
event: 'org-slug/festival-jazz-2026', // "tenant/evento" OU checkout_url absoluta da API
products: ['0191a1a0-...-batchtype-2'], // opcional — filtra a grade (mesmos ids de products[].id)
cupom: 'VERAO10', // opcional
ref: 'ComissarioNome', // opcional — código de comissário
});
checkout_url quando disponível
O JSON de /agenda já devolve a URL de checkout pronta
por evento — passe-a direto em event
(openCheckout({ event: item.checkout_url })) em vez de
remontar "tenant/evento" na mão.
openCheckout também aceita uma assinatura posicional
legada (openCheckout(checkoutPath, ref, cupom)), usada
por embed.js — em código novo, prefira sempre a forma
por objeto acima.
window.NewPass.close()
Fecha o overlay de checkout aberto, se houver. Não é chamado automaticamente ao concluir a compra — o checkout permanece na tela de confirmação até o visitante fechar.
Evento newpass:checkout_complete
Disparado em document quando o checkout é concluído (o comprador nunca precisa fechar o overlay manualmente para isso disparar):
document.addEventListener('newpass:checkout_complete', function (e) {
console.log('Pedido concluído:', e.detail.orderId);
// window.NewPass.close(); // opcional — o site decide se fecha o overlay
});
Atributos data-* (widget declarativo)
Úteis mesmo em integrações majoritariamente headless — dá para
misturar um [data-newpass-buy] pontual (ex.: botão
"Comprar" de uma landing) com renderização headless do resto da
agenda.
No <script>
| Atributo | Obrigatório | Descrição |
|---|---|---|
data-tenant | Para o modo declarativo | Slug do organizador — vira o default de todos os [data-newpass-agenda]/[data-newpass-buy] da página que não sobrescreverem o próprio. |
Em cada [data-newpass-agenda]
| Atributo | Obrigatório | Descrição |
|---|---|---|
data-tenant | Não | Sobrescreve o do <script> — útil com mais de um tenant na mesma página. |
data-limit | Não | Número máximo de eventos renderizados. Sem teto por padrão. |
data-mode | Não | "link" (padrão) abre event_url numa nova aba; "checkout" abre o overlay direto via openCheckout. |
Em cada [data-newpass-buy]
| Atributo | Obrigatório | Descrição |
|---|---|---|
data-event | Sim | "tenant-slug/event-slug", ou só "event-slug" combinado com data-tenant em algum <script> da página. Sem nenhum dos dois, gera console.error explícito — nunca falha silenciosamente. |
data-products | Não | Lista de UUIDs separados por vírgula (mesmos products[].id da API) — filtra a grade do checkout. |
data-cupom | Não | Código de cupom a auto-aplicar. |
data-ref | Não | Código de comissário. |
Classes CSS emitidas (sem estilo próprio — D2)
O widget não injeta nenhum CSS — o organizador estiliza do zero:
.np-agenda /* container da agenda */
.np-agenda__loading /* classe extra no container durante o fetch */
.np-agenda__empty /* <p> exibido sem nenhum evento disponível */
.np-agenda__error /* <p> exibido em caso de falha no fetch */
.np-card /* <article> de cada evento */
.np-card__banner /* <img> */
.np-card__title /* <h3> */
.np-card__date /* <time> */
.np-card__venue /* <div> nome — cidade — estado */
.np-card__price /* <div> "A partir de R$ X" — ausente se lowest_price for null */
.np-card__cta /* <button> "Comprar" */
Exemplos de código
1. Declarativo simples (sem escrever JS)
<!-- Uma única tag basta — data-tenant vira o default da página -->
<script src="https://newpass.com.br/widget/v1.js" data-tenant="org-slug"></script>
<div data-newpass-agenda data-limit="6" data-mode="checkout"></div>
<button data-newpass-buy data-event="festival-jazz-2026">
Quero ir ao Festival de Jazz
</button>
2. Headless puro (fetch manual + render customizado)
Mesmo sem usar nenhum atributo data-*, a tag
<script> ainda precisa estar na página — é ela que
expõe window.NewPass.openCheckout. Sem
[data-newpass-agenda]/[data-newpass-buy] no
HTML, o widget não inicializa nada sozinho.
<script src="https://newpass.com.br/widget/v1.js"></script>
<div id="agenda-custom"></div>
<script>
async function montarAgenda() {
const resp = await fetch('https://newpass.com.br/api/v1/public/org-slug/agenda');
const { data: eventos } = await resp.json();
const container = document.getElementById('agenda-custom');
container.innerHTML = eventos.map((evento) => `
<div class="meu-card-de-evento">
<h3>${evento.name}</h3>
<p>${evento.venue ? evento.venue.city : ''}</p>
<button data-checkout-url="${evento.checkout_url}">Comprar</button>
</div>
`).join('');
container.querySelectorAll('[data-checkout-url]').forEach((btn) => {
btn.addEventListener('click', () => {
window.NewPass.openCheckout({ event: btn.dataset.checkoutUrl });
});
});
}
montarAgenda();
</script>
Troubleshooting
Erro de CORS ao buscar a agenda
GET /api/v1/public/{tenantSlug}/agenda já tem CORS
liberado para qualquer origem — se o navegador ainda assim bloquear
a leitura da resposta, confira: (1) o método é GET; (2)
a URL bate exatamente com /api/v1/public/{tenant}/agenda
(nenhuma outra rota da API tem CORS habilitado, então uma URL
diferente — inclusive a de checkout — sempre vai bloquear por
design). Isso não deveria acontecer numa integração correta, mas se
acontecer, o problema quase sempre é a URL chamada, não o domínio de
origem.
Checkout não abre / iframe em branco / erro de CSP no console
O navegador mostra algo como "Refused to display '…' in a frame
because it set 'X-Frame-Options' to 'deny'" ou uma mensagem
citando Content-Security-Policy e
frame-ancestors. Causa: o domínio de onde a página está
sendo servida não está na allowlist do checkout. Correção:
- Cadastrar o domínio exato (sem
https://, sem porta, sem caminho) em /admin → Configurações → "Agenda e Embed". - O domínio precisa ser servido em HTTPS — a
allowlist só aceita origens
https://. - Aguardar até a próxima chamada ao checkout — não há cache de minuto nesse valor, mas o navegador pode ter cacheado a resposta anterior; recarregar a página do organizador (não só o iframe) resolve.
Evento não aparece na agenda
Confira, nesta ordem:
- O evento está com status "Publicado" (rascunho nunca aparece)?
- O toggle "Disponibilizar na agenda pública (embed)" está ativo? É independente do toggle de marketplace.
- O evento já encerrou? A agenda só lista eventos com fim do evento ou fim das vendas ainda no futuro.
- O evento está com
access_protection_level = "total"? Esse nível nunca aparece na agenda, por design — não há como contornar via headless.
data-newpass-buy não faz nada ao clicar
Confira o console: se data-event for só um slug
(ex.: "festival-jazz-2026") sem nenhum
data-tenant conhecido na página (nem no próprio
elemento, nem no <script>), o widget grava um
console.error explícito e não abre nada — nunca falha
silenciosamente. Corrija passando "tenant-slug/event-slug"
completo ou adicionando data-tenant ao
<script>.