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 atributos data-*; 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 /agenda diretamente e chama window.NewPass.openCheckout() para abrir o checkout, sem usar nenhum atributo data-*.

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

Para quem é esta página Times técnicos de organizadores com produto muito específico (agenda religiosa, pacotes de aventura, corridas) cujo layout não cabe na página pública padrão (/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:

  1. 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.
  2. 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, sem https:// nem barra final). O formulário do evento já bloqueia ativar embed_enabled sem nenhum domínio cadastrado.
Sem domínio cadastrado, o checkout não abre em overlay O JSON de /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).

CampoTipoDescrição
idstring (UUID)ID do evento
namestringNome do evento
slugstringSlug do evento (único por organizador)
banner_urlstring | nullURL da imagem de capa
starts_atstring (ISO 8601) | nullInício do evento
ends_atstring (ISO 8601) | nullFim do evento
venueobject | null{ name, city, state } — nunca o endereço completo
lowest_priceinteger (centavos) | nullMenor preço entre os produtos visíveis; null se não houver produto com preço
event_urlstring (URL absoluta)Link da página pública do evento (/o/{tenant}/{evento})
checkout_urlstring (URL absoluta)Link pronto do checkout — usar direto em openCheckout({ event: checkout_url })
productsarrayProdutos visíveis: [{ id, name, price }]id é o batch_type_id, usado em ?products[]=/data-products
Evento protegido por senha (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
});
Use sempre 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>

AtributoObrigatórioDescrição
data-tenantPara o modo declarativoSlug 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]

AtributoObrigatórioDescrição
data-tenantNãoSobrescreve o do <script> — útil com mais de um tenant na mesma página.
data-limitNãoNúmero máximo de eventos renderizados. Sem teto por padrão.
data-modeNão"link" (padrão) abre event_url numa nova aba; "checkout" abre o overlay direto via openCheckout.

Em cada [data-newpass-buy]

AtributoObrigatórioDescrição
data-eventSim"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-productsNãoLista de UUIDs separados por vírgula (mesmos products[].id da API) — filtra a grade do checkout.
data-cupomNãoCódigo de cupom a auto-aplicar.
data-refNãoCó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:

  1. Cadastrar o domínio exato (sem https://, sem porta, sem caminho) em /admin → Configurações → "Agenda e Embed".
  2. O domínio precisa ser servido em HTTPS — a allowlist só aceita origens https://.
  3. 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:

  1. O evento está com status "Publicado" (rascunho nunca aparece)?
  2. O toggle "Disponibilizar na agenda pública (embed)" está ativo? É independente do toggle de marketplace.
  3. O evento já encerrou? A agenda só lista eventos com fim do evento ou fim das vendas ainda no futuro.
  4. 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>.