Endpoints
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.
| Endpoint | Method | Body or query | Result |
|---|---|---|---|
/nimiq/nonce | POST | Empty body | Challenge payload with nonceId, nonce, message, origin, issuedAt, expiresAt |
/nimiq/verify | POST | nonceId, publicKeyHex, signatureHex, optional address | { ok: true, token } plus Better Auth session cookie |
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:
{
"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.
| Endpoint | Method | Body or query | Result |
|---|---|---|---|
/cross-device/start | POST | kind, optional adapterId, returnTo, display fields, payloadHash | orderId, claimToken, claimUrl, desktopToken, expiry |
/cross-device/claim | POST | orderId, claimToken | challengeToken |
/cross-device/challenge | GET | orderId, challengeToken | Challenge envelope and message |
/cross-device/approve | POST | orderId, challengeToken, proof | Approved status |
/cross-device/reject | POST | orderId, challengeToken | Rejected status |
/cross-device/cancel | POST | orderId, desktopToken | Cancelled status |
/cross-device/finalize | POST | orderId, desktopToken | Session token for login, proof artifact for sign or transaction |
/cross-device/events | GET | orderId, desktopToken | SSE status events |
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()
}
},
})
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.
| Endpoint | Method | Body or query | Result |
|---|---|---|---|
/ledger/account | GET | Session cookie | Ledger account |
/ledger/balance | GET | Optional asset query | Balance view list |
/ledger/entries | GET | Optional asset, limit, cursor query | Ledger entry list |
/ledger/credit | POST | Mutation body | Mutation result |
/ledger/debit | POST | Mutation body | Mutation result |
/ledger/adjust | POST | Mutation body | Mutation result |
/ledger/reserve | POST | Mutation body | Reservation result |
/ledger/reservations/release | POST | Reservation body | Release result |
/ledger/reservations/capture | POST | Reservation body | Capture result |
Ledger mutation endpoints are disabled unless enableMutations and authorizeMutation are configured.
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 })
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.