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

# API authentication

> Authenticate requests, manage API keys, and work with sessions.

Every path below is relative to your deployment — `https://usesink.co` on the hosted service.

## Authenticating a request

Two credentials work, and the `sk-` prefix is what tells them apart.

<CodeGroup>
  ```bash API key theme={null}
  curl https://usesink.co/workspaces/ \
    -H 'X-API-Key: sk-a1b2c3d4e5f60718'
  ```

  ```bash API key as bearer theme={null}
  curl https://usesink.co/workspaces/ \
    -H 'Authorization: Bearer sk-a1b2c3d4e5f60718'
  ```

  ```bash Session token theme={null}
  curl https://usesink.co/workspaces/ \
    -H 'Authorization: Bearer eyJhbGciOi…'
  ```
</CodeGroup>

An **API key** is minted in the dashboard or by `POST /keys/`, belongs to a user, and is what the CLI and any automation should use. A **session token** comes from a browser login and is short-lived.

### Workspace scope

On a path that contains `{workspace_id}`, that value decides the scope. On a path without one, Sink reads the `X-Workspace-Id` header, and failing that the credential's own scope — the workspace claim on a session token, or the holder's current workspace for an API key. A caller who is not a member of the resolved workspace gets `403`.

<Note>
  Interactive docs (`/docs`, `/redoc`, `/openapi.json`) are served only when a deployment runs with `DEBUG` enabled, so they are not available on the hosted instance.
</Note>

## Accounts

| Method   | Path                | Body                                                                                 | Notes                                                                                      |
| -------- | ------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
| `POST`   | `/auth/get-started` | `email`, `first_name`, `last_name`, `password`, `created_at`; `middle_name` optional | Creates a password account and its first workspace. `409` if the address is taken.         |
| `POST`   | `/auth/login`       | Form: `username`, `password`                                                         | Returns an access token and sets an HttpOnly refresh cookie.                               |
| `POST`   | `/auth/refresh`     | Refresh cookie, or `{ "refresh_token": "…" }`                                        | Cookie first, body as fallback. Rotates the refresh token.                                 |
| `POST`   | `/auth/logout`      | —                                                                                    | Revokes the current session and clears the cookie. Reports how many sessions were revoked. |
| `GET`    | `/auth/me`          | —                                                                                    | The authenticated user's profile.                                                          |
| `PATCH`  | `/auth/me`          | `name`, or `first_name` / `middle_name` / `last_name`                                | `name` wins when both are sent. Email cannot be changed here.                              |
| `DELETE` | `/auth/me`          | `{ "password": "…" }`                                                                | Irreversible. Deletes owned workspaces and leaves the rest; reports the counts.            |

### Verifying an email

| Method | Path                 | Purpose                                                 |
| ------ | -------------------- | ------------------------------------------------------- |
| `POST` | `/auth/request-otp`  | Mail the signed-in account a six-digit code.            |
| `POST` | `/auth/verify-email` | Same thing, under the name the dashboard's banner uses. |
| `POST` | `/auth/verify`       | Submit it: `{ "otp": 123456 }`.                         |

Codes expire on their own timer, and a fresh one is refused while a live one is outstanding.

### SSO

| Method | Path                    | Purpose                                |
| ------ | ----------------------- | -------------------------------------- |
| `GET`  | `/auth/google-login`    | Redirect into Google's consent screen. |
| `GET`  | `/auth/google-callback` | Complete the Google round trip.        |
| `GET`  | `/auth/github-login`    | Redirect into GitHub.                  |
| `GET`  | `/auth/github-callback` | Complete the GitHub round trip.        |

These are browser flows. A first sign-in through either creates the account and its first workspace.

## API keys

```bash theme={null}
curl -X POST https://usesink.co/keys/ \
  -H 'X-API-Key: sk-…' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "ci", "description": "Deploy bot", "expires_at": 43200 }'
```

<ParamField body="name" type="string" required>
  How the key shows up in the dashboard.
</ParamField>

<ParamField body="description" type="string">
  Free text — what this key is for.
</ParamField>

<ParamField body="expires_at" type="integer">
  Lifetime in **minutes from now**, not a timestamp. `43200` is thirty days. Omit for a key that does not expire.
</ParamField>

The response carries `token` — the actual credential. Only its SHA-256 digest is stored, so this is the one and only time it is shown.

| Method  | Path             | Purpose                                               |
| ------- | ---------------- | ----------------------------------------------------- |
| `POST`  | `/keys/`         | Mint a key.                                           |
| `GET`   | `/keys/`         | List the account's active keys. Never returns tokens. |
| `PATCH` | `/keys/{key_id}` | Update `name`, `expires_at` or `is_revoked`.          |
| `POST`  | `/keys/{key_id}` | Revoke this key.                                      |

A revoked or expired key answers `401` on its next use, with a message distinguishing the two.

## Sessions

| Method | Path                      | Purpose                                                                     |
| ------ | ------------------------- | --------------------------------------------------------------------------- |
| `GET`  | `/sessions/`              | List the account's live sessions — address, user agent, created and expiry. |
| `POST` | `/sessions/{session_id}/` | Revoke one session.                                                         |
| `POST` | `/sessions/`              | Revoke every session on the account.                                        |

Sessions are one per device: signing in again from the same user agent revokes that device's previous session rather than sitting alongside it.

## Errors

| Status | Meaning                                                                          |
| ------ | -------------------------------------------------------------------------------- |
| `401`  | No credential, or one that is invalid, revoked or expired.                       |
| `402`  | The workspace's plan does not allow this. See [Plans and limits](/guides/plans). |
| `403`  | Authenticated, but not a member of the workspace or team — or not its owner.     |
| `404`  | Not found, or not visible from this scope.                                       |
| `409`  | Conflicts with something that already exists.                                    |
| `422`  | The body failed validation.                                                      |
| `429`  | Rate limited. Honour `Retry-After`.                                              |

[Errors and limits](/api/errors) has the full picture, including rate-limit headers and the `207` case on secret reads.
