# AtmoRouter — Dashboard API

Source: https://atmorouter.dev/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](https://atmorouter.dev/docs/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](https://atmorouter.dev/docs/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.

| Field | Contract |
| --- | --- |
| model | Required model id. |
| api_key_id | Optional id of an API key owned by your account. Omit for the account default. |
| max_input_price / max_output_price | Required finite, nonnegative final retail rates in USD per million tokens. Zero disables paid requests. |
| queue_wait_seconds | Optional; 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](https://atmorouter.dev/docs/budgets#put-api-dashboard-budget-rules).

## 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.

| Parameter | Type | Description |
| --- | --- | --- |
| email | string | Required user email address. |
| ref | string | Optional referral code (unknown codes are ignored at completion). |
| source | string | Optional first-touch attribution from the landing URL. |
| utm_source | string | Optional campaign attribution, stored as provided. |
| utm_campaign | string | Optional campaign attribution, stored as provided. |
| landing_path | string | Optional landing path attribution, stored as provided. |

```bash
curl https://atmorouter.dev/api/dashboard/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'
```

```json
// HTTP 202 — check the inbox, then complete registration
{
  "status": "verification_requested",
  "retry_after_seconds": 60
}
```

| Status | Error code | What it means |
| --- | --- | --- |
| 202 | verification_requested | Verification email queued. Complete with POST /api/dashboard/complete-registration using the emailed token plus a new password, then sign in. |
| 400 | invalid_email | A valid email address is required. |
| 400 | invalid_request | Malformed JSON, an unknown field, or a bare null body — the parser is strict. |
| 403 | origin_rejected | Browser Origin header does not match the app origin. Plain curl without Origin is unaffected. |
| 405 | method_not_allowed | POST only. |
| 413 | request_too_large | Body over 4 KB. |
| 415 | unsupported_media_type | Content-Type must be application/json. |
| 429 | rate_limited | Signup quota spent. Retry-After header plus retry_after_seconds say when to retry. |
| 503 | signup_unavailable / service_unavailable | Registration 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.

| Parameter | Type | Description |
| --- | --- | --- |
| email | string | Registered account email (verified accounts only). |
| password | string | Account password. |

```bash
curl -c jar.txt https://atmorouter.dev/api/dashboard/login -H "Content-Type: application/json" -d '{"email":"you@example.com","password":"..."}'
```

```json
// HTTP 200 — jar.txt now holds the atmo_session cookie (HttpOnly, 30-day expiry)
{
  "user_id": "00000000-0000-4000-8000-000000000001",
  "email": "you@example.com",
  "token": "<64-hex session token — shown once>",
  "message": "login successful"
}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | login successful | Session created. Reuse the jar (-b jar.txt) for every authenticated row below; the token field also works as an Authorization: Bearer header. |
| 400 | invalid JSON | Malformed request body. |
| 401 | invalid credentials | Wrong password, unknown email, or unverified account — the response is identical either way so accounts cannot be enumerated. |
| 405 | method not allowed | POST only. |
| 429 | too many sign-in attempts … | Flood guard spent (20 per IP + 8 per account per 15 minutes). Retry-After says when to retry. |
| 500 | could not start session | Session 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](https://atmorouter.dev/docs/api-keys).

| Parameter (POST) | Type | Description |
| --- | --- | --- |
| name | string | Optional key label (defaults to empty). |
| rate_limit | integer | Optional per-minute request limit (omit for 500; explicit 1..1000, anything else 400). |
| allowed_models | string[] | 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": "..." }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 201 / 200 | key created / data list | POST mints ar- + 48 hex chars from 24 secure random bytes; GET lists newest-first scoped to your account. |
| 400 | invalid JSON | Malformed POST body. |
| 400 | rate_limit must be 1..1000 | Omit the field for the 500 default; any explicit value outside 1..1000 (including 0 and negatives) is rejected. |
| 401 | not authenticated | Missing, expired, or ar- key presented as session — Bearer ar- never works here. |
| 409 | key limit reached | The 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. |
| 500 | failed to generate secure key / internal error | Entropy 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" }
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | ok | Key deleted for your account. The next inference call with it answers 401. |
| 400 | invalid key id | ID is not UUID-shaped (36 chars, hex groups). Check for truncation when copying. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 404 | key not found | Unknown 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](https://atmorouter.dev/docs/observability#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 Parameter | Type | Description |
| --- | --- | --- |
| range | string | Time 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. |

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data list | Per-request entries scoped to your account: model, input/output/cached tokens, cost, settlement status, timestamp. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 500 | internal error | Store 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`.

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data object | Totals + by_model + by_provider (by family) for your account in the requested window. |
| 400 | user_id required | Session resolved without a user id — log in again. |
| 401 | not authenticated | Session 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 }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data + count + sync | Enabled models by default; ?all=1 also returns disabled/unpriced models. sync summarizes the last upstream refresh. |
| 500 | internal error | Catalog 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 }
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | balance | Prepaid USD, deducted per settled request. Gate automation on this number before spawning spend. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 404 | user not found | Session 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.

| Status | Error | What it means |
| --- | --- | --- |
| 200 | balance + methods + rails | Render the deposit page from this one call: balance, per-method availability, live minimums, pending order. |
| 401 | not authenticated | Session 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.

| Parameter | Type | Description |
| --- | --- | --- |
| amount_usd | number | Required deposit amount in USD. |
| token | string | Optional token: USDT (default) or USDC. |
| network | string | Optional 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 `503`s (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`.

| Status | Error | What it means |
| --- | --- | --- |
| 201 | order object | Checkout order reserved. Pay the exact actual_amount to receive_address before expires_at. |
| 400 | invalid JSON | Malformed body. |
| 400 | token must be USDT or USDC | Only those two tokens are accepted. |
| 400 | network must be solana, bsc, or polygon | Only those three networks are accepted. |
| 400 | amount must be between $1.00 and $100000 | Outside the rail window. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 405 | method not allowed | POST only. |
| 502 | failed to create payment order | Rail reachable but order creation failed; the reserved order is marked failed, retry with a fresh call. |
| 503 | USDT deposits are not configured yet | Rail credentials missing — check deposit/info enabled first. |
| 503 | USDT 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.

| Parameter | Type | Description |
| --- | --- | --- |
| amount_idr | integer | Required 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`).

| Status | Error | What it means |
| --- | --- | --- |
| 201 | invoice object | Invoice reserved for 2 hours. Open pay_url and scan; balance credits automatically on confirm. |
| 400 | invalid JSON | Malformed body. |
| 400 | minimum top-up is Rp… | Below the live-settings floor — read deposit/info min_idr first. |
| 400 | amount too large | Above the 2,000,000 IDR ceiling. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 403 | QRIS payment is disabled | Admin kill-switch off: the dashboard shows it disabled and no new invoice may issue. |
| 405 | method not allowed | POST only. |
| 502 | failed to create payment | Rail reachable but invoice creation failed; the reserved order is marked failed, retry with a fresh call. |
| 503 | QRIS payment is not configured | Rail 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": "you@example.com", "balance": 8.5 }
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | user object | Session identity: user id, email, prepaid USD balance. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 404 | user not found | Session 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 }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data list | One entry per family, ordered by family. An empty catalogue answers an empty data list. |
| 500 | internal error | Catalog 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": "" }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data list | Combined 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. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 500 | internal error | Store 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" }
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | order object | Current order state. credited means balance already grew — do not re-pay. |
| 400 | missing order_id | The query parameter is required. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 404 | order not found | Unknown order id, or an order owned by someone else: both answer identically so ids cannot be probed. |
| 405 | method not allowed | GET 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" }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data list | Per-order entries scoped to your account, newest first, at most 20. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 500 | internal error | Store 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" }
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | invoice object | Stored state. pending means unpaid-or-unconfirmed: pay at pay_url, then wait for automatic credit. retired means the order can no longer be paid. |
| 400 | invalid id | Id is missing, non-numeric, or not positive. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 404 | topup not found | Unknown id, or an invoice owned by someone else: both answer identically so ids cannot be probed. |
| 405 | method not allowed | GET 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" }]}
```

| Status | Error | What it means |
| --- | --- | --- |
| 200 | data list | Per-invoice entries scoped to your account, newest first, at most 50. |
| 401 | not authenticated | Session missing or expired — log in again. |
| 500 | internal error | Store 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.

| Status | Error | What it means |
| --- | --- | --- |
| 200 | health object | Aggregate-only health: no per-user data, no prompt content, nothing identifying a customer. Cache the banner window; drill with families and daily. |
| 429 | rate_limited | The shared per-address budget for the public endpoints is exhausted; Retry-After says when it resets. |
| 503 | unavailable | The 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](https://atmorouter.dev/docs/affiliate); combos live under [Combos](https://atmorouter.dev/docs/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.
