DocumentationDashboard API
Ask AI

Take the docs with you

Ask ChatGPTOpen a new chat ↗Ask ClaudeOpen a new chat ↗Ask on GeminiCopy prompt, then paste in Gemini ↗View llms.txtDocumentation index ↗

External chats receive links to public docs.

Management API

Behavior differences. Only endpoints that intentionally differ from the standard API carry a chip; the chip links to the note that explains the difference.

Automate the dashboard without guessing. The reference below describes the dashboard endpoints available on this host. Dashboard calls take the session cookie (log in once, reuse the jar); inference keys never work here. Public rows need no credential.

Combo management API limits. Combo management supports create, list, and delete. Other methods return 405. Listing storage failures return 503. Call private combo aliases through Chat Completions or Messages using an owner API key. See Combos for routing and permissions.
/api/dashboard/register/api/dashboard/login/api/dashboard/keys/api/dashboard/keys/{id}/api/dashboard/usage/api/dashboard/usage-stats
/api/dashboard/catalog/api/dashboard/balance/api/dashboard/deposit/info/api/dashboard/deposit/epusdt/create/api/dashboard/topup/qris/api/public/stats

Success answers a flat object or a named collection wrapper; failure answers {"error": "..."} with a plain string — except POST /api/dashboard/register, which nests {"error": {"code": "...", "message": "..."}} so signup clients can branch on codes. Authenticated endpoints take the session cookie from login (the login token also works as a Bearer header); an ar- inference key never works here and answers 401. Examples use illustrative values.

GET /api/dashboard/budgets

List exact account and API key model budgets, percentage rules, and the current final per-request minimum in USD. Requires your dashboard session. See Budgets for target selection and how limits intersect.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/budgets
json
{
  "budgets": [{"model":"cbcn/glm-5.3","max_input_price":1,"max_output_price":3,"queue_wait_seconds":30}],
  "rules": [{"scope_type":"all","scope_value":"","discount_percent":25,"queue_wait_seconds":30}],
  "minimum_charge":0.000001
}

The amount above is illustrative; always use the returned minimum. An omitted api_key_id means the account default for that model. An entry with a key id applies only to that owned key, together with the account budget.

PUT /api/dashboard/budgets

Create or replace one budget using the same fields as a listed entry. This operation requires your dashboard session.

FieldContract
modelRequired model id.
api_key_idOptional id of an API key owned by your account. Omit for the account default.
max_input_price / max_output_priceRequired finite, nonnegative final retail rates in USD per million tokens. Zero disables paid requests.
queue_wait_secondsOptional; only 30 is accepted and omission defaults to 30. Up to 30 seconds of routing wait; generation time is additional.
bash
curl -b jar.txt -X PUT https://atmorouter.dev/api/dashboard/budgets \
  -H "Content-Type: application/json" \
  -d '{"model":"cbcn/glm-5.3","max_input_price":1,"max_output_price":3,"queue_wait_seconds":30}'

Account and API key budgets intersect: the lower input cap and lower output cap apply independently. A request price header may tighten a saved cap, but cannot raise it.

DELETE /api/dashboard/budgets

Delete one budget with a JSON body containing model and optionally api_key_id. Omit the key id to delete the account default. Other applicable caps still apply; removing a saved cap returns to the catalog-bounded default where no other cap exists. Requires your dashboard session.

bash
curl -b jar.txt -X DELETE https://atmorouter.dev/api/dashboard/budgets \
  -H "Content-Type: application/json" \
  -d '{"model":"cbcn/glm-5.3"}'

PUT /api/dashboard/budget-rules

Save a percentage-off official rate rule with scope_type (all, family, or model), scope_value (empty string, the model-ID prefix before /, or an exact model id), finite discount_percent from 0 to 100, and optionally an owned api_key_id. Queue wait is fixed at 30 seconds. Requires a dashboard session. See Budgets API examples.

DELETE /api/dashboard/budget-rules

Remove one percentage rule by its scope_type, scope_value, and optional api_key_id. Requires a dashboard session. Other rules and exact rate caps continue to apply.

POST /api/dashboard/register

Start registration with an email address. A verification link is sent to the email to set password. Public endpoint, no authentication required.

ParameterTypeDescription
emailstringRequired user email address.
refstringOptional referral code (unknown codes are ignored at completion).
sourcestringOptional first-touch attribution from the landing URL.
utm_sourcestringOptional campaign attribution, stored as provided.
utm_campaignstringOptional campaign attribution, stored as provided.
landing_pathstringOptional landing path attribution, stored as provided.
bash
curl https://atmorouter.dev/api/dashboard/register \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'
json
// HTTP 202 — check the inbox, then complete registration
{
  "status": "verification_requested",
  "retry_after_seconds": 60
}
StatusError codeWhat it means
202verification_requestedVerification email queued. Complete with POST /api/dashboard/complete-registration using the emailed token plus a new password, then sign in.
400invalid_emailA valid email address is required.
400invalid_requestMalformed JSON, an unknown field, or a bare null body — the parser is strict.
403origin_rejectedBrowser Origin header does not match the app origin. Plain curl without Origin is unaffected.
405method_not_allowedPOST only.
413request_too_largeBody over 4 KB.
415unsupported_media_typeContent-Type must be application/json.
429rate_limitedSignup quota spent. Retry-After header plus retry_after_seconds say when to retry.
503signup_unavailable / service_unavailableRegistration disabled or the mail path is down.

POST /api/dashboard/login

Authenticate with email and password to receive a session token and Set-Cookie: atmo_session cookie (HttpOnly, 30-day expiry). Public endpoint, no authentication required. Flood guard: 20 per IP plus 8 per account per 15 minutes.

ParameterTypeDescription
emailstringRegistered account email (verified accounts only).
passwordstringAccount password.
bash
curl -c jar.txt https://atmorouter.dev/api/dashboard/login -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"..."}'
json
// HTTP 200 — jar.txt now holds the atmo_session cookie (HttpOnly, 30-day expiry)
{
  "user_id": "00000000-0000-4000-8000-000000000001",
  "email": "[email protected]",
  "token": "<64-hex session token — shown once>",
  "message": "login successful"
}
StatusErrorWhat it means
200login successfulSession created. Reuse the jar (-b jar.txt) for every authenticated row below; the token field also works as an Authorization: Bearer header.
400invalid JSONMalformed request body.
401invalid credentialsWrong password, unknown email, or unverified account — the response is identical either way so accounts cannot be enumerated.
405method not allowedPOST only.
429too many sign-in attempts …Flood guard spent (20 per IP + 8 per account per 15 minutes). Retry-After says when to retry.
500could not start sessionSession store failure; retry or report.

GET / POST /api/dashboard/keys

List existing keys or create a new API key. Requires active session cookie. Full lifecycle and properties are documented in API keys.

Parameter (POST)TypeDescription
namestringOptional key label (defaults to empty).
rate_limitintegerOptional per-minute request limit (omit for 500; explicit 1..1000, anything else 400).
allowed_modelsstring[]Optional model allowlist (defaults to empty = all models).
bash
# Create (POST 201) — full secret once; list (GET 200) — previews only
curl -b jar.txt https://atmorouter.dev/api/dashboard/keys -H "Content-Type: application/json" -d '{"name":"prod-app","rate_limit":500}'
curl -b jar.txt https://atmorouter.dev/api/dashboard/keys
json
// POST 201 — save "key" now, it is shown in full exactly once
{ "id": "00000000-0000-4000-8000-000000000000", "key": "ar-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "message": "key created" }

// GET 200 — per-key: 12-char preview, rate_limit, allowed_models, requests, last_used
{ "data": [{ "id": "00000000-0000-4000-8000-000000000000", "key": "ar-xxxxxxxxx…", "name": "prod-app",
  "rate_limit": 500, "allowed_models": [], "enabled": true, "requests": 0,
  "last_used": "0001-01-01T00:00:00Z", "created_at": "..." }]}
StatusErrorWhat it means
201 / 200key created / data listPOST mints ar- + 48 hex chars from 24 secure random bytes; GET lists newest-first scoped to your account.
400invalid JSONMalformed POST body.
400rate_limit must be 1..1000Omit the field for the 500 default; any explicit value outside 1..1000 (including 0 and negatives) is rejected.
401not authenticatedMissing, expired, or ar- key presented as session — Bearer ar- never works here.
409key limit reachedThe account already holds the maximum number of keys (50 by default). Delete a key you no longer use, then retry — there is no rotation endpoint, so revoke-then-create is the intended path.
500failed to generate secure key / internal errorEntropy or store failure; retry, and if it persists report with a timestamp.

DELETE /api/dashboard/keys/{id}

Revoke an API key by ID. Revocation is immediate and permanent (hard delete scoped to your account). Unknown or unauthorized key ID returns 404; malformed ID returns 400.

bash
curl -b jar.txt -X DELETE https://atmorouter.dev/api/dashboard/keys/00000000-0000-4000-8000-000000000000
json
// HTTP 200 — the key stops working immediately, no grace window
{ "status": "ok" }
StatusErrorWhat it means
200okKey deleted for your account. The next inference call with it answers 401.
400invalid key idID is not UUID-shaped (36 chars, hex groups). Check for truncation when copying.
401not authenticatedSession missing or expired — log in again.
404key not foundUnknown ID, or an ID owned by someone else: both answer identically so key IDs cannot be probed.
No rotation, revoke and recreate. There is no key-rotation endpoint: rotation with a grace window is not available on Atmo today. You revoke the old key (DELETE /api/dashboard/keys/{id}) and create a new one; the old key stops working immediately, with no grace window.

GET /api/dashboard/usage

Query request history and per-request token usage for your account. The latest 200 entries in the selected range are returned, newest first. Timing fields latency_ms and ttft_ms may be null; inference_metrics is null for older requests. New request-time billing receipts and applied-rule snapshots are prospective; historical details may be unavailable. Failures expose a canonical reason and source when known. Preflight refusals before forwarding may not appear in this history. See Usage billing receipts for definitions. Requires a login session.

bash
curl -b jar.txt "https://atmorouter.dev/api/dashboard/usage?range=7d"
json
// HTTP 200 — newest first, capped at 200 entries; unknown range falls back to all time
{ "data": [{ "model": "cbcn/glm-5.3", "input_tokens": 9, "output_tokens": 7,
  "cached_tokens": 0, "cost": 0.0000231, "status": "success",
  "latency_ms": 1200, "ttft_ms": 200,
  "inference_metrics": {"request_id":"example-request-id","first_content_ms":450,"upstream_attempts":1,"terminal_event":true},
  "created_at": "2026-10-11T08:13:18.951407+07:00" }]}
Query ParameterTypeDescription
rangestringTime window filter: 24h → 24 hours, 7d → 7 days, 30d → 30 days, 90d → 90 days. Empty or anything else → 100 years (all history). Same mapping as usage-stats, so the log list and the summary cards never disagree.
StatusErrorWhat it means
200data listPer-request entries scoped to your account: model, input/output/cached tokens, cost, settlement status, timestamp.
401not authenticatedSession missing or expired — log in again.
500internal errorStore failure; retry, and if it persists report with a timestamp.

GET /api/dashboard/usage-stats

Aggregated usage totals and cost breakdown grouped by model and family across billed traffic. Requires active session cookie.

bash
curl -b jar.txt "https://atmorouter.dev/api/dashboard/usage-stats?range=30d"
json
// HTTP 200 — totals count billed traffic only (success + partial); error entries cost 0 and are out
{ "data": { "total_requests": 12, "total_input_tokens": 9010, "total_output_tokens": 3420,
  "total_cached_tokens": 1200, "total_cost": 0.0183, "official_cost": 0.0411, "total_saved": 0.0228,
  "by_model": { "cbcn/glm-5.3": { "requests": 12, "input_tokens": 9010, "output_tokens": 3420, "cached_tokens": 1200, "cost": 0.0183 } },
  "by_provider": { "CodeBuddy CN": { "requests": 12, "input_tokens": 9010, "output_tokens": 3420, "cached_tokens": 1200, "cost": 0.0183 } } } }

The by_provider key name is historical: it groups by catalogue family (resolved via a catalog join, falling back to the slug before / for models that left the catalog), not by splitting the model id. total_saved is official_cost − total_cost floored at zero, where cached input counts at 10% of the official input rate. The range parameter follows the same 24h / 7d / 30d / 90d → all mapping as GET /api/dashboard/usage.

StatusErrorWhat it means
200data objectTotals + by_model + by_provider (by family) for your account in the requested window.
400user_id requiredSession resolved without a user id — log in again.
401not authenticatedSession missing or expired — log in again.
What usage tracking records. The server stores the key id with each keyed request, but GET /api/dashboard/usage and GET /api/dashboard/usage-stats aggregate per account, with breakdowns by model and family. Requests through the playground carry no key id by design.

GET /api/dashboard/catalog

Full model catalog with pricing, limits, and discount metadata. Public read endpoint, no authentication required.

bash
curl https://atmorouter.dev/api/dashboard/catalog
curl "https://atmorouter.dev/api/dashboard/catalog?all=1"   # include unpriced models
json
// HTTP 200 — input/output prices are USD per million tokens; upstream identity never ships
{ "count": 1, "sync": { "total": 0, "priced": 0, "unpriced": 0, "uncompetitive": 0, "excluded": 0, "pruned": 0, "at": "0001-01-01T00:00:00Z" },
  "data": [{ "id": "acme/gpt-x", "family": "Acme", "official_in": 1, "official_out": 2,
    "input_price": 0.5, "output_price": 1, "cached_input_price": 0.5,
    "context_length": 128000, "max_output": 4096, "modality": "text",
    "supports_cache": false, "enabled": true, "discount_pct": 50 }]}
StatusErrorWhat it means
200data + count + syncEnabled models by default; ?all=1 also returns disabled/unpriced models. sync summarizes the last upstream refresh.
500internal errorCatalog read failure; retry, and if it persists report with a timestamp.
Catalogue filter, not a toggle. GET /api/dashboard/catalog?all=1 is a read filter that includes unpriced models; there is no user-facing toggle to enable or disable catalogue entries. Enabling stays operator-side.

GET /api/dashboard/balance

Returns current account prepaid balance in USD. Requires active session cookie.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/balance
json
// HTTP 200 — the automation check the plan asks for (fresh account)
{ "balance": 0 }
StatusErrorWhat it means
200balancePrepaid USD, deducted per settled request. Gate automation on this number before spawning spend.
401not authenticatedSession missing or expired — log in again.
404user not foundSession points at an account that no longer exists — log in again, then report if it repeats.

GET /api/dashboard/deposit/info

Returns live deposit parameters, current balance, and status of payment methods (QRIS and Epusdt crypto rails). Requires active session cookie.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/info
json
// HTTP 200 — read "enabled" before creating anything; false means the rail answers 503
{ "balance": 0,
  "methods": [
    { "id": "qris", "label": "QRIS", "kind": "fiat", "available": false,
      "note": "Scan with any Indonesian bank or e-wallet app. Credited automatically." },
    { "id": "epusdt", "label": "USDT / USDC", "kind": "crypto", "available": false,
      "note": "USDT or USDC on Solana, BNB Chain, or Polygon via hosted checkout. Amount and address are unique per order." } ],
  "qris": { "enabled": false, "min_idr": 10000, "usd_idr_rate": 16500 },
  "epusdt": { "enabled": false, "min_usd": 1, "network": "Solana / BNB Chain / Polygon",
    "tokens": ["USDT", "USDC"], "pending": null } }

QRIS available is the kill-switch AND the rail config together; Epusdt available is the Epusdt credentials check. Defaults when no settings have been saved are min_idr 10000 and usd_idr_rate 16500; the Epusdt floor constant is $1. pending carries your newest pending Epusdt order, or null when none exists.

StatusErrorWhat it means
200balance + methods + railsRender the deposit page from this one call: balance, per-method availability, live minimums, pending order.
401not authenticatedSession missing or expired — log in again.

POST /api/dashboard/deposit/epusdt/create

Create a hosted USDT or USDC checkout deposit order ($1.00–$100,000 when the Epusdt rail is configured; otherwise 503 before field validation). Requires active session cookie.

ParameterTypeDescription
amount_usdnumberRequired deposit amount in USD.
tokenstringOptional token: USDT (default) or USDC.
networkstringOptional network: solana (default), bsc, or polygon.
bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/epusdt/create \
  -H "Content-Type: application/json" -d '{"amount_usd":25,"token":"USDT","network":"solana"}'
json
// HTTP 201 — pay before expires_at; each order gets its own amount + address
{ "id": 7, "order_id": "AR-E-7-9f3a1c", "trade_id": "EP-88112", "amount_usd": 25,
  "actual_amount": 25.013, "token": "USDT", "network": "solana",
  "receive_address": "9xQeWvG816bUx9EPjHSo8uY6TqukC7d3m2p1oR4s", "payment_url": "https://pay.example/EP-88112",
  "expires_at": "2026-10-11T10:13:18+07:00" }

// HTTP 503 when the rail is unconfigured (checked BEFORE field validation)
{ "error": "USDT deposits are not configured yet" }

Token matching is case-insensitive and defaults to USDT; binance is accepted as an alias for bsc. Amount must sit between the $1 floor and $100,000. The two 503s (rail credentials, then GATEWAY_PUBLIC_URL for the Epusdt callback) run before any field validation, so an unconfigured rail answers 503 even for a bad body. Credit lands via the provider webhook — poll GET /api/dashboard/deposit/epusdt/status afterwards; history lives at GET /api/dashboard/deposit/epusdt/list.

StatusErrorWhat it means
201order objectCheckout order reserved. Pay the exact actual_amount to receive_address before expires_at.
400invalid JSONMalformed body.
400token must be USDT or USDCOnly those two tokens are accepted.
400network must be solana, bsc, or polygonOnly those three networks are accepted.
400amount must be between $1.00 and $100000Outside the rail window.
401not authenticatedSession missing or expired — log in again.
405method not allowedPOST only.
502failed to create payment orderRail reachable but order creation failed; the reserved order is marked failed, retry with a fresh call.
503USDT deposits are not configured yetRail credentials missing — check deposit/info enabled first.
503USDT deposits need GATEWAY_PUBLIC_URL …Operator callback misconfiguration; not yours to fix, report it.

POST /api/dashboard/topup/qris

Create a QRIS invoice in Indonesian Rupiah (IDR) with automated crediting upon payment. Amount floor is the live-settings minimum (default 10,000 IDR, ceiling 2,000,000 IDR) when the QRIS rail is configured; otherwise 503 before field validation. Requires active session cookie.

ParameterTypeDescription
amount_idrintegerRequired deposit amount in IDR (minimum from live settings, default 10,000 IDR).
bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/topup/qris \
  -H "Content-Type: application/json" -d '{"amount_idr":50000}'
json
// HTTP 201 — open pay_url and scan within 2 hours (expires_in 7200s)
{ "id": 3, "order_code": "AR-3-a1b2c3", "amount_idr": 50000, "amount_usd": 3.030303,
  "fx_rate": 16500, "qris": "", "pay_url": "https://pay.example/AR-3-a1b2c3", "expires_in": 7200 }

// HTTP 503 when the rail is unconfigured (checked BEFORE field validation)
{ "error": "QRIS payment is not configured" }

Floor comes from live settings (default 10,000 IDR) and ceiling is 2,000,000 IDR; USD credit is amount_idr ÷ fx_rate truncated to micro-dollars. The qris field is always empty — Sumopod renders the QR on the hosted pay_url page, so there is no QR payload to carry. total_payment_idr is the gross the buyer pays (fee inside, not on top). Like Epusdt, rail checks run first: unconfigured answers 503, kill-switch off answers 403, both before field validation. Credit is automatic on payment — poll GET /api/dashboard/topup/status; history lives at GET /api/dashboard/topups (combined: GET /api/dashboard/deposits).

StatusErrorWhat it means
201invoice objectInvoice reserved for 2 hours. Open pay_url and scan; balance credits automatically on confirm.
400invalid JSONMalformed body.
400minimum top-up is Rp…Below the live-settings floor — read deposit/info min_idr first.
400amount too largeAbove the 2,000,000 IDR ceiling.
401not authenticatedSession missing or expired — log in again.
403QRIS payment is disabledAdmin kill-switch off: the dashboard shows it disabled and no new invoice may issue.
405method not allowedPOST only.
502failed to create paymentRail reachable but invoice creation failed; the reserved order is marked failed, retry with a fresh call.
503QRIS payment is not configuredRail credentials missing — check deposit/info available first.

GET /api/dashboard/me

Confirm who the session belongs to without trusting anything the client stored locally: user id, email, balance. Requires active session cookie. An ar- inference key never works here and answers 401.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/me
json
// HTTP 200 — keys exactly user_id, email, balance
// identity values below are illustrative
{ "user_id": "1af7f336-…", "email": "[email protected]", "balance": 8.5 }
StatusErrorWhat it means
200user objectSession identity: user id, email, prepaid USD balance.
401not authenticatedSession missing or expired — log in again.
404user not foundSession points at an account that no longer exists — log in again, then report if it repeats.

GET /api/dashboard/market

Per-family market snapshot over enabled models: how many models each family sells, the sell-price range, and the average discount against official list prices. No authentication required.

bash
curl https://atmorouter.dev/api/dashboard/market
json
// HTTP 200 — prices are sell_in; discount is measured against official_in
{ "data": [{ "family": "CodeBuddy CN", "models": 2, "min_price": 0.9,
  "max_price": 1.8, "avg_discount_pct": 10 }]}
StatusErrorWhat it means
200data listOne entry per family, ordered by family. An empty catalogue answers an empty data list.
500internal errorCatalog read failure; retry, and if it persists report with a timestamp.

GET /api/dashboard/deposits

Live combined deposit history across both rails — every settle writes exactly one entry here. Session-scoped, full history with no LIMIT, keys id, amount, status, created_at. Requires active session cookie.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposits
json
// HTTP 200 — combined across rails; current caveat:
// created_at currently scans to "" and only the newest entry is returned
{ "data": [{ "id": "…", "amount": 5.0, "status": "completed", "created_at": "" }]}
StatusErrorWhat it means
200data listCombined settle ledger: no method or currency fields, newest first. Treat timestamps as unreliable and the list as newest-first-truncated until the read-path fix lands.
401not authenticatedSession missing or expired — log in again.
500internal errorStore failure; retry, and if it persists report with a timestamp.

GET /api/dashboard/deposit/epusdt/status

Poll one Epusdt order by order id. This endpoint settles: a pending order with a trade id and a configured rail is re-verified against the provider inline — status 2 credits your balance, 3 marks the order expired. Polling here is not read-only. Requires active session cookie.

bash
curl -b jar.txt "https://atmorouter.dev/api/dashboard/deposit/epusdt/status?order_id=AR-E-7-9f3a1c"
json
// HTTP 200 — keys exactly order_id, status
{ "order_id": "AR-E-7-9f3a1c", "status": "credited" }
StatusErrorWhat it means
200order objectCurrent order state. credited means balance already grew — do not re-pay.
400missing order_idThe query parameter is required.
401not authenticatedSession missing or expired — log in again.
404order not foundUnknown order id, or an order owned by someone else: both answer identically so ids cannot be probed.
405method not allowedGET only.

GET /api/dashboard/deposit/epusdt/list

List past Epusdt orders, newest first, capped at 20 entries. No parameters. Requires active session cookie.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/epusdt/list
json
// HTTP 200 — full per-order keys, healthy timestamps
// identity values below are illustrative
{ "data": [{ "order_id": "AR-E-7-9f3a1c", "trade_id": "EP-88112", "amount_usd": 25,
  "actual_amount": 25.013, "token": "USDT", "network": "solana",
  "receive_address": "9xQe…", "payment_url": "https://pay.example/EP-88112",
  "status": "credited", "created_at": "2026-10-12T03:52:06+07:00" }]}
StatusErrorWhat it means
200data listPer-order entries scoped to your account, newest first, at most 20.
401not authenticatedSession missing or expired — log in again.
500internal errorStore failure; retry, and if it persists report with a timestamp.

GET /api/dashboard/topup/status

Poll one QRIS invoice by numeric id — stored state only. Sumopod exposes no status endpoint, expiry never settles, and only the signed webhook credits, so polling here never moves money. Requires active session cookie.

bash
curl -b jar.txt "https://atmorouter.dev/api/dashboard/topup/status?id=3"
json
// HTTP 200 — keys exactly id, status
{ "id": 3, "status": "pending" }
StatusErrorWhat it means
200invoice objectStored state. pending means unpaid-or-unconfirmed: pay at pay_url, then wait for automatic credit. retired means the order can no longer be paid.
400invalid idId is missing, non-numeric, or not positive.
401not authenticatedSession missing or expired — log in again.
404topup not foundUnknown id, or an invoice owned by someone else: both answer identically so ids cannot be probed.
405method not allowedGET only.

GET /api/dashboard/topups

List past QRIS invoices, newest first, capped at 50 entries. No parameters. Requires active session cookie.

bash
curl -b jar.txt https://atmorouter.dev/api/dashboard/topups
json
// HTTP 200 — invoice entries with healthy timestamps (live shape)
// identity values below are illustrative
{ "data": [{ "id": 3, "order_code": "AR-3-a1b2c3", "amount_idr": 50000,
  "amount_usd": 3.030303, "fx_rate": 16500, "status": "pending",
  "pay_url": "https://pay.example/AR-3-a1b2c3", "created_at": "2026-10-12T03:52:06+07:00" }]}
StatusErrorWhat it means
200data listPer-invoice entries scoped to your account, newest first, at most 50.
401not authenticatedSession missing or expired — log in again.
500internal errorStore failure; retry, and if it persists report with a timestamp.

GET /api/public/status

Public platform health: request and latency windows over 24 hours and 7 days, a 30-minute banner window, per-family gauges, and a 14-day trend. No authentication required. Spend totals elsewhere count success + partial as billed; availability here is stricter — partial counts as failure.

bash
curl https://atmorouter.dev/api/public/status
json
// HTTP 200 — 16 top-level keys + 11 per-family keys
{ "requests_24h": 3, "success_pct_24h": 33.33, "latency_p50_24h": 120,
  "requests_7d": 3, "success_pct_7d": 33.33, "latency_p50_7d": 120,
  "requests_30min": 3, "availability_30min": 33.33, "buckets_7d": [{ "t": 1791745509 }],
  "definitions": { "currentWindowMinutes": 30, "historyDays": 7, "bucketMinutes": 120,
    "operationalAtOrAbovePct": 95, "degradedAtOrAbovePct": 80,
    "minRequestsForBanner": 20, "minRequestsForStateHint": 20 },
  "families": [{ "family": "example", "requests_24h": 3, "success_pct_24h": 33.33,
    "latency_p50_ms": 120, "latency_p95_ms": 120, "ttft_1h_ms": null,
    "throughput_1h": 0, "state": "low-traffic", "last_success": "2026-10-11T20:52:06Z",
    "buckets_7d": [], "spark_24h": [] }],
  "daily": [{ "date": "2026-10-11", "requests": 3, "success_pct": 33.33 }],
  "models_total": 2, "models_priced": 2,
  "catalog_synced_at": "2026-10-11T20:52:06Z", "generated_at": "2026-10-12T03:52:06Z" }

State per family comes from one rule: zero requests reads idle, fewer than 20 requests reads low-traffic, and at or above 95% reads operational, at or above 80% reads degraded, anything lower reads outage. Empty windows report null, never a fake 100%: ttft_1h_ms is null when nothing streamed in the last hour, bucket entries omit their value on quiet windows, and daily success renders 0 on empty days.

StatusErrorWhat it means
200health objectAggregate-only health: no per-user data, no prompt content, nothing identifying a customer. Cache the banner window; drill with families and daily.
429rate_limitedThe shared per-address budget for the public endpoints is exhausted; Retry-After says when it resets.
503unavailableThe aggregate is momentarily unavailable — the endpoint never returns a fabricated healthy snapshot.

GET /api/announcement

Current site banner. No authentication required. An empty body means render nothing — still a 200, so the client can tell “no announcement” apart from a server error.

bash
curl https://atmorouter.dev/api/announcement
json
// HTTP 200 — keys exactly body, updated_at
// values below are illustrative
{ "body": "Scheduled maintenance Sunday 02:00–03:00 UTC.", "updated_at": "2026-10-12T03:52:06+07:00" }

Affiliate sub-paths (/api/dashboard/affiliate/summary, /payouts, /referrals, /ledger) are covered in full under Affiliate; combos live under Combos. This reference keeps only their overview-table rows.

GET /api/public/stats

Platform-wide aggregate metrics for the landing page (request counts, active models, tokens processed). Public read endpoint, no authentication required. Responses carry Cache-Control max-age=60 as a client/proxy hint and are served from a short-lived server-side cache, so a value can be up to a minute old. Both public endpoints share one per-address request budget; once it is exhausted the answer is 429 with Retry-After telling you when it resets.

bash
curl https://atmorouter.dev/api/public/stats
json
// HTTP 200 — header Cache-Control: public, max-age=60 is a caching hint for clients and proxies
{ "total_requests": 1, "total_tokens": 150, "total_spent": 0.001, "total_saved": 0,
  "users": 2, "total_deposits": 0, "deposit_count": 0, "deposit_idr": 0,
  "models": 2, "families": 2, "cheapest_input": 0, "max_discount_pct": 100,
  "success_rate_7d": 50, "requests_7d": 2, "latency_p50_ms": 0,
  "health": [{ "family": "Acme", "requests": 2, "success_pct": 50 }] }

Spend totals count billed traffic (success + partial); availability is stricter — partial counts as failure there, same as GET /api/public/status. Deposit totals count settled (credited) deposits only, across every deposit source. success_rate_7d is null (not zero) when the last 7 days hold no requests. Per-family health is capped at 12 entries, busiest first.