Examples
Runnable and copy-paste examples for ATM checkout, webhooks, status polling, and tickets.
Compatible with the closed-beta ATM app APIs and versioned ATM event headers. Check atm-api-version on every webhook or XRPC receiver event.
Local examples folder
The ATM repo includes an examples folder for closed-beta app developers. Treat these as starter-kit apps that exercise the same server-side contracts as the beta SDK scaffold.
cd examples/atm-node-app
cp .env.example .env
npm install
npm run devRun the starter kit
The starter kit is designed to prove the integration shape before you copy helpers into your app. Start with the test environment, send a dashboard test event, then try checkout/status once your app can verify signed events.
- Register the app in ATM test mode and enable the modules you want to test.
- Copy the test webhook signing secret into examples/atm-node-app/.env.
- Expose http://localhost:8787/webhooks/atm through a tunnel and save that as the test webhook URL.
- Send a payment.completed test event and confirm the server logs one verified delivery.
- Redrive the same delivery and confirm your app deduplicates by delivery id.
- Call POST /checkout, open the returned ATM checkout URL, then poll GET /status?token=... on return.
curl http://localhost:8787/health
# Then use the ATM dashboard test-event button to send a real signed delivery.Minimal checkout server
A minimal integration creates the app order, builds a private ATM checkout envelope, calls the strict initiate route, and redirects the buyer to the returned ATM checkout URL.
const checkout = await atm.initiatePayment({
recipient: "did:plc:creator",
amount: 1200,
currency: "usd",
paymentType: "shop",
returnUrl: "https://app.example/return",
cancelUrl: "https://app.example/product"
});
return Response.redirect(checkout.url);Hosted checkout recipe
This is the launch payment recipe for apps. Your server owns the app order and envelope. ATM owns the checkout page, processor session, wallets, receipts, and proof coordination.
async function startAtmCheckout(order: AppOrder) {
const product = await buildPrivateAtmEnvelope({
recipientDid: order.recipientDid,
paymentType: order.kind,
amount: order.amount,
currency: "usd",
listing: order.atmProductStrongRef,
payerDid: order.buyerDid,
buyerAssertionJwt: order.buyerAssertionJwt,
returnUrl: "https://app.example/orders/" + order.id,
cancelUrl: "https://app.example/products/" + order.productId
});
const serviceAuth = await mintServiceAuth({
lxm: "network.attested.payment.initiate"
});
const response = await fetch(
"https://checkout.atmosphere.money/xrpc/network.attested.payment.initiate",
{
method: "POST",
headers: {
authorization: "Bearer " + serviceAuth,
"content-type": "application/json"
},
body: JSON.stringify({ product })
}
);
if (!response.ok) throw new Error(await response.text());
return (await response.json()) as { token: string; url: string };
}Webhook receiver recipe
Fulfill from ATM events, not browser redirects. Store delivery ids before side effects so dashboard redrives and retries are harmless.
export async function POST(request: Request) {
const rawBody = await request.text();
const event = JSON.parse(rawBody) as AtmEvent;
verifyAtmWebhookSignature(
rawBody,
request.headers.get("atm-signature"),
request.headers.get("atm-delivery-id"),
process.env.ATM_WEBHOOK_SECRET!
);
const inserted = await insertDeliveryId(event.deliveryId);
if (!inserted) return Response.json({ ok: true, duplicate: true });
if (event.type === "payment.completed") {
await markOrderPaid(event.data.appOrderId, event.data.paymentId);
}
return Response.json({ ok: true });
}XRPC receiver recipe
AT Protocol-native apps can receive the same signed event envelope through an app-hosted XRPC method. Use this when your app already has an XRPC service surface; otherwise HTTP webhooks are simpler.
POST /xrpc/money.atmosphere.event.receive
Authorization: Bearer <ATM service-auth jwt>
Content-Type: application/json
{
"id": "evt_...",
"deliveryId": "del_...",
"environment": "test",
"type": "payment.completed",
"data": {
"paymentId": "pay_...",
"appOrderId": "ord_123"
}
}Ticket purchase recipe
Paid tickets add one required step before checkout: create a hold with ATM Tickets. The hold reserves scarce capacity and returns the ATM checkout URL for the buyer.
const availability = await tickets.getTicketAvailability({
environment: "test",
eventUri
});
if (!availability.items.find((item) => item.tierId === tierId)?.available) {
throw new Error("Sold out");
}
const hold = await tickets.createTicketHold({
environment: "test",
eventUri,
buyerDid,
buyerAssertionJwt,
items: [{ tierId, quantity: 2 }],
returnUrl: "https://events.example/orders/123",
cancelUrl: "https://events.example/e/abc"
});
return Response.redirect(hold.checkout.url);Free ticket recipe
Limited free tickets should still use ATM Tickets because capacity is scarce. Use app service-auth plus a buyer assertion and issue the ticket immediately without checkout.
const claim = await tickets.claimFreeTicket({
environment: "test",
eventUri,
tierId,
buyerDid,
buyerAssertionJwt,
idempotencyKey: "claim:" + eventUri + ":" + buyerDid + ":" + tierId
});
return Response.json({
ticketId: claim.ticket.id,
status: claim.ticket.status
});Webhook receiver
The example webhook verifies the raw body, deduplicates delivery id, and updates the app order only after ATM emits payment.completed.
Status polling
The browser return handler can poll status to resolve processing UI, but fulfillment should still come from the event stream.
Ticket hold
Ticketing examples use the same ATM App Node SDK because Tickets is an ATM module. The atmosphere.tickets site carries the ticket-specific concepts, diagrams, generated reference, and scanner flows.
Example links
The current examples are local because the public example repos are not published yet. Once launch repos are public, these docs should link directly to runnable source.
| ATM Node app | examples/atm-node-app - checkout, status, webhooks, ticket availability, holds, free claims, and check-in. |
|---|---|
| Tickets docs | https://atmosphere.tickets - ticket-specific integration recipes and XRPC reference. |
| App Node SDK | @atmosphere-money/app-node@beta - MIT-licensed server-side SDK for checkout, webhooks, service-auth, and Tickets calls. |
| Package browser | https://npmx.dev/package/@atmosphere-money/app-node - npmx package view with README, source, provenance, vulnerabilities, and package health signals. |