> ## Documentation Index
> Fetch the complete documentation index at: https://docs.square1.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Account API overview

> The cookie-session surface the dashboard uses for keys, sessions, and profile data

Alongside the [`/v1` developer API](/concepts/base-urls), Square1 exposes a
second HTTP surface that manages the **account itself** — API keys, browser
sessions, your own usage metrics, password and passkeys, and BYOK
credentials.

This is the surface the Square1 dashboard talks to. It is documented here
because a handful of things (listing your keys, revoking a lost session,
pulling your own per-model metrics) have no `/v1` equivalent.

<Warning>
  **Read this before you build against it.** These endpoints are not a
  product API. They are not versioned, they predate the `/v1` surface's
  conventions, and the shapes below reflect what the dashboard happens to
  need. Treat every response as additive-only and pin nothing.
</Warning>

## Base URL

```text theme={null}
https://dash.square1.dev
```

All paths on this page are relative to that origin. The `/v1` endpoints do
**not** live here, and these endpoints do not live on
`inference.square1.dev` — see [Base URLs](/concepts/base-urls).

## Authentication

Two credentials are accepted, and which ones work depends on the endpoint.

### Cookie session — `ws-session`

Signing in mints an `HttpOnly`, `Secure`, `SameSite=Lax` cookie named
`ws-session`, scoped to `/` with a 30-day lifetime. Every endpoint on this
page accepts it.

The sign-in routes that mint it (`POST /api/auth/login`,
`POST /api/auth/passkey/verify`, and the community-verification signup flow)
are outside the scope of this page.

### Bearer API key — dual auth

Most `/api/user/*` endpoints also accept an ordinary API key:

```text theme={null}
Authorization: Bearer sq-arca-...
```

This is the same key the `/v1` surface uses. When you authenticate this way
there is no session row behind your request — which matters for
[Sessions](/account/sessions), where `current` is then `false` on every row.

**BYOK management is the exception**: `/api/user/me/byok*` accepts a Bearer
key too, but applies a stricter gate. See
[BYOK management](/account/byok-management#gates).

<Note>
  **Cookie auth is not practical for unattended automation.** `ws-session`
  is minted by a login form that sits behind a Cloudflare Turnstile
  challenge when the operator has it enabled — there is no headless
  credential exchange. If you are scripting, use a Bearer API key on the
  dual-auth endpoints; reuse an existing browser session only for the
  handful of endpoints that require the cookie.
</Note>

## Account state

An account that is `unlinked` or `revoked` can still reach most of this
surface — that is deliberate, because self-recovery happens in the
dashboard. Two things are locked down anyway:

* **Minting credentials** (`POST /api/user/me/keys`,
  `POST /api/user/me/rotate-key`) requires `active`, and answers
  `403 account_unlinked` otherwise.
* **BYOK** requires `active` as well.

`suspended` and `retired` accounts are rejected outright.

<Note>
  The two credentials are not exactly equivalent: a `revoked` account can
  still reach this surface **with the cookie** but not with a Bearer key.
  `active` and `unlinked` work either way.
</Note>

The full state table lives in
[Authentication](/authentication#account-states).

## Response envelopes

Most endpoints answer with:

```json theme={null}
{ "success": true, "data": { "...": "..." } }
```

That is the majority, not the rule. The exceptions on this surface:

| Endpoint                    | Shape                                          |
| --------------------------- | ---------------------------------------------- |
| `GET /api/user/me`          | A **bare object** — no `success`, no `data`    |
| `GET /api/user/me/metrics`  | A bare object                                  |
| `GET /api/user/me/requests` | `{ "requests": [...], "limit": 20 }`           |
| `POST /api/user/sfw`        | `{ "success": true, "sfw_mode": true }` — flat |
| Deletes and other mutations | `{ "success": true }` with no `data`           |

Read for the field you want; don't assume a wrapper.

## Field naming

<Warning>
  **Key casing is not consistent, including inside a single response.**
  This is a known wart, not a pattern you can predict.
</Warning>

* `/api/user/*` request bodies are mostly camelCase (`currentPassword`,
  `profileUrl`) — but `POST /api/user/sfw` takes `sfw_mode`.
* `/api/user/me/byok*` is **entirely snake\_case**, request and response.
* `GET /api/user/me` mixes both in one payload: `profile`, `modelUsage`,
  and `totals` are camelCase while `display_id`, `today_self`,
  `recentRequests[]` rows, and everything under `plan` are snake\_case.
* `GET /api/user/me/metrics` is entirely snake\_case.

Copy the exact spelling from the tables on each page.

## Errors

Errors mostly use the same envelope as the `/v1` surface:

```json theme={null}
{
  "error": {
    "message": "재인증이 필요합니다. 대시보드에서 다시 인증해 주세요.",
    "type": "invalid_request_error",
    "code": "account_unlinked"
  }
}
```

Two differences from [`/v1` errors](/concepts/errors):

1. **`code` is frequently absent.** Many failures on this surface carry only
   `message` and `type`. Where a `code` exists it is listed explicitly on
   each page — branch on it there, and fall back to the HTTP status
   everywhere else.
2. **One endpoint returns a bare string.** `GET /api/user/me` answers
   `403` with `{"error": "User not found (legacy key)"}` — `error` is a
   string, not an object. If you unwrap `error.message` blindly you will
   get `undefined` here.

As always, `message` is Korean and is written for humans. Don't parse it.

## Rate limits

Only some paths carry an application-level window. Each is **60 seconds,
fixed** (not sliding), and counted **per client IP** — not per account.

| Path                                                     | Limit         | Notes                                             |
| -------------------------------------------------------- | ------------- | ------------------------------------------------- |
| `GET /api/user/me/metrics` + `GET /api/user/me/requests` | **120 / min** | One shared budget across both                     |
| `POST /api/user/me/password`                             | **5 / min**   |                                                   |
| `GET /api/user/me/keys/:id/reveal`                       | **30 / min**  |                                                   |
| `/api/user/me/byok` + `/api/user/me/byok/*`              | **15 / min**  | One shared budget across all seven BYOK endpoints |
| `/api/user/me/email/*`                                   | **15 / min**  |                                                   |
| `/api/auth/*`                                            | **15 / min**  | Group window over the sign-in routes              |

Everything else under `/api/user/*` — including the key list, key
creation/deletion, session management, passkeys, and `GET /api/user/me` —
has **no application-level window**. Edge limits still apply.

### Rate-limit headers

Requests that pass through one of the windows above carry exactly two
headers, on both accepted and rejected requests:

```text theme={null}
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
```

<Warning>
  There is **no `X-RateLimit-Reset`**, and the `429` from these windows
  carries **no `Retry-After`**. The window is a fixed 60 seconds; wait that
  long. Paths without a window emit neither header.
</Warning>

A rejection from these windows is:

```json theme={null}
{
  "error": {
    "message": "Too many requests. Please try again later.",
    "type": "rate_limit_exceeded",
    "code": "rate_limited"
  }
}
```

Separately, `POST /api/user/me/rotate-key` enforces its own 60-second
cooldown *instead of* a window, and its `429` carries **no `code`** and no
`Retry-After`. Handle a bare `429` as retryable.

## Browser access

<Note>
  Cross-origin browser access is not supported. These endpoints are served
  for the Square1 web app's own origin, and the custom response headers
  above are not exposed to cross-origin JavaScript. If you need this data in
  a page you host, proxy it through your backend with a Bearer key.
</Note>

## Pages

<CardGroup cols={2}>
  <Card title="API keys" icon="key" href="/account/api-keys">
    List, create, delete, reveal, and rotate your `sq-arca-` keys.
  </Card>

  <Card title="Sessions" icon="desktop" href="/account/sessions">
    See where you're signed in and cut off a lost device.
  </Card>

  <Card title="Profile and usage" icon="chart-line" href="/account/profile">
    `GET /api/user/me`, your own metrics, and your recent requests.
  </Card>

  <Card title="BYOK management" icon="plug" href="/account/byok-management">
    Register and verify your own provider credentials.
  </Card>

  <Card title="Password and passkeys" icon="lock" href="/account/passwords-passkeys">
    Change your password; manage WebAuthn credentials.
  </Card>
</CardGroup>
