Build

Build with Atmosphere Tickets.

Use app service-auth, buyer assertions, availability, holds, checkout, free claims, QR passes, and ticket integrations.

Developer beta SDK beta 0.0.0-beta.2 29 ticket lexicons Multi-app shared config roadmap

Auth model

Most ticket actions need two identities: the app that is making the infrastructure call, and the user whose intent is attached to the action. The app DID is not the buyer or organizer. It identifies the event app for module access, fees, webhooks, environment, and app-scoped visibility.

AuthUsed forWhat it proves
App service-authTicket XRPC callsA registered event app is calling ATM Tickets with an exact method scope.
Organizer approvalAvailability, holds, free claimsThe organizer approved the app to sell or claim tickets for the event.
Buyer assertionPaid or free ticket claimThe app saw this signed-in buyer for this action.
Organizer assertionTicket setupThe organizer authorized the app to configure ticketing for the event.
OAuth grantProtocol writesThe grantee may write allowed records to a user repo.
app service-auth JWT
  iss = app DID
  aud = ATM broker/service DID
  lxm = tickets.atmosphere.createTicketHold

buyerAssertionJwt
  iss = buyer DID
  aud = ATM broker/service DID
lxm = money.atmosphere.payment.assertPayer

Approval gates are part of auth.

Creating a tier does not by itself grant a selling app access to availability, paid holds, or free limited claims. ATM checks the app module, environment, service-auth token, organizer approval, and optional buyer/organizer assertion before mutating ticket state.

App dashboard config

The ATM app dashboard is the control plane for ticketing apps. Organizers should not see service-auth, receiver signatures, fee routing, or redrive logs unless they are also the app operator.

Developer

App configuration

Enable Tickets per environment, set app fee behavior, configure QR/wallet options, and choose public-record defaults.

Organizer approval

Sell into an event

Request ticket recipient approval before availability, holds, free claims, or paid checkout can run for an organizer.

Developer

Webhook settings

Choose events, rotate signing secrets, send test events, inspect attempts, configure optional XRPC receivers, and redrive failed deliveries.

Developer

Service-auth guidance

Copy audience, lxm values, example requests, environment-specific endpoints, and ticket method scopes.

Tickets

Operational bridge

View ticket purchase payments, delivery attempts, scanner activity, and safe support ids in ATM without exposing raw ticket secrets.

SDK examples

Keep ticket calls on the app server. Tickets uses ATM's published beta @atmosphere-money/app-node package rather than a separate ticket SDK because holds, checkout, webhooks, and payment status all share ATM app auth.

import { createAtmAppClient } from "@atmosphere-money/app-node";

const atm = createAtmAppClient({
  getServiceAuthToken: async ({ lxm, aud }) => {
    return mintMyAppServiceAuthJwt({ lxm, aud });
  }
});
// Node/TypeScript: create a paid ticket hold
const hold = await atm.createTicketHold({
  environment: "test",
  eventUri: "at://did:plc:organizer/community.lexicon.calendar.event/abc",
  buyerDid: "did:plc:buyer",
  buyerAssertionJwt,
  items: [{ ticketTierId: "tier_...", quantity: 2 }],
  returnUrl: "https://events.example/orders/123",
  cancelUrl: "https://events.example/e/abc",
  idempotencyKey: "hold:event:buyer:tier:2"
});
# curl: read availability
curl "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.getTicketAvailability?eventUri=at://...&environment=test" \
  -H "Authorization: Bearer $APP_SERVICE_AUTH"

SDK beta and compatibility

Tickets uses the same beta app SDK and testing helpers as ATM. Keep SDK updates paired with ATM API version notes, because ticket holds, app callbacks, checkout, and payment status share the same app authentication and event envelope.

PackageVersionUseNotes
@atmosphere-money/app-node0.0.0-beta.2Server-side ATM/Tickets app calls.Dependency-light Node package for service-auth, holds, webhook verification, and XRPC receiver helpers.
@atmosphere-money/testingbetaFixtures and callback tests.Use for sample tickets.issued, ticket.checked_in, signatures, and replay/idempotency checks.
@atmosphere-money/mcpbetaAgent-friendly developer tools.Local/test-mode wrapper for config inspection, fixture generation, webhook validation, and delivery log debugging.

Compatibility note

Public ticket lexicons are still in community review. Treat the beta SDK as the app-developer surface for pilots, and pin exact versions before running live traffic.

Read the ATM SDK reference

MCP and testing tools

The MCP server is not required to integrate with Tickets. It is a convenience layer for coding agents and local developer workflows that need to inspect configuration, create fixtures, send test events, and debug receiver delivery without browsing the dashboard.

Good MCP actionsInspect app config, generate ticket fixtures, build webhook test payloads, list delivery logs, redrive test events, and verify receiver health.
Keep out of MCP for betaLive charges, refunds, payout settings, organizer KYC, buyer PII export, and arbitrary public repo writes.
Receiver choiceUse HTTP webhooks by default. Choose XRPC receiver callbacks only when the app already hosts an XRPC surface.
Source of truthFulfillment still depends on signed ATM/Tickets events and service reads, not on MCP output.

Framework recipes

Tickets follows the same model as ATM docs: the contract is framework-neutral, and framework examples all implement the same app-server responsibilities. Keep service-auth minting, hold creation, webhook/XRPC receiver verification, and fulfillment mutations on a trusted backend.

RuntimeUseNotes
Generic NodeLong-lived app services, agents, workers.Use the starter in examples/tickets-node-app as the lowest-common-denominator reference.
Next.js route handlersDashboard-adjacent event apps and Vercel-style deployments.Create checkout/hold routes server-side and use the App Node SDK's Web Request webhook helpers.
Express/FastifyTraditional API servers.Preserve raw request bodies for signed webhook verification before JSON parsing.
HonoSmall AT Protocol services and edge-like Node runtimes.Use the Hono webhook helper and keep service-auth signing in server context.
Cloudflare WorkersLightweight receiver and scanner surfaces.Use the Workers helper for webhook verification; keep durable idempotency in KV, Durable Objects, or Postgres, not memory.
SvelteKitAtmosphere event apps with server routes or actions.Call Tickets from server routes or actions. Never call ticket hold or check-in mutation methods directly from browser code.
Shared

Every framework implements the same five pieces

  1. Mint a fresh app service-auth JWT for the exact tickets.atmosphere.* method.
  2. Optionally attach buyer or organizer assertions when acting for a signed-in user.
  3. Create holds or free claims on the server and return only safe UI data to the browser.
  4. Verify signed webhooks or optional XRPC receiver callbacks before fulfillment.
  5. Store delivery ids, hold ids, ticket ids, and scanner state idempotently in app storage.

Integration recipes

These are the minimum app-server recipes most event apps need: paid checkout, free scarce claims, scanner check-in, and event receiver handling. Keep app service-auth and receiver secrets on your server.

01

Paid ticket checkout

Read availability, create a hold, then redirect to the ATM checkout URL returned by the hold.

const availability = await tickets.getTicketAvailability({
  environment: "test",
  eventUri
});

assertCanBuy(availability, tierId, quantity);

const hold = await tickets.createTicketHold({
  environment: "test",
  eventUri,
  buyerDid,
  buyerAssertionJwt,
  items: [{ ticketTierId: tierId, quantity }],
  returnUrl: "https://events.example/orders/123",
  cancelUrl: "https://events.example/e/abc"
});

return Response.redirect(hold.checkout.url);
02

Free limited ticket claim

Use the same scarce inventory transaction without a payment redirect.

const claim = await tickets.claimFreeTicket({
  environment: "test",
  eventUri,
  ticketTierId: tierId,
  buyerDid,
  buyerAssertionJwt,
  idempotencyKey: "claim:" + eventUri + ":" + buyerDid + ":" + tierId
});

return Response.json({ ticket: claim.ticket });
03

Scanner check-in

Scan an opaque token, never a raw ticket id or buyer DID. Repeat scans return existing state.

const result = await tickets.checkInTicket({
  ticketToken: scannedQrToken,
  checkInListId,
  idempotencyKey: "scan:" + scannedQrTokenHash
});

if (result.status === "already_checked_in") {
  showAlreadyCheckedIn(result.checkedInAt);
} else {
  showEntryAllowed(result.ticket);
}
04

Event receiver

Use either HTTP webhooks or the optional XRPC receiver. Both are signed by ATM and are at-least-once.

async function handleAtmTicketEvent(event) {
  if (!(await insertDeliveryId(event.deliveryId))) return;

  switch (event.type) {
    case "tickets.issued":
      await showTicketsToBuyer(event.data);
      break;
    case "ticket.checked_in":
      await updateScannerDashboard(event.data);
      break;
    case "ticket.voided":
      await revokeVisiblePass(event.data.ticketId);
      break;
  }
}

Sequence diagrams

01

Paid ticket purchase

Buyer selects tickets in the event app, the app creates a hold, ATM checkout collects payment, then Tickets issues ticket records and sends events.

02

Free limited claim

Buyer clicks Get ticket, the app sends app service-auth plus buyer assertion, Tickets locks capacity and issues immediately without checkout.

03

Check-in

Scanner reads an opaque token, calls verify for display state, then calls checkInTicket for the mutating entry action.

04

Refund or void

ATM refund/dispute state triggers Tickets to void or review issued tickets and revoke pass tokens.

Paid ticket
buyer -> app: select tier
app -> Tickets: createTicketHold(app auth + buyer assertion)
Tickets -> app: hold + ATM checkout URL
buyer -> ATM: pay
ATM -> Tickets: payment confirmed
Tickets -> app: tickets.issued

Availability

Availability is a private service read derived from capacity, active holds, issued tickets, sale windows, tier visibility, and app environment. Apps should call availability from their server before rendering purchase buttons. Public records can show a safe availability hint, but they are not inventory truth.

GET /xrpc/tickets.atmosphere.getTicketAvailability
Authorization: Bearer <app service-auth jwt>

query:
  eventUri = at://...
  environment = test

Helper snippets

These snippets belong on the event app server. Replace token minting stubs with your AT Protocol auth stack and keep app service-auth, buyer assertions, webhook secrets, and scan tokens out of browser code.

01

Read availability

async function getAvailability(eventUri) {
  const params = new URLSearchParams({ environment: "test", eventUri });
  const response = await fetch(
    "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.getTicketAvailability?" + params,
    { headers: { authorization: "Bearer " + appServiceAuthJwt } }
  );
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
02

Create paid hold

async function createHold({ eventUri, ticketTierId, buyerDid, buyerAssertionJwt }) {
  const response = await fetch(
    "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.createTicketHold",
    {
      method: "POST",
      headers: {
        authorization: "Bearer " + appServiceAuthJwt,
        "content-type": "application/json"
      },
      body: JSON.stringify({
        environment: "test",
        eventUri,
        buyerDid,
        buyerAssertionJwt,
        items: [{ ticketTierId, quantity: 1 }]
      })
    }
  );
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
03

Claim free ticket

async function claimFreeTicket({ eventUri, ticketTierId, buyerDid, buyerAssertionJwt }) {
  const idempotencyKey = "claim:" + eventUri + ":" + buyerDid + ":" + ticketTierId;
  const response = await fetch(
    "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.claimFreeTicket",
    {
      method: "POST",
      headers: {
        authorization: "Bearer " + appServiceAuthJwt,
        "content-type": "application/json"
      },
      body: JSON.stringify({
        environment: "test",
        eventUri,
        ticketTierId,
        buyerDid,
        buyerAssertionJwt,
        idempotencyKey
      })
    }
  );
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
04

Check in token

async function checkInTicket({ ticketToken, checkInListId }) {
  const response = await fetch(
    "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.checkInTicket",
    {
      method: "POST",
      headers: {
        authorization: "Bearer " + scannerServiceAuthJwt,
        "content-type": "application/json"
      },
      body: JSON.stringify({ ticketToken, checkInListId })
    }
  );
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}

API examples

GET

Availability

curl "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.getTicketAvailability?eventUri=at://...&environment=test" \
  -H "Authorization: Bearer $APP_SERVICE_AUTH"
POST

Create paid hold

{
  "environment": "test",
  "eventUri": "at://...",
  "buyerDid": "did:plc:buyer",
  "buyerAssertionJwt": "...",
  "items": [{ "ticketTierId": "tier_...", "quantity": 2 }],
  "returnUrl": "https://events.example/orders/123",
  "cancelUrl": "https://events.example/e/abc"
}
POST

Claim free ticket

{
  "environment": "test",
  "eventUri": "at://...",
  "ticketTierId": "tier_free",
  "buyerDid": "did:plc:buyer",
  "buyerAssertionJwt": "...",
  "idempotencyKey": "claim:event:buyer:tier"
}
POST

Check in

{
  "ticketToken": "opaque_scan_token",
  "checkInListId": "list_...",
  "idempotencyKey": "scan:..."
}

Interactive API explorer

Generate copyable request shells for common Tickets calls. This docs widget does not send requests; it gives your app server a starting point.

curl "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.getTicketAvailability?eventUri=at://...&environment=test" \
  -H "Authorization: Bearer $APP_SERVICE_AUTH"

Holds and checkout

Paid and scarce tickets start with a hold. The hold reserves capacity for a short checkout window and returns the ATM checkout URL. If checkout fails, cancels, or expires, the hold releases. If ATM confirms payment, Tickets promotes the hold into issued tickets exactly once.

BEGIN
  lock capacity groups in deterministic order
  lock ticket tiers
  compute available = capacity - sold - active held
  reject if available is too low
  create hold with expiresAt
COMMIT

Checkout quantity should not be adjustable after the hold is created. If an app later needs adjustable quantities, reserve the maximum possible quantity before checkout and release unused capacity after confirmation.

Concurrency and idempotency

No-oversell is the core contract. Apps can render many storefronts, but Tickets must serialize scarce writes in the private database. Public repo records and relay/event-stream timing are never inventory locks.

RiskRequired behaviorApp behavior
Concurrent holdsLock capacity groups and tiers in deterministic order.Refresh availability after a 409 response.
Double submitUse idempotency keys for hold, issuance, refund, void, and check-in mutations.Reuse the same key while retrying the same user action.
Checkout expiryRelease capacity when the hold expires or payment is canceled.Send the buyer back to ticket selection with a fresh availability read.
Webhook retryDeduplicate by event id and delivery id before side effects.Make handlers safe to run more than once.
Repeat scanReturn the existing check-in state instead of creating another entry.Show already checked in with timestamp and scanner context.

Free limited tickets

Limited free tickets should use the same capacity transaction as paid tickets, without sending the user through checkout. This is expected to use tickets.atmosphere.claimFreeTicket. Apps should include buyer DID and a buyer assertion so free claims do not become anonymous RSVP spam.

  1. Buyer clicks Get ticket inside the event app.
  2. Event app calls Tickets with app service-auth.
  3. Request includes buyer DID and buyer assertion.
  4. Tickets verifies the app, buyer assertion, limits, and capacity.
  5. Tickets issues the ticket immediately and emits app events.

QR and wallet passes

QR codes and wallet passes are presentation layers over issued tickets. Apps can enable hosted QR tickets, Apple Wallet passes, and Google Wallet passes per environment without building a pass provider integration themselves. Pass branding should use the app profile plus organizer, event, and tier display metadata. QR payloads must contain only opaque scan tokens or scan URLs, never DIDs, emails, payment ids, ticket ids, attendee answers, or raw secrets.

Hosted buyer ticket pages use tickets.atmosphere.tickets. Scanner staff can use the default QR scanner app at scan.atmosphere.tickets, so event apps do not need to ship scanner infrastructure just to launch ticketed events. Apps can still build custom scanner UX later by calling the same verification and check-in methods. Both hosted surfaces are served by the ATM runtime for launch, with atmosphere.money fallback URLs available for operational recovery.

Stored privately

Scan authority

Hashed token secrets, revocation status, ticket status, wallet provider ids, and sync errors.

Returned to apps

Presentation

Safe event, tier, brand, and scanner URL data for displaying the ticket to the buyer.

Hosted delivery

Ticket page and QR fallback

Ticket emails link to the hosted ticket page and can include safe QR presentation data; PDFs are not required for launch.

Wallet passes

Apple and Google Wallet

Wallet provider rows carry only opaque ATM scan URLs; hosted QR tickets remain the fallback when wallet passes are unavailable.

Default scanner

scan.atmosphere.tickets

Scanner staff can verify and check in opaque tokens with the hosted scanner app before an event app builds its own door tools.

Custom scanner

Same verification contract

Apps that need branded door workflows can call verifyTicket and checkInTicket while preserving the same private token model.