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

# API keys + auth API

> Programmatic surfaces for creating, listing, and revoking SimpleFunctions API keys, plus the three-endpoint browser handshake the CLI uses for `sf login`.

SimpleFunctions issues per-user API keys of the form `sf_live_<base64url>`. Keys are shown exactly **once** at creation and stored hashed (Argon2) on the server. Every authenticated route accepts a key in `Authorization: Bearer sf_live_...`.

There is no public `GET /api/keys/{id}` — once created, the raw secret is only available from the create response.

## API keys

### List keys

```http theme={null}
GET /api/keys
```

**Auth:** `Authorization: Bearer sf_live_...` or browser session.

**Response 200**

```json theme={null}
{
  "keys": [
    {
      "id": "key_01J0...",
      "name": "my-agent",
      "keyPrefix": "sf_live_aB3D",
      "lastUsedAt": "2026-05-05T18:42:01.000Z",
      "revokedAt": null,
      "createdAt": "2026-04-22T10:11:08.000Z"
    }
  ]
}
```

`keyPrefix` is the first 12 characters and is safe to display. Revoked keys are returned with `revokedAt` set; they cannot authenticate any new request.

### Create a key

```http theme={null}
POST /api/keys
```

**Auth:** required.

**Body**

| Field  | Type   | Required | Default         | Notes                                                    |
| ------ | ------ | -------- | --------------- | -------------------------------------------------------- |
| `name` | string | optional | `"Unnamed Key"` | Human label. Shown in the dashboard and `GET /api/keys`. |

**Response 201**

```json theme={null}
{
  "id": "key_01J0...",
  "key": "sf_live_aB3D...XYZ",
  "keyPrefix": "sf_live_aB3D",
  "name": "my-agent",
  "message": "Save this key — it will not be shown again."
}
```

The `key` field is the **only** time the raw secret is returned. Store it now.

### Revoke a key

```http theme={null}
DELETE /api/keys/{id}
```

**Auth:** required. The caller must own the key.

**Response 200**

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

**Response 404**

```json theme={null}
{ "error": "Key not found or already revoked" }
```

### Curl

```bash theme={null}
# list
curl -H "Authorization: Bearer $SF_API_KEY" \
  "https://simplefunctions.dev/api/keys"

# create
curl -X POST -H "Authorization: Bearer $SF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-agent"}' \
  "https://simplefunctions.dev/api/keys"

# revoke
curl -X DELETE -H "Authorization: Bearer $SF_API_KEY" \
  "https://simplefunctions.dev/api/keys/key_01J0..."
```

## CLI auth handshake (`sf login`)

The CLI cannot accept a Supabase password locally, so it runs a three-endpoint handshake that proxies the browser session into a fresh API key.

```text theme={null}
CLI                                        Browser                  Server
 │  1. POST /api/auth/cli  { sessionToken }  ─────────────────────▶  store session (5 min TTL)
 │
 │  2. open https://app.simplefunctions.dev/auth/cli?token=...
 │
 │                                            3. user logs in (Supabase)
 │                                            4. POST /api/auth/cli/complete { sessionToken }
 │                                                                  ▶ create API key, attach to session
 │
 │  5. poll GET /api/auth/cli/poll?token=...  ──────────────────▶   { status: 'pending' | 'ready' }
 ▼
 receive sf_live_..., persist locally
```

### `POST /api/auth/cli` — initialize

**Auth:** none (the `sessionToken` is the shared secret).

**Body**

| Field          | Type   | Required | Notes                                                     |
| -------------- | ------ | -------- | --------------------------------------------------------- |
| `sessionToken` | string | yes      | At least 32 characters. CLI generates this with a CSPRNG. |

**Response 200**

```json theme={null}
{ "ok": true, "expiresAt": "2026-05-05T20:05:00.000Z" }
```

**Errors**

| Status | Body                                    | Cause                                 |
| ------ | --------------------------------------- | ------------------------------------- |
| `400`  | `{ "error": "Invalid JSON" }`           | Body is not valid JSON.               |
| `400`  | `{ "error": "Invalid session token" }`  | Missing or shorter than 32 chars.     |
| `409`  | `{ "error": "Session already exists" }` | Token collision — generate a new one. |
| `500`  | `{ "error": "Internal error" }`         | Server failure.                       |

### `GET /api/auth/cli/poll?token=...` — poll for the key

**Auth:** none. The session token is the shared secret.

**Response 200 (pending)**

```json theme={null}
{ "status": "pending" }
```

**Response 200 (ready)**

```json theme={null}
{ "status": "ready", "apiKey": "sf_live_..." }
```

The server deletes the session row immediately after returning a key, so a successful poll is a one-shot.

**Errors**

| Status | Body                            | Cause                                            |
| ------ | ------------------------------- | ------------------------------------------------ |
| `400`  | `{ "error": "token required" }` | `?token=` is missing.                            |
| `410`  | `{ "status": "expired" }`       | Session expired (5 min TTL) or already consumed. |

### `POST /api/auth/cli/complete` — browser callback

This is what the browser hits after Supabase login. CLI integrators don't call it directly.

**Auth:** Supabase session cookie (browser-side).

**Body**

| Field          | Type   | Required | Notes                                                                                       |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------- |
| `sessionToken` | string | yes      | Must match the token created by `POST /api/auth/cli` and not yet have an `apiKey` attached. |

**Response 200**

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

**Errors**

| Status | Body                                                                    | Cause                                 |
| ------ | ----------------------------------------------------------------------- | ------------------------------------- |
| `400`  | `{ "error": "Invalid JSON" }` or `{ "error": "sessionToken required" }` | Bad body.                             |
| `401`  | `{ "error": "Not authenticated" }`                                      | No Supabase session.                  |
| `410`  | `{ "error": "Session expired or already used" }`                        | Session expired or already converted. |

The created key is named `"CLI (browser login)"` so users can see it in the dashboard and revoke it later.

## Signup

```http theme={null}
POST /api/signup
```

Public endpoint for new account creation. Returns the Supabase session and a starter API key. Use the dashboard at `https://simplefunctions.dev/signup` for the standard flow; this endpoint is mainly for programmatic onboarding.

## See also

<CardGroup cols={2}>
  <Card title="Authentication" href="/guides/authentication">
    All auth flavors — API key, browser session, BYOK exchange creds.
  </Card>

  <Card title="Direct API access" href="/build/direct-api-access">
    First curl calls and language examples.
  </Card>

  <Card title="API keys (enterprise)" href="/enterprise/api-keys">
    Rotation policy and per-tool restriction.
  </Card>

  <Card title="Errors" href="/reference/errors">
    Full error envelope reference.
  </Card>
</CardGroup>
