Reference

Endpoints

Better Auth endpoint contracts for Nimiq auth, cross-device approval, and ledger.

Use this page to wire clients, tests, route allowlists, and server mocks.

All prefixes are configurable. The examples use defaults.

Nimiq auth

Default prefix: /nimiq.

EndpointMethodBody or queryResult
/nimiq/noncePOSTEmpty bodyChallenge payload with nonceId, nonce, message, origin, issuedAt, expiresAt
/nimiq/verifyPOSTnonceId, publicKeyHex, signatureHex, optional address{ ok: true, token } plus Better Auth session cookie
app/direct-sign-in.ts
const nonce = await authClient.$fetch('/nimiq/nonce', {
  method: 'POST',
  body: {},
})

const signature = await window.nimiq!.sign(nonce.message)

const verified = await authClient.$fetch('/nimiq/verify', {
  method: 'POST',
  body: {
    nonceId: nonce.nonceId,
    publicKeyHex: signature.publicKey,
    signatureHex: signature.signature,
    address: 'NQ0700000000000000000000000000000000',
  },
})

console.log(verified)

Expected result shape:

response.json
{
  "ok": true,
  "token": "better-auth-session-token"
}

Use these endpoints when the signing wallet is available in the same browser. Do not call /verify with a reused nonce; nonce storage is TTL-bound and one-time by design.

Cross-device approval

Default prefix: /cross-device.

EndpointMethodBody or queryResult
/cross-device/startPOSTkind, optional adapterId, returnTo, display fields, payloadHashorderId, claimToken, claimUrl, desktopToken, expiry
/cross-device/claimPOSTorderId, claimTokenchallengeToken
/cross-device/challengeGETorderId, challengeTokenChallenge envelope and message
/cross-device/approvePOSTorderId, challengeToken, proofApproved status
/cross-device/rejectPOSTorderId, challengeTokenRejected status
/cross-device/cancelPOSTorderId, desktopTokenCancelled status
/cross-device/finalizePOSTorderId, desktopTokenSession token for login, proof artifact for sign or transaction
/cross-device/eventsGETorderId, desktopTokenSSE status events
app/desktop.ts
import {
  finalizeCrossDeviceOrder,
  startCrossDeviceOrder,
  subscribeToCrossDeviceOrder,
} from '@onmax/better-auth-cross-device/client'

const order = await startCrossDeviceOrder(authClient.$fetch.bind(authClient), {
  endpointPrefix: '/cross-device',
  kind: 'login',
  displayTitle: 'Sign in to Arcade Rewards',
  displaySummary: 'Approve this login in Nimiq Pay.',
})

const events = subscribeToCrossDeviceOrder({
  endpointPrefix: '/cross-device',
  orderId: order.orderId,
  desktopToken: order.desktopToken,
  onEvent: async (event) => {
    if (event === 'approved') {
      const result = await finalizeCrossDeviceOrder(authClient.$fetch.bind(authClient), {
        endpointPrefix: '/cross-device',
        orderId: order.orderId,
        desktopToken: order.desktopToken,
      })

      console.log(result)
      events.close()
    }
  },
})
app/phone.ts
import { createNimiqMiniAppApprover, parseCrossDeviceClaimUrl } from '@onmax/cross-device-nimiq'

const approver = createNimiqMiniAppApprover()
const claim = parseCrossDeviceClaimUrl(window.location.href)

await approver.approve(authClient.$fetch.bind(authClient), {
  ...claim,
  endpointPrefix: '/cross-device',
})

Expected status order for login: claimed, waiting_user, approved, then finalized. Rejection, cancellation, and expiry stop the flow before finalization.

Use /events from the desktop browser. Do not subscribe from the phone approval screen because it does not own the desktopToken.

Ledger

Default prefix: /ledger.

EndpointMethodBody or queryResult
/ledger/accountGETSession cookieLedger account
/ledger/balanceGETOptional asset queryBalance view list
/ledger/entriesGETOptional asset, limit, cursor queryLedger entry list
/ledger/creditPOSTMutation bodyMutation result
/ledger/debitPOSTMutation bodyMutation result
/ledger/adjustPOSTMutation bodyMutation result
/ledger/reservePOSTMutation bodyReservation result
/ledger/reservations/releasePOSTReservation bodyRelease result
/ledger/reservations/capturePOSTReservation bodyCapture result

Ledger mutation endpoints are disabled unless enableMutations and authorizeMutation are configured.

app/ledger.ts
import { getLedgerBalance, listLedgerEntries } from '@onmax/better-auth-ledger/client'

const balance = await getLedgerBalance(authClient.$fetch.bind(authClient), {
  endpointPrefix: '/ledger',
  asset: 'coin',
})

const entries = await listLedgerEntries(authClient.$fetch.bind(authClient), {
  endpointPrefix: '/ledger',
  asset: 'coin',
  limit: 20,
})

console.log({ balance, entries })
server/admin-credit.ts
await auth.api.$fetch('/ledger/credit', {
  method: 'POST',
  body: {
    userId: 'user_123',
    asset: 'coin',
    amount: 100,
    reason: 'daily_bonus',
    idempotencyKey: 'daily_bonus:user_123:2026-05-06',
  },
})

Expected behavior: read endpoints require a session. Mutation endpoints additionally require plugin-level authorization.

Copyright © 2026