# Connect with OpenLLM (/connect)

Your users already pay for AI. With a **Connect with OpenLLM** button, they bring it to your app. They sign in with OpenLLM, and your app runs AI on **their** account: their Claude, ChatGPT, and other subscriptions, their own API keys, and their local models.

* You pay $0 for their inference.
* You never see their provider keys.
* They set limits for your app and can disconnect it at any time.

It's standard OAuth 2.1 (authorization code with PKCE) plus one step: on the consent screen, OpenLLM creates a key for your app and seals it to your app's public key. Your server collects it once.

You don't need a package:

* **Helper**: [`openllm-connect.ts`](https://www.openllm.sh/connect/openllm-connect.ts). One file, no dependencies, MIT.
* **Button**: [`button.svg`](https://www.openllm.sh/connect/button.svg). See [Button and brand rules](#brand).

## Get access
OpenLLM reviews every app by hand before it can connect anyone. There is no self-serve app creation, and no credentials exist until your app is approved.

<Steps>
  <Step>
    ### Apply
    Fill in the form at [openllm.sh/developers/apply](https://www.openllm.sh/developers/apply): your email, your app's name, website, what it does, its callback URLs (`https` only, or `http://localhost:…` while you build), and a logo URL.
  </Step>

  <Step>
    ### Talk to us
    We email you to set up a short call. To follow your application, sign in to OpenLLM with the email you applied under. It shows under **Settings → Developers → Apps**.
  </Step>

  <Step>
    ### Get approved
    We email you when your app is approved. If we decline it, the email and your app's row carry the reason, and you can edit and resubmit.
  </Step>

  <Step>
    ### Create your credentials
    In **Settings → Developers → Apps**, press **Set up** on your approved app. Your browser makes the key pair and creates the app, then shows a `.env` block:

    ```bash
    OPENLLM_CLIENT_ID=…
    OPENLLM_CLIENT_SECRET=…        # kept in your vault; "New secret" makes another
    OPENLLM_PRIVATE_KEY=…          # your app's X25519 private key, made in your browser
    OPENLLM_ISSUER=…               # the OAuth issuer
    OPENLLM_RESOURCE=https://openllm.sh/v1
    OPENLLM_API_ORIGIN=https://openllm.sh
    OPENLLM_REDIRECT_URI=…         # one of your callback URLs
    ```

    The client secret and private key are sealed under your vault, so your app's page shows them again any time while your vault is unlocked. OpenLLM's server never sees them in plaintext. Keep every value server-side. If you lose the private key, you need a new app.
  </Step>
</Steps>

## Add the routes
Copy `openllm-connect.ts` into your server code unchanged. It must never run in a browser: it holds your client secret and private key. It needs only `fetch` and WebCrypto with X25519 (Node 20+, Bun, Deno, Cloudflare Workers, Vercel Functions).

You need three routes.

### Start
Your button links here. Make PKCE and state, keep them in a short-lived httpOnly cookie, and redirect.

```ts
import { openllmConfigFromEnv, startConnect } from "./openllm-connect";

const openllm = openllmConfigFromEnv();
const { url, state, codeVerifier } = await startConnect(openllm);
// set cookie openllm_oauth = JSON.stringify({ state, codeVerifier })
//   httpOnly, secure, sameSite=lax, maxAge 600, path=/
// redirect 302 → url
```

### Callback
At the path of `OPENLLM_REDIRECT_URI`. Check `state`, finish, and store the connection. `finishConnect` opens the sealed key with your private key and returns the connection: the OpenLLM user id, the connection's own `sk-llm-…` key, and its tokens.

```ts
import { finishConnect, reconnectTokens } from "./openllm-connect";

// Read ?code and ?state, compare state with the cookie (reject on mismatch),
// and delete the cookie. ?error=access_denied means the user said no.
try {
  const connection = await finishConnect(openllm, { code, codeVerifier });
  await saveConnection(currentUser, connection); // encrypt at rest
} catch (err) {
  // Connected before: keep their stored key, take the new tokens.
  const stored = await loadConnection(currentUser);
  const next = stored && reconnectTokens(stored, err);
  if (!next) throw err;
  await saveConnection(currentUser, next);
}
```

A user's key can be collected only once. If they connect again, `finishConnect` throws `already_picked_up` and `reconnectTokens` combines the key you stored with the new tokens.

### Call the API
Any OpenAI-compatible call, as that user:

```ts
import { openllmFetch } from "./openllm-connect";

const res = await openllmFetch(
  openllm,
  connection,
  "/v1/chat/completions",
  {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      model: "plus", // or "ultra", "lite", or any id from /v1/models
      messages: [{ role: "user", content: "Hello" }],
      stream: true,
    }),
  },
  (next) => saveConnection(currentUser, next), // tokens were refreshed
);
```

`openllmFetch` refreshes the access token a minute before it expires and passes you the new connection to store. It sets two headers on every request:

```
Authorization: Bearer <access token>     # proves it's your app, for this user
X-OpenLLM-Key: sk-llm-…                  # the connection's own key
```

The rest of the [API](/api) works the same way: `/v1/messages` (Anthropic), `/v1/responses`, `/v1/models`, `/v1/embeddings`, `/v1/images/generations`, and `/v1/audio/*`. Calls are server-to-server only. Don't call `/v1` from a browser.

### The button
Link it to your start route:

```html
<a href="YOUR_START_ROUTE">
  <img src="https://openllm.sh/connect/button.svg" alt="Connect with OpenLLM" height="44" />
</a>
```

## What your users see
1. Your button sends them to OpenLLM sign-in. New users get an account in a few seconds, with nothing to set up first.
2. The consent screen shows your app's name, where it sends them back, and optional limits they can set for you: monthly tokens, models, and an expiry.
3. They return to your callback. Their key runs on whatever their account can serve: their subscriptions through the OpenLLM app or daemon on their computer, their API keys, and the [included models](/providers/included) on their plan. Someone brand new may have nothing connected yet. See `subscription_requires_daemon` under [Errors](#errors).

Users can change your limits or disconnect your app at any time under **Connections → Apps**.

## Errors
API errors are `{ "error": { "code", "message" } }`. The helper throws `OpenLLMError` with `code`, `status`, and `needsReconnect`. `needsReconnect` is true for `connector_reconnect`, `connector_auth`, and `invalid_grant`.

| Code                                                     | Meaning                                                                                                                | What to do                                                                                                                            |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `connector_reconnect`                                    | The user disconnected your app, or reset their vault                                                                   | Delete the stored connection and show the button again                                                                                |
| `connector_auth`                                         | The token or key is invalid for this user                                                                              | Same as above                                                                                                                         |
| `invalid_grant`                                          | The refresh token no longer works, but the user is still connected                                                     | Keep the stored connection and show the button. The callback needs it for `reconnectTokens`, because the key can't be collected twice |
| `connector_opaque_token`                                 | `resource` was missing on authorize or token                                                                           | The helper always sends it                                                                                                            |
| `already_picked_up`                                      | This user was already connected                                                                                        | Call `reconnectTokens(stored, err)`                                                                                                   |
| `no_pending_grant` / `grant_expired`                     | Consent didn't finish, or took over 15 minutes                                                                         | Send them through the button again                                                                                                    |
| `pkce_mismatch`                                          | Wrong `code_verifier` for this sign-in                                                                                 | Check your cookie handling                                                                                                            |
| `app_not_open`                                           | HTTP 403. Your app isn't approved by OpenLLM yet, so only its owner can connect. People already connected keep working | Finish [getting access](#get-access)                                                                                                  |
| `access_*`                                               | The user's limits for your app (tokens, models, rate)                                                                  | Tell the user. They can raise them under **Connections**                                                                              |
| `subscription_requires_daemon` / `local_requires_daemon` | The user's models need a provider or computer that isn't connected or is offline                                       | Ask them to finish setup at [openllm.sh/get-started](https://www.openllm.sh/get-started), then retry                                  |

## Disconnect and revocation
To disconnect a user from your side, call `disconnect`. Their key stops working at once.

```ts
import { disconnect } from "./openllm-connect";

await disconnect(openllm, connection);
```

The user can also disconnect your app under **Connections → Apps**, or reset their vault. Your next call then fails with `connector_reconnect`: delete the stored connection and show the button again.

## Security
* Your client secret, private key, and each user's `sk-llm` key and refresh token are credentials. Store them server-side, encrypted at rest. Never log them or send them to the browser.
* Always check `state` on the callback.
* The sealed key opens only with your private key, and it is bound to your client id, the user, and the connection. A copy is useless anywhere else.
* Your app gets its **own** key for each user, never the user's own key.

## Changing your app
Edit your app under **Settings → Developers → Apps**.

* **Name, homepage, and logo** apply at once. Leave the logo empty and OpenLLM uses the one on your homepage. People see it when they connect.
* **A new callback host** goes to review. Your current callbacks keep working until the change is approved, and people already connected stay connected.

## Button and brand rules
Use the official button, linked to your start route:

| File                                                                  | Use on                          |
| --------------------------------------------------------------------- | ------------------------------- |
| [`button.svg`](https://www.openllm.sh/connect/button.svg)             | Default (dark button)           |
| [`button-dark.svg`](https://www.openllm.sh/connect/button-dark.svg)   | Light backgrounds (dark button) |
| [`button-light.svg`](https://www.openllm.sh/connect/button-light.svg) | Dark backgrounds (light button) |

* Give the button comparable visual prominence to your other sign-in options.
* Say clearly which of your plans use the user's OpenLLM account.
* Don't redraw or recolor the button. For the OpenLLM logo itself, see [Brand assets](/brand-assets).

## Using a coding agent
An agent-readable copy of this page is at [docs.openllm.sh/connect.md](https://docs.openllm.sh/connect.md). Point your coding agent at it: it copies `openllm-connect.ts` unchanged, adds the start and callback routes and a per-user store with your project's framework, session, and database, and reads every `OPENLLM_*` value from the environment.

## Related
* [API reference](/api)
* [SDK](/sdk)
* [Errors](/errors)
* [Brand assets](/brand-assets)