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

# BYOK management

> Register, verify, and manage your own provider credentials

[BYOK](/guides/byok) lets you route `/v1/chat/completions` requests through
**your own** model-provider account using the `@provider/model` syntax. These
seven endpoints are how the credentials behind that syntax are managed.

They live under `https://dash.square1.dev/api/user/me/byok`.

<Note>
  Everything here concerns credentials **you** registered on **your own**
  account. Provider names appear in these responses because you are the
  source of that information — they say nothing about how the shared model
  pool is served.
</Note>

## Gates

This router applies three checks in order, ahead of every endpoint below:

1. **Active session or key** — an `unlinked` or `revoked` account is
   rejected here even though it can reach the rest of `/api/user/*`.
2. **No guests** — guest accounts expire, and an expired guest would orphan
   stored ciphertext.
3. **Feature enabled** — BYOK is an operator switch, and it additionally
   requires the server's credential-encryption secret to be configured.

Failures 2 and 3 both answer **`403 byok_disabled`**:

```json theme={null}
{
  "error": {
    "message": "이 기능은 현재 사용할 수 없습니다.",
    "type": "invalid_request_error",
    "code": "byok_disabled"
  }
}
```

Guests get the same code with a different message
(`게스트 계정에서는 사용할 수 없습니다.`). Branch on the code, not the text.

<Warning>
  **All seven endpoints share one rate-limit budget: 15 requests/minute.**
  A model-list fetch and a save compete for the same window.
</Warning>

## Field naming

<Warning>
  **This router is entirely snake\_case**, request bodies and responses
  alike — the exact opposite of its `/api/user/*` siblings, which are mostly
  camelCase. `auth_scheme`, `is_primary`, `fallback_model_id`,
  `base_url_origin`.
</Warning>

## List credentials — `GET /api/user/me/byok`

```bash theme={null}
curl https://dash.square1.dev/api/user/me/byok \
  -H "Authorization: Bearer $SQUARE1_API_KEY"
```

```json theme={null}
{
  "success": true,
  "data": {
    "credentials": [
      {
        "id": 7,
        "provider": "openai",
        "auth_scheme": "api-key",
        "label": "work",
        "masked_hint": "sk-pr…",
        "is_primary": true,
        "pinned": false,
        "status": "active",
        "last_verified_at": "2026-08-29T10:00:00.000Z",
        "last_used_at": "2026-08-29T11:14:02.000Z",
        "last_error_code": null,
        "created_at": "2026-08-20T08:31:00.000Z",
        "base_url_origin": null,
        "fallback_model_id": null,
        "has_org_id": false,
        "has_project_id": false,
        "region": null
      }
    ],
    "providers": [ /* ... */ ],
    "max_per_provider": 3,
    "fallback_default_on": false,
    "fallback_override": null,
    "fallback_enabled": false
  }
}
```

### `credentials[]`

| Field                          | Notes                                                                                                                                       |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `masked_hint`                  | The **first 5 characters** of the secret plus `…`. Computed once at save time; the same rule for every provider                             |
| `status`                       | `active` or `rejected`. A dispatch failure flips a credential to `rejected`; a successful model-list fetch flips it back                    |
| `last_error_code`              | Set alongside `rejected`, cleared on re-verification                                                                                        |
| `base_url_origin`              | **Origin only** — never the full URL, since a custom base URL can carry a token in its path or query                                        |
| `has_org_id`, `has_project_id` | Presence booleans. The values themselves are never returned                                                                                 |
| `is_primary`                   | The credential used when `@provider/model` doesn't name a label. The first credential you save for a provider becomes primary automatically |
| `pinned`, `fallback_model_id`  | Your dispatch preferences — see [PATCH](#update-a-credential)                                                                               |

**The secret is write-only.** There is no reveal endpoint, no unmasked
field, and no way to read it back. Replace it by saving a new credential.

### `providers[]`

The catalog of provider types you may register, one row per
**(provider, auth scheme)** pair. A provider that supports two schemes
appears twice — because the per-provider cap is counted per scheme, so
collapsing them would make the count wrong.

Each row carries `id`, `label`, `auth_scheme`, `required_fields`,
`optional_fields`, `supports_models_list`, and `multi_scheme`.

Drive your UI from this array rather than hardcoding a list — which
providers are offered, and which fields each needs, is server-side data. The
`label` values are the human-readable provider names shown in the dashboard.

### The three fallback fields

They look redundant and are not:

| Field                 | Meaning                                                      |
| --------------------- | ------------------------------------------------------------ |
| `fallback_default_on` | The operator's global default                                |
| `fallback_override`   | **Your** explicit choice, or `null` for "follow the default" |
| `fallback_enabled`    | The two collapsed — the value actually in effect             |

Sending only the last one would make "following the default" and
"explicitly chose the same value" indistinguishable, and there would be no
way back to `null`.

## Set the fallback toggle — `PUT /api/user/me/byok/fallback`

```bash theme={null}
curl -X PUT https://dash.square1.dev/api/user/me/byok/fallback \
  -H "Authorization: Bearer $SQUARE1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": null}'
```

```json theme={null}
{ "success": true, "data": { "fallback_override": null, "fallback_enabled": false } }
```

`enabled` accepts `true`, `false`, or `null`. **`null` is a value, not a
deletion** — it means "follow the operator default again", which is why
there is no `DELETE` on this path. Omitting `enabled` entirely is a
`400 invalid_request`.

A `404` with no `code` is possible but rare — it means the account row
vanished between authentication and the write.

## Verify without saving — `POST /api/user/me/byok/verify`

The "test" button. Runs a live probe against the provider and reports the
result, storing nothing.

```bash theme={null}
curl -X POST https://dash.square1.dev/api/user/me/byok/verify \
  -H "Authorization: Bearer $SQUARE1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "openai",
    "auth_scheme": "api-key",
    "label": "work",
    "secret": "sk-...",
    "meta": {}
  }'
```

Success:

```json theme={null}
{ "success": true, "data": { "ok": true, "model_count": 62 } }
```

<Warning>
  **A failed verification is still HTTP 200.** The result is in the body:

  ```json theme={null}
  { "success": false, "data": { "ok": false, "kind": "...", "message": "..." } }
  ```

  Check `data.ok`, not the status code. This endpoint reserves non-2xx for
  malformed requests — not for "your credential didn't work".
</Warning>

When the credential requires a `base_url`, a successful response also
carries **`resolved_base_url`** — the URL the server actually assembled from
your input. Show it: pasting a full endpoint path (rather than a base) is a
common mistake that otherwise surfaces only as an opaque failure.

| HTTP | `code`                    | Meaning                                                                                                                     |
| ---- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| 400  | `byok_credential_missing` | The request itself is unusable: unknown provider, unsupported auth scheme, empty secret, or a missing required `meta` field |
| 501  | `byok_upstream_error`     | This provider type has no verification path                                                                                 |

## Save a credential — `POST /api/user/me/byok`

Same body as `verify`. The server re-runs the identical probe first and
**only stores a credential that passed** — there is no unverified state.

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

<Warning>
  **The same probe failure that `verify` reports as `200 {ok:false}` is a
  `400` here.** One request, two surfaces, two shapes. Do not write one
  handler for both — branch on the endpoint you called.
</Warning>

* `label` is optional (max 40 characters). Omit it and the server assigns
  the lowest unused `key-N` for that provider and scheme.
* **The label is immutable after creation** — it is bound into the
  encryption of the secret.
* The first credential saved for a provider becomes `is_primary`.

| HTTP | `code`                     | Meaning                                                                                                            |
| ---- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 400  | (the probe's own code)     | The credential failed live verification. Nothing was stored                                                        |
| 400  | `byok_credential_conflict` | You hit `max_per_provider` for this provider and scheme, **or** that label is already in use                       |
| 400  | `byok_credential_missing`  | Unusable request — see `verify` above                                                                              |
| 400  | `invalid_request`          | A `meta` field is malformed (for example a region that isn't region-shaped). The message names the offending field |

<Note>
  `byok_credential_conflict` covers both the cap and a duplicate label. If
  you need to tell them apart, compare your active count against
  `max_per_provider` from the list response.
</Note>

## Update a credential — `PATCH /api/user/me/byok/:id`

```bash theme={null}
curl -X PATCH https://dash.square1.dev/api/user/me/byok/8 \
  -H "Authorization: Bearer $SQUARE1_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"pinned": true, "fallback_model_id": "deepseek-v4-pro"}'
```

Exactly three fields are accepted:

| Field               | Notes                                                                      |
| ------------------- | -------------------------------------------------------------------------- |
| `pinned`            | Boolean                                                                    |
| `is_primary`        | Only `true` does anything — you promote a credential, you don't demote one |
| `fallback_model_id` | A **Square1 catalog** model id, or `null` / `""` to clear                  |

`label` and `secret` are **not** accepted. The label is part of the secret's
encryption, and secrets are replaced by saving a new credential.

`fallback_model_id` must name a model in the shared catalog — a BYOK
(`@provider/...`) id is rejected, because falling back from your key to your
key defeats the point. An unknown id is
`400 byok_credential_missing`.

`404 byok_credential_missing` if the id isn't yours. A non-integer id is a
`400` with the same code.

## Delete a credential — `DELETE /api/user/me/byok/:id`

```json theme={null}
{ "success": true }
```

Soft-revokes the credential and promotes another to primary if needed.
`404 byok_credential_missing` if it isn't yours. The label frees up for
reuse.

## List a credential's models — `GET /api/user/me/byok/:id/models`

A server-side proxy that asks **your** provider what models your credential
can see. Your secret never leaves the server.

```bash theme={null}
curl https://dash.square1.dev/api/user/me/byok/8/models \
  -H "Authorization: Bearer $SQUARE1_API_KEY"
```

```json theme={null}
{ "success": true, "data": { "models": [ /* ... */ ], "cached": false } }
```

* `cached` tells you whether the answer came from the short-lived per-credential
  cache. A cache hit never skips the ownership check.
* A successful fetch marks the credential verified — a `rejected` credential
  returns to `active`.

| HTTP | `code`                           | Meaning                                          |
| ---- | -------------------------------- | ------------------------------------------------ |
| 400  | (the upstream's classified code) | Your provider refused the listing                |
| 404  | `byok_credential_missing`        | Not your credential (or no such id)              |
| 501  | `byok_upstream_error`            | This provider type doesn't offer a model listing |

The ids returned here are **your provider's** ids — use them verbatim after
the `@provider/` prefix. BYOK models never appear in
[`GET /v1/models`](/api-reference).

## Calling a BYOK model

Registration is only half of it; the request syntax, the `@provider/model`
forms, and the dispatch-time error surface are covered in
[BYOK](/guides/byok).
