Docs

Hono recipe

Implement ATM checkout and signed webhook verification in a Hono app.

Closed beta@atmosphere-money/app-nodeSDK beta: 0.0.0-beta.3ATM API beta: 2026-0671 published lexicons

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

Use Hono when your AT Protocol app is a small service with Web Request routes. The same app export can run in Node-compatible Hono deployments and inform Workers-style code.

sh
npm install @atmosphere-money/app-node@beta hono

Create 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. Persist ATM's app status token beside the order before returning only the checkout URL to the browser. The URL contains a separate browser bearer.

ts
import { Hono } from "hono";
import { createAtmAppClient } from "@atmosphere-money/app-node";

type Bindings = { ATM_WEBHOOK_SECRET: string };
const app = new Hono<{ Bindings: Bindings }>();
const atm = createAtmAppClient({
  getServiceAuthToken: ({ lxm, aud }) => mintAppServiceAuthJwt({ lxm, aud })
});

app.post("/checkout", async (context) => {
  const { recipientDid, amountCents } = await context.req.json();
  const payout = await atm.getPayoutStatus(recipientDid);
  if (!payout.payable) {
    return context.json({ error: "RecipientNotPayable" }, 409);
  }

  const approval = await atm.requestRecipientApproval({
    recipientDid,
    environment: "test",
    paymentTypes: ["shop"],
    feeShareBps: 300,
    requestReason: "Enable Hono checkout"
  });
  if (approval.status !== "approved") {
    return context.json({
      error: "RecipientAppApprovalRequired",
      approvalUrl: approval.dashboardUrl
    }, 409);
  }

  const order = await createAppOrder({ 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 }
  });

  await saveAtmStatusToken(order.id, checkout.token);
  return context.json({ url: checkout.url });
});

Add event receiver

New AT Protocol-native apps use the canonical#AtmEventReceiver / money.atmosphere.event.receiveEvent wake-up transport. A conventional web app can explicitly select the signed HTTP webhook compatibility path shown by this framework recipe. Verify the configured receiver before using it to wake an immediate canonical status/query read.

ts
import { createHonoWebhookHandler } from "@atmosphere-money/app-node";

app.post("/webhooks/atm", async (context) => {
  const handler = createHonoWebhookHandler({
    secret: context.env.ATM_WEBHOOK_SECRET,
    expectedType: "payment.completed",
    deliveryStore: {
      claim: claimWebhookDelivery,
      complete: completeWebhookDelivery,
      release: releaseWebhookDelivery
    },
    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(appOrderId, event.data.payment.id);
      return { body: { ok: true } };
    }
  });
  return handler(context);
});

Fulfill payment or ticket

The fulfillment step is the same in Hono: poll with the stored status handle, map the completed ATM payment back to your app order, and write the app-side fulfillment state once. Deduplicate each receiver delivery id before waking the same poller.

  1. 01

    Deduplicate

    Apply the same completed status once; optionally claim a push delivery id before waking reconciliation.

  2. 02

    Match order

    Load the app order that stores the initiation status token and private correlation data.

  3. 03

    Fulfill

    Grant access, issue app content, reveal tickets, update a subscription, or notify the buyer.

  4. 04

    Reconcile

    Store canonical ATM state and any optional event id beside the app order for refunds and disputes.

Run local test fixture

Use the runnable starter when one exists. Your core test should prove status-token persistence, authenticated polling, duplicate terminal handling, and the app fulfillment mutation. If you enable push, also generate a signed fixture with@atmosphere-money/testing and prove raw-body verification plus duplicate delivery handling.

sh
cd examples/atm-hono-worker-starter
npm install
npm run typecheck
npm run smoke

Runtime notes

Starterexamples/atm-hono-worker-starter is the lightweight starter checked in CI.
Web RequestThe helper reads context.req.raw, so signed verification sees the original Request body.
WorkersFor Cloudflare Workers, use the Workers page when you need platform-specific env and storage guidance.