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

# Origins

> The rule that makes a public key safe to publish.

A publishable key is readable by anyone who views your page source. What stops
someone lifting it and running your workspace's agent from their own site is the
origin check.

## The rule

Every widget endpoint resolves the key, then checks the request `Origin` against
the workspace. In production, the check runs in this order:

<Steps>
  <Step title="Your workspace domain, if one is set">
    One workspace, one domain. The request origin's host must match the domain
    you set in **Settings → Workspace**.

    The comparison strips a leading `www.` from both sides, so `example.com` and
    `www.example.com` are the same. Everything else must match exactly — a
    subdomain does not inherit. A request with no `Origin` header is refused.

    When a domain is set, this is the whole check. There is no separate list to
    configure.
  </Step>

  <Step title="No domain set — fail closed">
    Before onboarding sets a domain, `localhost`, `127.0.0.1` and `::1` are
    allowed implicitly on any port, so local development works.

    Any other origin must be on the key's explicit allowlist. A production key
    with no domain and no allowlist entries is **not** open to the internet — it
    is refused.
  </Step>
</Steps>

<Note>
  Outside production the check does not run at all. A valid key is still
  required, but any origin is accepted, so a local host page works against a
  locally-run platform on any port regardless of the workspace's domain. This
  never loosens a real deployment.
</Note>

## Getting this right

Set your domain in **Settings → Workspace** as soon as you have one. Until you
do, your key works from localhost and nowhere else, which usually shows up as
the widget working in development and silently not appearing in staging.

| Workspace domain  | Request origin                     | Result                                        |
| ----------------- | ---------------------------------- | --------------------------------------------- |
| `example.com`     | `https://example.com`              | Allowed                                       |
| `example.com`     | `https://www.example.com`          | Allowed                                       |
| `www.example.com` | `https://example.com`              | Allowed                                       |
| `example.com`     | `https://app.example.com`          | **Refused** — a subdomain is a different host |
| `example.com`     | `https://example.com.attacker.net` | Refused                                       |
| `example.com`     | *(no `Origin` header)*             | Refused                                       |
| *(none set)*      | `http://localhost:3000`            | Allowed                                       |
| *(none set)*      | `https://example.com`              | Refused unless on the key's allowlist         |

<Warning>
  If your app runs on a subdomain, set the workspace domain to that subdomain.
  Setting it to the apex domain will not cover `app.` — the host comparison is
  exact once `www.` is normalised away.
</Warning>

## How a refusal looks

Most widget endpoints answer a disallowed origin with `403`:

```json theme={null}
{ "error": "origin not allowed" }
```

Some use a longer form:

```json theme={null}
{ "error": "origin not allowed for this workspace key" }
```

Some do not report it as an error at all — the calls that run on page load fall
back to safe defaults or empty results instead, because a hard failure there
would surface as a console error inside your app. If the widget appears but
never does anything, check the origin before you check anything else.

## CORS

Peeve echoes your origin in `Access-Control-Allow-Origin` only when the key
resolves *and* the origin is allowed. Otherwise it answers `*`, which — combined
with the endpoint refusing to act — reveals nothing.

Preflight (`OPTIONS`) is answered `*`, because it carries no credentials.

Allowed request headers: `Content-Type`, `Authorization`, `X-Peeve-Key`. The
MCP endpoints also allow `Mcp-Session-Id`.

<Note>
  The MCP endpoints and the artifact endpoint answer `Access-Control-Allow-Origin: *`
  unconditionally. They are read-only and guide-only, so there is nothing to
  protect with an origin check. See [the MCP overview](/mcp/overview).
</Note>

## What the origin check is for

The origin check bounds what a **browser** can do with a publishable key it
found in someone else's page. It is not a general access control, and it is not
meant to be — a server can send any `Origin` header it likes.

That is why the [server API](/api/overview) does not rely on it at all: those
endpoints require a secret key, which is a credential you keep private, and they
send no CORS headers so a page cannot call them in the first place.
