Management API
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.
/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/statsSuccess 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/budgets
{
"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. |
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.
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.
| Parameter | Type | Description |
|---|---|---|
| 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. |
curl https://atmorouter.dev/api/dashboard/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}'// 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 |
|---|---|---|
| string | Registered account email (verified accounts only). | |
| password | string | Account password. |
curl -c jar.txt https://atmorouter.dev/api/dashboard/login -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"..."}'// 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"
}| 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.
| 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). |
# 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// 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.
curl -b jar.txt -X DELETE https://atmorouter.dev/api/dashboard/keys/00000000-0000-4000-8000-000000000000
// 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. |
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.
curl -b jar.txt "https://atmorouter.dev/api/dashboard/usage?range=7d"
// 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.
curl -b jar.txt "https://atmorouter.dev/api/dashboard/usage-stats?range=30d"
// 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. |
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.
curl https://atmorouter.dev/api/dashboard/catalog curl "https://atmorouter.dev/api/dashboard/catalog?all=1" # include unpriced models
// 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. |
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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/balance
// 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/info
// 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. |
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"}'// 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.
| 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). |
curl -b jar.txt https://atmorouter.dev/api/dashboard/topup/qris \
-H "Content-Type: application/json" -d '{"amount_idr":50000}'// 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/me
// HTTP 200 — keys exactly user_id, email, balance
// identity values below are illustrative
{ "user_id": "1af7f336-…", "email": "[email protected]", "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.
curl https://atmorouter.dev/api/dashboard/market
// 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposits
// 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.
curl -b jar.txt "https://atmorouter.dev/api/dashboard/deposit/epusdt/status?order_id=AR-E-7-9f3a1c"
// 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/deposit/epusdt/list
// 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.
curl -b jar.txt "https://atmorouter.dev/api/dashboard/topup/status?id=3"
// 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.
curl -b jar.txt https://atmorouter.dev/api/dashboard/topups
// 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.
curl https://atmorouter.dev/api/public/status
// 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.
curl https://atmorouter.dev/api/announcement
// 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.
curl https://atmorouter.dev/api/public/stats
// 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.