App Node SDK
The closed-beta TypeScript package for calling ATM from app backends.
Compatible with the closed-beta ATM app APIs and versioned ATM event headers. Check atm-api-version on every webhook or XRPC receiver event.
Package status
ATM's first SDK surface is the App Node package in packages/app-node. It is shaped as @atmosphere-money/app-node and is published on npm for invited beta app developers. The exported API is the intended app-server package surface.
SDK beta and API compatibility
The beta SDK version is 0.0.0-beta.0. It targets ATM API beta: 2026-06 and the current closed-beta app dashboard contracts. Webhook and XRPC receiver events include an atm-api-version header; receivers should log it and avoid accepting unknown future shapes without an explicit compatibility review.
| Package | @atmosphere-money/app-node |
|---|---|
| SDK version | 0.0.0-beta.0 |
| API compatibility | ATM API beta: 2026-06 |
| Event version signal | atm-api-version header on HTTP webhooks and XRPC receiver events. |
| Breaking changes | Documented in SDK reference, Versioning, and package CHANGELOG before broad app onboarding. |
Install
During beta, install the package directly from npm. Keep it on your backend and pin beta updates deliberately.
npm install @atmosphere-money/app-node@betaThe package requires Node 22 or newer and exports ESM plus TypeScript declarations from dist.
Initialize
The SDK asks your backend for a fresh service-auth JWT for every app-facing XRPC call. Scope each JWT to the exact method via lxm and to the ATM broker audience.
import { createAtmAppClient } from "@atmosphere-money/app-node";
const atm = createAtmAppClient({
getServiceAuthToken: async ({ lxm, aud }) => {
return mintMyAppServiceAuthJwt({
lxm,
aud,
exp: Math.floor(Date.now() / 1000) + 60,
jti: crypto.randomUUID()
});
}
});Start checkout
Check recipient payability before showing checkout buttons, then initiate the strict attested.network broker route with ATM's private checkout envelope.
const payout = await atm.getPayoutStatus("did:plc:creator");
if (!payout.payable) {
return { disabled: true, reason: payout.reason };
}
const checkout = await atm.initiatePayment({
recipient: "did:plc:creator",
amount: 1200,
currency: "usd",
paymentType: "shop",
environment: "test",
returnUrl: "https://app.example/orders/ord_123",
cancelUrl: "https://app.example/products/product_123",
metadata: {
appOrderId: "ord_123"
}
});
return checkout.url;Verify webhooks
Use the exact raw body and ATM headers. Deduplicate by delivery id before writing fulfillment side effects.
import {
constructAtmWebhookEvent,
constructTypedAtmWebhookEvent,
createHonoWebhookHandler,
createCloudflareWorkerWebhookHandler
} from "@atmosphere-money/app-node";
const event = constructAtmWebhookEvent({
rawBody,
secret: process.env.ATM_WEBHOOK_SECRET!,
headers: {
signature: request.headers.get("atm-signature"),
deliveryId: request.headers.get("atm-delivery-id"),
event: request.headers.get("atm-event"),
apiVersion: request.headers.get("atm-api-version"),
environment: request.headers.get("atm-environment")
}
});
if (await hasHandled(event.id)) {
return new Response("ok");
}
switch (event.type) {
case "payment.completed":
await fulfillPayment(event.data.payment);
break;
case "tickets.issued":
await showTicketsToBuyer(event.data.tickets);
break;
}
await markHandled(event.id);When you know the expected event type, the typed constructor gives you a narrower event.data shape.
const paymentEvent = constructTypedAtmWebhookEvent({
rawBody,
secret: process.env.ATM_WEBHOOK_SECRET!,
expectedType: "payment.completed",
headers: {
signature: request.headers.get("atm-signature"),
deliveryId: request.headers.get("atm-delivery-id"),
apiVersion: request.headers.get("atm-api-version"),
environment: request.headers.get("atm-environment")
}
});
await fulfillPayment(paymentEvent.data.payment);For route-level integration, use createNodeWebhookHandler, createNextWebhookRoute, createExpressWebhookHandler, createHonoWebhookHandler, or createCloudflareWorkerWebhookHandler. All of them verify the signed raw body and give your code a typed ATM event before fulfillment.
Tickets helpers
Tickets uses the same App Node package because ticket holds, free limited claims, QR/pass checks, and checkout all depend on ATM app auth and payment state.
const availability = await atm.getTicketAvailability({
environment: "test",
eventUri: "at://did:plc:organizer/community.lexicon.calendar.event/demo"
});
const hold = await atm.createTicketHold(createTicketHoldBody({
environment: "test",
eventUri: "at://did:plc:organizer/community.lexicon.calendar.event/demo",
buyerDid: "did:plc:buyer",
buyerAssertionJwt: "short-lived-buyer-assertion",
items: [{ ticketTierId: "tier_123", quantity: 2 }],
returnUrl: "https://app.example/tickets/return",
cancelUrl: "https://app.example/events/demo",
idempotencyKey: "hold:event:buyer:tier_123:2"
}));
const freeClaim = await atm.claimFreeTicket(createFreeTicketClaimBody({
environment: "test",
eventUri: "at://did:plc:organizer/community.lexicon.calendar.event/demo",
ticketTierId: "tier_free",
buyerDid: "did:plc:buyer",
buyerAssertionJwt: "short-lived-buyer-assertion",
idempotencyKey: "claim:event:buyer:tier_free"
}));
const verified = await atm.verifyTicket({
environment: "test",
ticketToken: "opaque_scan_token"
});Ticket-specific concepts, diagrams, and scanner flows live on atmosphere.tickets. ATM docs cover the shared payment, app auth, and event surfaces.
Framework examples
Copyable examples live in the repo so app developers can start from their own backend style without rewriting the verification logic.
| Next checkout route | docs/developer/examples/next-checkout-route.ts |
|---|---|
| Next webhook route | docs/developer/examples/next-webhook-route.ts |
| Next XRPC receiver route | docs/developer/examples/next-xrpc-receiver-route.ts |
| Express webhook route | docs/developer/examples/express-webhook-route.ts |
| Fastify webhook route | docs/developer/examples/fastify-webhook-route.ts |
| Hono webhook route | docs/developer/examples/hono-webhook-route.ts |
| Cloudflare Worker webhook | docs/developer/examples/cloudflare-worker-webhook.ts |
| Happy path app | docs/developer/examples/happy-path-app.ts |
| Ticket hold checkout | docs/developer/examples/ticket-hold-checkout.ts |
| Free limited ticket claim | docs/developer/examples/free-ticket-claim.ts |
Errors and retries
Failed requests throw AtmApiError with a stable code, HTTP status, and parsed response body. Retry transport failures and 5xx responses with the same app idempotency key. Do not retry a payment by asking the buyer to pay again unless ATM says the checkout is failed or expired.
import { AtmApiError } from "@atmosphere-money/app-node";
try {
return await atm.initiatePayment(input);
} catch (error) {
if (error instanceof AtmApiError) {
if (error.status >= 500) retryWithSameIdempotencyKey();
if (error.code === "RecipientNotPayable") showSetupRequiredState();
}
throw error;
}API surface
| createAtmAppClient | Create a backend client for payout status, checkout, payment status, profile, and ticket calls. |
|---|---|
| ATM_XRPC_METHODS | Stable method constants used by the SDK so app code can avoid scattered NSID strings. |
| createAtmCheckoutProduct | Build ATM's private `atm.checkout.v1:` product envelope for strict attested.network initiate calls. |
| constructAtmWebhookEvent | Verify HTTP webhook signature and parse the event envelope. |
| constructTypedAtmWebhookEvent | Verify a webhook and narrow data for a known event type. |
| createNodeWebhookHandler | Create a Request/Response webhook handler for Node-compatible runtimes. |
| createNextWebhookRoute | Alias for Next route handlers and other Web Request-compatible runtimes. |
| createExpressWebhookHandler | Create an Express-style webhook handler without adding an Express dependency. |
| createHonoWebhookHandler | Create a Hono route handler around the same signed webhook verification. |
| createCloudflareWorkerWebhookHandler | Create a Workers fetch handler around the same signed webhook verification. |
| createTicketHoldBody | Validate and prune paid ticket hold request bodies before calling the Tickets XRPC method. |
| createFreeTicketClaimBody | Validate and prune free limited-ticket claim request bodies before calling the Tickets XRPC method. |
| constructAtmXrpcReceiverEvent | Verify optional XRPC receiver service-auth and parse the same event envelope. |
| constructTypedAtmXrpcReceiverEvent | Verify an XRPC receiver event and narrow data for a known event type. |
| verifyServiceAuthRequest | Verify a service-auth bearer request before custom XRPC receiver routing. |
| signAtmWebhookPayload | Test helper for local webhook signature fixtures. |
| AtmApiError | Structured request failure with code, status, and parsed body. |
Payments
getPayoutStatus, initiatePayment, getPaymentStatus.
Profiles
getProfile reads ATM profile data from the AppView.
Tickets
create/update/archive tiers, capacity groups, availability, holds, release, claims, lists, verify, and check-in.
Events
HTTP webhook and optional XRPC receiver constructors use the same ATM envelope shape.
Go live with the SDK
- Create the app DID and register the app role in ATM.
- Keep service-auth minting and webhook secrets on the app backend.
- Configure separate test and live webhook URLs or XRPC receivers.
- Use an app idempotency key for each app order, ticket hold, or free claim.
- Verify webhook signatures or XRPC receiver service-auth before fulfillment.
- Store delivery ids and payment ids before sending tickets, downloads, emails, or app mutations.
- Test payability-disabled states, refunds, subscription changes, ticket release, and redrive.
- Switch the app dashboard from test to live intentionally; do not reuse test secrets in live.
Browser and Supper packages
Browser embeds should be separate packages. ATM checkout embeds can become an @atmosphere-money/checkout-embed wrapper around an ATM-hosted iframe, with hosted checkout as fallback.
Creators who only want a button on their own website should use Supper's add-to-your-site guide. Supper remains the app in that flow; creators do not need app service-auth, webhooks, SDKs, or app onboarding.
Ergonomics v0.1
Keep the beta package small until the first external app developers have used it. The SDK should remove the sharp edges testers hit in real integrations, not become a large platform wrapper before those needs are proven.
- Add helpers only when a tester or reference app needs them in real integration work.
- Keep low-level primitives available beside route helpers.
- Do not put browser checkout, Supper widgets, or Stripe client secrets in app-node.
- Prefer framework examples over framework dependencies inside the core package.
- Graduate repeated docs snippets into SDK helpers only after they appear in more than one real app.
- Treat webhook/XRPC verification, idempotency, checkout initiation, status checks, and Tickets calls as the core v0.1 surface.
Publish readiness
- Keep the @atmosphere-money npm org restricted to release maintainers.
- Keep package exports server-only and free of browser checkout secrets.
- Run npm run check and npm run release:check in packages/app-node before tagging a version.
- Run npm run check in packages/testing before publishing fixture helpers.
- Run npm run publish:check before publishing each beta; the package license is MIT.
- Run npm pack --dry-run and inspect the tarball contents.
- Publish generated TypeScript declarations with every release.
- Keep starter apps importing packed SDK tarballs in CI rather than relying on monorepo source imports.
- Document breaking changes in SDK docs before broad app onboarding.