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

# Authentication

> Send a workspace API key as a bearer token. Every route on the API requires one, and every refusal is a 401 in the standard error body.

Authentication is one header. There is no other scheme, no query parameter, and
no unauthenticated route on this API.

Every request to the API carries your key in an `Authorization` header using the
`Bearer` scheme. A missing header, a scheme other than `Bearer`, a key of the
wrong shape, and a key we reject are all answered with `401`.

<CodeGroup>
  ```bash curl theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
  curl https://api.hopscotchlabs.ai/v1/chat/completions \
    -H "Authorization: Bearer $HOPSCOTCH_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "openai/gpt-4o-mini",
      "messages": [{"role": "user", "content": "Say hi in five words."}]
    }'
  ```

  ```python Python theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
  import os
  from openai import OpenAI

  client = OpenAI(
      base_url="https://api.hopscotchlabs.ai/v1",
      api_key=os.environ["HOPSCOTCH_API_KEY"],
  )
  ```

  ```typescript Node theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
  import OpenAI from "openai";

  const client = new OpenAI({
    baseURL: "https://api.hopscotchlabs.ai/v1",
    apiKey: process.env.HOPSCOTCH_API_KEY,
  });
  ```
</CodeGroup>

Keep the key out of your source. Read it from the environment, as above.

## Key format

A key is a non-secret prefix followed by 43 random characters from the URL-safe
base64 alphabet (`A-Z`, `a-z`, `0-9`, `-`, `_`).

| Prefix     | Where the key works |
| ---------- | ------------------- |
| `ub_live_` | Production          |
| `ub_stg_`  | Staging             |
| `ub_dev_`  | Development         |

The prefix is there so a key pasted somewhere public is recognisable as ours by
secret scanners, and so the environment a key belongs to is never a guess. The
prefix is not secret; everything after it is.

Keys are created in the dashboard under Keys and shown once, at creation. We
store a one-way hash, so we cannot show you a key again or recover one for you.
If a key is lost, create a new one and revoke the old one.

A key belongs to the workspace, not to the person who created it. It carries no
personal identity, and the requests it makes are the workspace's requests.

## What a 401 means

Four situations produce a `401` with the type `authentication_error` and the code
`invalid_api_key`. The `message` is the only thing that differs, and two of them
share one message deliberately.

```json No key sent theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "You did not provide an API key. Send it in an Authorization header using Bearer auth.",
    "type": "authentication_error",
    "param": null,
    "code": "invalid_api_key",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}
```

A missing `Authorization` header and a header using any scheme other than Bearer
both get this.

```json Key of the wrong shape theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "Incorrect API key format. A Hopscotch key looks like ub_live_ followed by 43 characters. Check you have not pasted a key from another provider.",
    "type": "authentication_error",
    "param": null,
    "code": "invalid_api_key",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}
```

A key that cannot be one of ours is refused on its shape alone, before anything
is looked up. This is the message you get after pasting a provider's key or a
truncated one.

```json Key we will not accept theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "Incorrect API key provided.",
    "type": "authentication_error",
    "param": null,
    "code": "invalid_api_key",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}
```

A key that is correctly shaped but is not live gets this one sentence, whether it
was never issued, has been revoked, or belongs to an account that can no longer
make requests. That is deliberate: telling a caller which of those it was would
confirm to whoever holds a stolen key that the key was once real. Only our own
logs distinguish them, and the request id is how support finds the line.

<Note>
  Every response carries the same id on two headers: `x-hopscotch-request-id`,
  which is ours, and `x-request-id`, which is the one the official OpenAI SDKs
  surface on their error objects. Every error envelope repeats it as
  `request_id`. Quote it when you ask us about a request. Without it we are
  guessing at which of your requests you mean.
</Note>

## Rate limiting on the key

Each key has a request rate limit. Exceeding it is a `429` with the type
`rate_limit_error` and the code `rate_limit_exceeded`, carrying a `Retry-After`
header in seconds:

```json theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "Rate limit reached for this key. Try again in 12 seconds. Request id 00000000-0000-4000-8000-000000000000.",
    "type": "rate_limit_error",
    "param": null,
    "code": "rate_limit_exceeded",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}
```

This is one of several refusals that share the `429` status. They are told apart
by `code`, never by status alone. See
[Errors](/api-reference/errors#rate-limits-and-spend-refusals).

## Handling keys

* Send the key from a server you control. A key in browser code is a key
  published.
* Store it as a secret, not in source control. The `ub_` prefixes exist partly so
  scanners can catch the mistake, and catching it is not the same as preventing
  it.
* Rotate by creating the replacement first, moving traffic to it, then revoking
  the old key. Revoking is done in the dashboard under Keys.
* A key that stops working answers `401` with `Incorrect API key provided.`, so
  treat that response as "this key is gone" rather than as a transient failure to
  retry.
