Build
Build with Atmosphere Tickets.
Use app service-auth, buyer assertions, availability, holds, checkout, free claims, QR passes, and ticket integrations.
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.
| Auth | Used for | What it proves |
|---|---|---|
| App service-auth | Ticket XRPC calls | A registered event app is calling ATM Tickets with an exact method scope. |
| Organizer approval | Availability, holds, free claims | The organizer approved the app to sell or claim tickets for the event. |
| Buyer assertion | Paid or free ticket claim | The app saw this signed-in buyer for this action. |
| Organizer assertion | Ticket setup | The organizer authorized the app to configure ticketing for the event. |
| OAuth grant | Protocol writes | The 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.
App configuration
Enable Tickets per environment, set app fee behavior, configure QR/wallet options, and choose public-record defaults.
Sell into an event
Request ticket recipient approval before availability, holds, free claims, or paid checkout can run for an organizer.
Webhook settings
Choose events, rotate signing secrets, send test events, inspect attempts, configure optional XRPC receivers, and redrive failed deliveries.
Service-auth guidance
Copy audience, lxm values, example requests, environment-specific endpoints, and ticket method scopes.
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.
| Package | Version | Use | Notes |
|---|---|---|---|
@atmosphere-money/app-node | 0.0.0-beta.2 | Server-side ATM/Tickets app calls. | Dependency-light Node package for service-auth, holds, webhook verification, and XRPC receiver helpers. |
@atmosphere-money/testing | beta | Fixtures and callback tests. | Use for sample tickets.issued, ticket.checked_in, signatures, and replay/idempotency checks. |
@atmosphere-money/mcp | beta | Agent-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 referenceMCP 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 actions | Inspect app config, generate ticket fixtures, build webhook test payloads, list delivery logs, redrive test events, and verify receiver health. |
|---|---|
| Keep out of MCP for beta | Live charges, refunds, payout settings, organizer KYC, buyer PII export, and arbitrary public repo writes. |
| Receiver choice | Use HTTP webhooks by default. Choose XRPC receiver callbacks only when the app already hosts an XRPC surface. |
| Source of truth | Fulfillment 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.
| Runtime | Use | Notes |
|---|---|---|
| Generic Node | Long-lived app services, agents, workers. | Use the starter in examples/tickets-node-app as the lowest-common-denominator reference. |
| Next.js route handlers | Dashboard-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/Fastify | Traditional API servers. | Preserve raw request bodies for signed webhook verification before JSON parsing. |
| Hono | Small AT Protocol services and edge-like Node runtimes. | Use the Hono webhook helper and keep service-auth signing in server context. |
| Cloudflare Workers | Lightweight receiver and scanner surfaces. | Use the Workers helper for webhook verification; keep durable idempotency in KV, Durable Objects, or Postgres, not memory. |
| SvelteKit | Atmosphere 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. |
Every framework implements the same five pieces
- Mint a fresh app service-auth JWT for the exact
tickets.atmosphere.*method. - Optionally attach buyer or organizer assertions when acting for a signed-in user.
- Create holds or free claims on the server and return only safe UI data to the browser.
- Verify signed webhooks or optional XRPC receiver callbacks before fulfillment.
- 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.
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);
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 });
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);
}
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
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.
Free limited claim
Buyer clicks Get ticket, the app sends app service-auth plus buyer assertion, Tickets locks capacity and issues immediately without checkout.
Check-in
Scanner reads an opaque token, calls verify for display state, then calls checkInTicket for the mutating entry action.
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.
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();
}
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();
}
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();
}
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
Availability
curl "https://checkout.atmosphere.money/xrpc/tickets.atmosphere.getTicketAvailability?eventUri=at://...&environment=test" \
-H "Authorization: Bearer $APP_SERVICE_AUTH"
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"
}
Claim free ticket
{
"environment": "test",
"eventUri": "at://...",
"ticketTierId": "tier_free",
"buyerDid": "did:plc:buyer",
"buyerAssertionJwt": "...",
"idempotencyKey": "claim:event:buyer:tier"
}
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.
| Risk | Required behavior | App behavior |
|---|---|---|
| Concurrent holds | Lock capacity groups and tiers in deterministic order. | Refresh availability after a 409 response. |
| Double submit | Use idempotency keys for hold, issuance, refund, void, and check-in mutations. | Reuse the same key while retrying the same user action. |
| Checkout expiry | Release capacity when the hold expires or payment is canceled. | Send the buyer back to ticket selection with a fresh availability read. |
| Webhook retry | Deduplicate by event id and delivery id before side effects. | Make handlers safe to run more than once. |
| Repeat scan | Return 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.
- Buyer clicks Get ticket inside the event app.
- Event app calls Tickets with app service-auth.
- Request includes buyer DID and buyer assertion.
- Tickets verifies the app, buyer assertion, limits, and capacity.
- 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.
Scan authority
Hashed token secrets, revocation status, ticket status, wallet provider ids, and sync errors.
Presentation
Safe event, tier, brand, and scanner URL data for displaying the ticket to the buyer.
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.
Apple and Google Wallet
Wallet provider rows carry only opaque ATM scan URLs; hosted QR tickets remain the fallback when wallet passes are unavailable.
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.
Same verification contract
Apps that need branded door workflows can call verifyTicket and checkInTicket while preserving the same private token model.