Account & security
Only endpoints that intentionally differ from the standard API carry a chip; the chip links to the note that explains the difference.
Most of this section rides the session cookie, not an ar- key. Signing in issues an HttpOnly SameSite=Lax cookie with a 30-day expiry; me, reset-password, export, and the delete request and cancel steps answer 401 not authenticated without one. Verification (resend/complete) and the deletion confirm are token-bearing instead — they must work from an email link — and logout is an idempotent sign-out that clears whatever cookie is present.
/api/dashboard/me/api/dashboard/logout/api/dashboard/reset-password/api/dashboard/exportSession: check who you are, then leave
GET /api/dashboard/me — Returns the session owner's user id, email, and balance. Confirm the cookie here before scripting anything else.
POST /api/dashboard/logout — Destroys the session and clears the cookie. POST only; any other method answers 405.
Verification: resend, then complete
Signup is email-first. Register and resend share one 202 shape: HTTP 202 with verification_requested plus a resend cooldown. Completion is a separate call carrying the emailed token and the new password — it answers 201 registered and issues no session, so sign in afterwards. An expired or spent link answers 410 verification_invalid. Passwords must be 8–72 bytes, enforced at signup and reset alike.
# 1. Request the verification email (register and resend share one flow)
curl https://atmorouter.dev/api/dashboard/register -H "Content-Type: application/json" -d '{"email":"[email protected]"}'
# -> 202 {"status":"verification_requested","retry_after_seconds":60}
# 2. Finish signup with the emailed token plus password (no session is issued)
curl https://atmorouter.dev/api/dashboard/complete-registration -H "Content-Type: application/json" -d '{"token":"<64-hex-from-email>","password":"correct horse battery staple"}'
# -> 201 {"status":"registered"} — now sign in with POST /api/dashboard/loginToken errors are ordered and branchable: malformed syntax answers 400 invalid_request, an unknown or spent token answers 410 verification_invalid, and a live token with a weak password answers 400 invalid_password. Each resend mints a new token and revokes its siblings at completion, in one transaction, so only the newest link stays usable; a replayed token answers 410. Tokens expire 30 minutes after issue, checked both before and during completion. Send quota is three per email per hour plus 20 per IP, with global daily/monthly buckets: the fourth send inside the hour answers 429 rate_limited with retry_after_seconds and a matching Retry-After header, so a 429 on a fresh address means that address recently requested mail.
POST /api/dashboard/resend-verification — Re-send the verification email; same 202 shape as register.
POST /api/dashboard/complete-registration — Finish signup with token plus password. Returns no session; a weak password on a live token answers 400 invalid_password while a bad token answers 410.
Password change kills other sessions and all API keys
POST /api/dashboard/reset-password — Supply the current and the new password under your session. A wrong current password answers 403, and the step-up shares the sign-in limiter (20 tries per IP plus 8 per account per 15 minutes, then 429 with Retry-After). On success the password rotates, every other session is deleted while your current cookie survives, and every API key for the account is revoked — password, sessions, and keys commit atomically, so a stolen key cannot survive compromise recovery. Re-issue keys afterwards: the full secret is shown once at creation.
# Log in first (the jar keeps the session cookie)
curl -c jar.txt https://atmorouter.dev/api/dashboard/login -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"old-secret"}'
# Change the password; every OTHER session dies, all API keys are revoked, this jar survives
curl -b jar.txt -c jar.txt https://atmorouter.dev/api/dashboard/reset-password -H "Content-Type: application/json" -d '{"current_password":"old-secret","new_password":"new-secret-at-least-8"}'
# -> 200 {"status":"password updated","message":"Password updated. All other sessions were signed out and all API keys were revoked — create new keys."}
# Old keys are dead (401); create a fresh one
curl -b jar.txt https://atmorouter.dev/api/dashboard/keys -H "Content-Type: application/json" -d '{"name":"post-rotation"}'
# -> 201 {"key":"ar-…","message":"key created"} — save the secret now, it is shown onceDeletion in three steps, with a way back
Deletion is request, confirm, and optionally cancel — a staged state machine, not a single DELETE.
| Step | Endpoint | What happens |
|---|---|---|
| 1. Request | POST /api/dashboard/delete-account/request | Session plus the real password, under the same step-up limiter as reset. A 32-byte token is hashed server-side and a confirmation email follows; the link stays valid 15 minutes. |
| 2. Confirm | POST /api/dashboard/delete-account/confirm | Token in the POST JSON body. Confirm stages the account to pending_deletion with deletion_due_at 7 days out, clears all sessions, and consumes every token for the user in one transaction. |
| 3. Cancel (optional) | POST /api/dashboard/delete-account/cancel | Only a pending_deletion account can cancel: status flips back to active and a fresh session cookie is issued. |
# 1. Request deletion (password-gated; emailed link valid 15 minutes)
curl -b jar.txt https://atmorouter.dev/api/dashboard/delete-account/request -H "Content-Type: application/json" -d '{"password":"..."}'
# -> 200 {"status":"verification_email_sent","message":"Verification link sent to your email. Link valid for 15 minutes."}
# 2. Confirm with the emailed token IN THE POST BODY (never a query string)
curl https://atmorouter.dev/api/dashboard/delete-account/confirm -H "Content-Type: application/json" -d '{"token":"<64-hex-from-email>"}'
# -> 200 {"status":"pending_deletion","deletion_due_at":"...","message":"Account scheduled for deletion in 7 days."}
# 3. Cancel during the 7-day grace window (sign in again first — sessions were cleared)
curl -c jar.txt https://atmorouter.dev/api/dashboard/login -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"..."}'
curl -b jar.txt https://atmorouter.dev/api/dashboard/delete-account/cancel -X POST
# -> 200 {"status":"active","message":"Account deletion cancelled."}/confirm-delete?token=…): it sits in the address bar, browser history, and any query-logging access log until the page POSTs it and strips it. Treat the link as a bearer secret inside its 15-minute window, and prefer pasting the token into the JSON-body curl above over clicking through on a shared machine. The API call itself accepts the token from the body only.POST /api/dashboard/delete-account/request — Step 1: password-gated, emailed token valid 15 minutes.
POST /api/dashboard/delete-account/confirm — Step 2: token in the body, stages pending_deletion with a 7-day grace window.
POST /api/dashboard/delete-account/cancel — Step 3: restores active and re-issues a session.
During the grace window sign-in still works: pending_deletion sessions stay valid so the user can reach the cancel endpoint (login admits both statuses). Inference keys do not — key validation requires an active account, so a pending_deletion key stops calling while the dashboard stays reachable. Once deletion_due_at passes, a single cleanup pass removes the account and everything belonging to it — usage, keys, combos, deposits, sessions, tokens — atomically.
ar- inference keys stop the moment the account enters the deletion grace window, the session-cookie playground refuses with 403 account_pending_deletion, and new key issuance is refused the same way — matching the deletion email's "all API keys are paused and no new tokens can be generated" promise. Sign-in, cancel, and read-only dashboard paths stay reachable so the user can reverse the deletion.Export: profile, redacted keys, capped usage
GET /api/dashboard/export — Returns your profile (id, email, balance), your keys with secrets reduced to a stored 12-character preview written once at creation, and the newest 1,000 usage entries. Full key secrets appear exactly once, at creation — export can never leak them.
curl -b jar.txt https://atmorouter.dev/api/dashboard/export
# -> 200 {"user":{"id":"...","email":"[email protected]","balance":8.5},
# "keys":[{"id":"...","key":"ar-xxxxxxxxx…","name":"prod","enabled":true,"requests":0,"created_at":"..."}],
# "usage":[{"model":"...","input_tokens":9,"output_tokens":7,"cached_tokens":0,"cost":0.0000231,"status":"success","created_at":"..."}],
# "exported_at":"..."}The key field in every export entry is the 12-character preview, never the secret; usage is newest-first capped at 1,000 entries.