Docs

Service-auth cookbook

Concrete service-auth recipes for app calls, buyer assertions, XRPC receivers, ticket holds, and replay protection.

Closed beta@atmosphere-money/app-nodeSDK beta: 0.0.0-beta.0ATM API beta: 2026-0642 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.

Which token to use

ATM uses short-lived service-auth in two directions. The app proves its own DID when it calls ATM. The app may also include a user assertion when it is acting for a buyer or organizer who is already signed into the app.

App calls ATMUse app service-auth. iss is the app DID and lxm is the exact ATM method.
Buyer present in appInclude buyerAssertionJwt beside buyerDid inside private checkout or ticket requests.
Organizer configures ticketsInclude organizerAssertionJwt when the app creates or changes ticket tiers for an organizer.
ATM calls app XRPC receiverATM sends service-auth to the app; the app verifies ATM as caller.
HTTP webhookUse raw-body HMAC signature verification, not service-auth.

App service-auth

App service-auth protects app fee attribution, environment access, webhook routing, module permissions, and app-visible payment rows. Do not trust an app DID supplied only as a URL parameter.

ts
const token = await getServiceAuth({
  iss: appDid,
  aud: "did:plc:atm-broker#AttestedNetwork",
  lxm: "network.attested.payment.initiate",
  exp: Math.floor(Date.now() / 1000) + 60,
  jti: crypto.randomUUID()
});
FieldTypeRequiredDescription
issdidyesRegistered app DID.
auddid#serviceyesATM broker or method-specific service audience.
lxmnsidyesExact XRPC method being called.
expintegeryesShort expiry, usually about one minute.
jtistringyesUnique token id used for replay protection.

Buyer assertion

A buyer assertion is a low-friction proof that the app saw the signed-in buyer for this action. It is not an OAuth grant and does not let ATM write to the buyer's PDS.

ts
const buyerAssertionJwt = await getServiceAuth({
  iss: buyerDid,
  aud: "did:plc:atm-broker#AttestedNetwork",
  lxm: "money.atmosphere.payment.assertPayer",
  exp: Math.floor(Date.now() / 1000) + 60,
  jti: crypto.randomUUID()
});

const checkoutEnvelope = {
  payerDid: buyerDid,
  buyerAssertionJwt,
  returnUrl,
  cancelUrl,
  metadata: { appOrderId }
};

Organizer assertion

Ticketing apps may need to prove that an organizer authorized ticket tier or capacity changes inside the app. Use the same pattern, but scope the assertion to the ticket method being called.

ts
const organizerAssertionJwt = await getServiceAuth({
  iss: organizerDid,
  aud: "did:plc:atm-broker#Tickets",
  lxm: "tickets.atmosphere.createTicketTier",
  exp: Math.floor(Date.now() / 1000) + 60,
  jti: crypto.randomUUID()
});

ATM receiver callback

XRPC receiver callbacks are optional. They are useful for apps that already expose AT Protocol service surfaces and want ATM delivery to match the rest of their app architecture.

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

export async function receiveAtmEvent(request: Request) {
  const expectedAudience = createAtmXrpcReceiverAudience(appDid);

  await verifyServiceAuth({
    header: request.headers.get("authorization"),
    iss: "did:plc:atm-broker",
    aud: expectedAudience,
    lxm: "money.atmosphere.event.receive"
  });

  const event = await request.json();
  await fulfillIdempotently(event.deliveryId, event);
  return Response.json({ ok: true });
}

Replay protection

  • Use short expiries for every service-auth JWT.
  • Store jti values for the replay window before performing side effects.
  • Reject a token if iss, aud, lxm, exp, kid, signature, or jti is wrong.
  • Use one token for one request; do not reuse buyer assertions across checkout attempts.
  • Deduplicate app events by deliveryId and business object id.

Common failures

CodeHTTPRetryCauseDeveloper action
AppUnauthorized401noThe app DID is not registered, disabled, or not enabled for the requested module.Check app registration and environment module settings.
InvalidAudience401noThe JWT aud does not match ATM's expected service audience.Mint a fresh token with the exact documented audience.
InvalidMethod401noThe JWT lxm does not match the XRPC method being called.Use the exact method NSID in lxm.
TokenReplay409noThe jti has already been seen.Mint a fresh token per request and check retry logic.
BuyerAssertionInvalid401noThe buyer assertion issuer, audience, method, expiry, or signature failed.Mint a fresh buyer assertion from the signed-in buyer context.
Service-auth cookbook - Atmosphere Money Docs