First real app tutorial
The canonical app integration route: register, configure test mode, request creator approval, create checkout, receive payment.completed, and fulfill.
Compatible with the closed-beta ATM app APIs and versioned ATM event headers. Check atm-api-version on every webhook or XRPC receiver event.
Goal
This is the canonical ATM app integration path. At the end, the app has a test environment, a receiver, a successful checkout path, and a redrive-safe event handler that fulfills only from ATM truth.
- 01
Register app
Add the app role to the app DID and keep configuration in test mode.
- 02
Configure test
Enable modules, add a receiver, copy the test secret, and subscribe to events.
- 03
Request approval
Ask the creator to approve this app, environment, fee cap, payment types, and public-record behavior.
- 04
Create checkout
Create an app order, build the private ATM checkout envelope, and redirect the buyer.
- 05
Receive event
Verify payment.completed, deduplicate the delivery id, and store the ATM payment id.
- 06
Fulfill
Mark the app order paid and move toward live only after redrive and refund paths pass.
App server
Runs the starter kit or your own equivalent server-side integration.
ATM dashboard
Stores app config, modules, webhooks, delivery logs, and test/live settings.
ATM checkout
Owns the buyer payment form, wallets, receipts, status, and proof coordination.
App fulfillment
Runs only after a verified ATM event or confirmed ATM status.
Prerequisites
- An allowlisted app DID with access to app registration.
- A server runtime that can keep app secrets private.
- A tunnel or deployed test URL for HTTP webhooks.
- A test recipient that has completed payment setup when testing paid checkout.
- A plan for idempotency keys and delivery id deduplication.
1. Register the app
- 01
Sign in
Use the DID that represents the app, not an individual developer's personal account.
- 02
Request app role
ATM adds the app role while preserving payer or creator roles on the same DID.
- 03
Complete profile
Name, avatar, description, and app URL become the app-facing identity in ATM.
- 04
Stay in test
The app dashboard defaults to test configuration for modules, secrets, and receivers.
2. Configure test mode
| Modules | Enable only what the app is testing: payments, products, subscriptions, tickets, or callbacks. |
|---|---|
| Webhook URL | Start with HTTP webhooks unless your app already hosts XRPC methods. |
| Signing secret | Copy the test secret once and store it in server-side env. |
| Event selection | Subscribe to only the events your app knows how to handle. |
| App fee | Set the app fee policy intentionally before any checkout test. |
3. Run the starter kit
The starter kit proves local receiver verification and status handling before you wire application-specific order logic.
cd examples/atm-node-app
cp .env.example .env
npm run build --prefix ../../packages/app-node
npm install
npm run typecheck
npm run smoke
npm run dev
curl http://localhost:8787/healthThe compact source file at docs/developer/examples/happy-path-app.tsshows the same app-order, checkout, webhook, and fulfillment sequence without the full starter-kit HTTP server.
4. Request creator approval
Before an app can accept payments for a creator, the creator approves that app in ATM. Approval is scoped to app DID, recipient DID, environment, payment types, app fee cap, and public-record behavior.
const approval = await atm.requestRecipientApproval({
recipientDid,
environment: "test",
paymentTypes: ["shop"],
feeShareBps: 300,
publicRecords: {
defaults: {
appRecord: "private",
attestation: "private"
}
},
requestReason: "Enable Example App checkout",
setupReturnUrl: "https://app.example/settings/payments"
});
if (approval.status !== "approved") {
return Response.redirect(approval.dashboardUrl);
}Fee decreases stay within scope. New payment types, fee increases, or changes that make records more public move the approval to needs-review until the creator confirms.
5. Create the first checkout
For a real app, create an app order first, then create the ATM checkout envelope from that order. The browser should receive only the returned ATM checkout URL.
const order = await createAppOrder({
recipientDid,
amountCents: 1200,
currency: "usd"
});
const checkout = await atm.initiatePayment({
environment: "test",
recipient: order.recipientDid,
amount: order.amountCents,
currency: order.currency,
paymentType: "shop",
metadata: { appOrderId: order.id },
returnUrl: "https://app.example/orders/" + order.id,
cancelUrl: "https://app.example/support"
});
return Response.redirect(checkout.url);6. Verify event delivery
Send a dashboard test event first. Then complete a real test checkout and verify that both paths are deduplicated by delivery id.
- The receiver validates `Atm-Signature` over the raw body and `Atm-Delivery-Id`.
- The receiver stores the delivery id before side effects.
- Redriving the same delivery does not fulfill twice.
- The app handles unknown future event fields as additive.
7. Inspect dashboard state
| App dashboard | Check module status, test/live environment, and fee settings. |
|---|---|
| Webhooks | Confirm delivery status, attempts, response code, and redrive action. |
| Payments | Confirm the payment row, app id, app fee fields, customer fields, and proof status. |
| Products or tickets | Confirm linked fulfillment refs or issued ticket state if those modules are enabled. |
8. Ready for live
- Guest and signed-in payer checkout paths pass.
- Webhook and optional XRPC receiver paths pass.
- Failed delivery and redrive pass.
- Refund/cancel/subscription-change paths pass for enabled modules.
- Private data stays off protocol and out of public logs.
- Live secrets are separate from test secrets.