Cloudflare Workers recipe
Implement ATM event verification and checkout handoff from a Workers-style backend.
Compatible with the closed-beta ATM app APIs and versioned ATM event headers. Check atm-api-version on every webhook or XRPC receiver event.
Install SDK
Workers apps should keep webhook secrets in environment bindings and store idempotency in Durable Objects, D1, KV, or an external database.
npm install @atmosphere-money/app-node@betaCreate checkout route
Create an app order first, check the recipient's payout status, confirm creator app approval, then ask ATM to create the hosted checkout. The route returns only the checkout URL and token to the browser.
import { createAtmAppClient } from "@atmosphere-money/app-node";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const atm = createAtmAppClient({
getServiceAuthToken: ({ lxm, aud }) => mintAppServiceAuthJwt(env, { lxm, aud })
});
if (request.method === "POST" && new URL(request.url).pathname === "/checkout") {
const { recipientDid, amountCents } = await request.json();
const payout = await atm.getPayoutStatus(recipientDid);
if (!payout.payable) return Response.json({ error: "RecipientNotPayable" }, { status: 409 });
const approval = await atm.requestRecipientApproval({
recipientDid,
environment: "test",
paymentTypes: ["shop"],
feeShareBps: 300,
requestReason: "Enable Workers checkout"
});
if (approval.status !== "approved") {
return Response.json({
error: "RecipientAppApprovalRequired",
approvalUrl: approval.dashboardUrl
}, { status: 409 });
}
const order = await createAppOrder(env, { recipientDid, amountCents });
const checkout = await atm.initiatePayment({
environment: "test",
recipient: order.recipientDid,
amount: order.amountCents,
currency: "usd",
paymentType: "shop",
returnUrl: `https://app.example/orders/${order.id}/return`,
cancelUrl: `https://app.example/orders/${order.id}`,
metadata: { appOrderId: order.id }
});
return Response.json({ url: checkout.url, token: checkout.token });
}
return new Response("Not found", { status: 404 });
}
};Verify webhook or XRPC receiver
Fulfillment should come from verified ATM events. Signed HTTP webhooks are the default; XRPC receiver callbacks are optional for apps that already host an AT Protocol service surface.
import { createCloudflareWorkerWebhookHandler } from "@atmosphere-money/app-node";
export async function handleWebhook(request: Request, env: Env) {
const handler = createCloudflareWorkerWebhookHandler({
secret: env.ATM_WEBHOOK_SECRET,
expectedType: "payment.completed",
insertDeliveryIdOnce: async (deliveryId) => {
return insertDeliveryIdOnce(env, deliveryId);
},
onEvent: async (event) => {
const metadata = event.data.payment.metadata as
| { appOrderId?: string }
| undefined;
const appOrderId = String(metadata?.appOrderId ?? "");
if (!appOrderId) return { status: 422, body: { error: "MissingAppOrderId" } };
await fulfillOrder(env, appOrderId, event.data.payment.id);
return { body: { ok: true } };
}
});
return handler.fetch(request);
}Fulfill payment or ticket
The fulfillment step is the same in Cloudflare Workers: deduplicate the ATM delivery id, map the ATM payment or ticket event back to your app order, write the app-side fulfillment state once, and store the ATM id for support and reconciliation.
- 01
Deduplicate
Insert the ATM delivery id with a unique constraint before side effects.
- 02
Match order
Read appOrderId, ticket hold id, listing ref, or another private app correlation id from event metadata.
- 03
Fulfill
Grant access, issue app content, reveal tickets, update a subscription, or notify the buyer.
- 04
Reconcile
Store the ATM payment id and event id beside the app order for refunds, disputes, and redrive.
Run local test fixture
Use the runnable starter when one exists, or generate a signed webhook fixture with @atmosphere-money/testing. Your test should prove raw-body verification, duplicate delivery handling, and the app fulfillment mutation.
cd examples/atm-hono-worker-starter
npm install
npm run smoke
# Then add a Workers-specific integration test around your chosen storage layerRuntime notes
| Storage | Use Durable Objects, D1, KV with compare-and-set, or Postgres for delivery idempotency. Do not use in-memory Sets. |
|---|---|
| Secrets | Use Workers environment bindings for webhook secrets and app signing material. |
| Starter | The Hono / Workers starter demonstrates the same Web Request shape and signed event fixture. |