# Authentication

> API keys for the REST API, OAuth for the MCP server.

The two surfaces authenticate differently, and deliberately so. The REST API is machine
credentials: one key, no human in the loop. The MCP server is delegated access: an agent
acting as a specific signed-in person.

## REST API keys

Every REST request carries a bearer token in the `Authorization` header.

```bash
curl -sS https://api.versionstory.com/v1/compare \
  -H "Authorization: Bearer vs_live_..." \
  -F "original=@v1.docx" \
  -F "modified=@v2.docx"
```

### Getting a key

Keys are created in the Version Story web app, on the **Developer** page (the terminal icon
in the sidebar, or [app.versionstory.com/developer](https://app.versionstory.com/developer)),
under **API keys**. Issuing or revoking one requires the **Developer** permission on the
organization.

- **Signing up gives you access.** [Create an account](https://app.versionstory.com/register)
  and you own a new organization with the Developer permission, so the page is there
  straight away. New organizations start on an [API trial](/developers/reference/formats-and-limits#api-trials).
- **In an existing organization,** a member who manages members can grant you the
  Developer permission from your entry in the organization's member settings.

![The API keys tab of Version Story's Developer page, listing each key with its last-used and created dates](developers/authentication/1-api-keys-card)

The Developer page's other tabs are useful alongside the keys: **Overview** charts API
activity and success rate and holds the [zero-day retention](/developers/rest/compare#zero-day-retention)
setting, and **Repository** shows every document and redline the API has created.

Give the key a name that says what it serves — "Contract intake integration" reads better
than "key 2" when someone audits the list a year later. The token is shown once, on
creation, in a banner with a copy button. Store it before you dismiss the banner.

### Getting a key from a coding agent

A coding agent setting up an integration (Claude Code, Codex, Cursor and the like) can get its
own key, with one click from you and no copying. It opens Version Story in your browser, you
sign in or register and click **Allow**, and the agent receives a new key and stores it where
your secrets live. This is the OAuth authorization code flow with PKCE, for a native app on a
loopback address ([RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252)):

1. The agent makes a PKCE pair (a random `code_verifier` and its S256 `code_challenge`) and a
   random `state`, and listens for one request at `http://127.0.0.1:<port>/callback`.
2. It opens `https://app.versionstory.com/developer/authorize` (or
   `https://uk.versionstory.com/developer/authorize` for the UK) with the query `client_name`,
   which becomes the key's name, `redirect_uri`, `code_challenge`, `code_challenge_method=S256`
   and `state`.
3. You sign in or register, and allow the request for your organization. Allowing it needs the
   Developer permission, as creating a key on the Developer page does.
4. The browser brings a one-time `code` back to the agent's listener, and the agent redeems it
   within five minutes:

```bash
curl -sS https://api.versionstory.com/v1/api-keys \
  -H "Content-Type: application/json" \
  -d '{"code": "...", "code_verifier": "...", "redirect_uri": "http://127.0.0.1:53682/callback"}'
```

```json
{
  "api_key": "vs_live_...",
  "name": "Claude Code on dana-laptop",
  "organization_id": "...",
  "organization_name": "Reyes LLP"
}
```

`POST /v1/api-keys` is the one call that takes no key. The `redirect_uri` must be exactly
`http://127.0.0.1:<port>/callback` (or `[::1]`), with no query, and the same value the authorize
page was given; a code is redeemed once. A code that is unknown, expired, already used, or
sent with the wrong verifier or redirect is `400 INVALID_AUTHORIZATION_CODE`.

The person who allowed the request is emailed that a key was created with their account, and
the key list marks the key **Created by sign-in**, so a key allowed by mistake is easy to spot
and revoke.

### What a key is

A key looks like `vs_{environment}_{id}_{secret}` — for example `vs_live_9f3c…_R7x…`. The
environment segment names the region the key belongs to, and so the API it works with:

| Prefix | Region | API |
| --- | --- | --- |
| `vs_live_` | US | `https://api.versionstory.com` |
| `vs_liveuk_` | UK | `https://api.uk.versionstory.com` |

A key works only with its own region's API. UK organizations sign in at
[uk.versionstory.com](https://uk.versionstory.com), and their documents stay in the UK.

A key belongs to the **organization**, not to the person who created it, so it keeps
working when that person changes roles or leaves. Requests authenticate as the
organization's service account: a machine identity that exists only to carry API work and
has no sign-in of its own. That account is what every API-created comparison is attributed
to and metered against. The member who issued the key is recorded as `created_by_email`, as
an audit trail and nothing more.

### Handling a key

- **The secret is shown once, at minting, and is never recoverable.** Version Story stores
  only a SHA-256 hash of it. If you lose the key, you get a new one; nobody can read the
  old one back to you.
- Treat it like a password. Keep it in a secrets manager or an environment variable, never
  in source control, and never in client-side code — a key in a browser bundle is a key
  the whole internet has.
- An organization can hold several keys at once, and each one can be revoked on its own.
  Revocation takes effect on the next request. Rotate in this order: create the new key,
  deploy it, then revoke the old one. The other order leaves a window with no valid
  credential.
- The key list shows each key's name, when it was created, and when it was last used, and
  marks revoked keys — enough to tell a live integration from one you can safely retire.
  Hover a key's Created date to see who issued it.

### Auth failures

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `INVALID_AUTHORIZATION_CODE` | `POST /v1/api-keys` only: the code is unknown, expired or used, or its verifier or redirect does not match |
| 401 | `INVALID_API_KEY` | Missing, malformed, unknown, revoked, or expired key, or a key sent to the other region's API |
| 403 | `API_KEY_ORG_MISMATCH` | The key's service account is no longer operating in the key's organization |

`API_KEY_ORG_MISMATCH` is deliberately loud rather than silent: honoring a request whose
principal has drifted would attribute and bill the work to the wrong organization. Because
a key's service account is derived from the organization itself, you should never see this
in practice. If you do, issue a new key and revoke the old one.

## MCP OAuth

The MCP server implements OAuth 2.1 with PKCE and
[RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) dynamic client registration, so
compliant MCP clients need no pre-shared credentials. Point the client at the server URL
and it handles registration and the sign-in redirect itself.

```
https://mcp-compare.versionstory.com/mcp
```

Discovery metadata is served where the spec says it should be:

| Endpoint | Purpose |
| --- | --- |
| `/.well-known/oauth-protected-resource` | Protected-resource metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) |
| `/.well-known/oauth-authorization-server` | Authorization-server metadata |
| `/register` | Dynamic client registration |
| `/authorize` | Authorization endpoint |
| `/token` | Token exchange and refresh |
| `/revoke` | Token revocation ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009)) |
| `/userinfo` | The signed-in person's identity |

Scopes are `openid`, `email`, and `profile` — the server needs only to identify the person
signing in. Authorization is then enforced per project on every call: a tool that touches a
project the signed-in user has no access to fails, regardless of the token being valid.

### Token lifetime

Access tokens are short-lived and refreshable. `check_connection` reports exactly where the
current one stands:

```json
{
  "authorized": true,
  "issued_at": "2026-08-10T20:14:03+00:00",
  "expires_at": "2026-08-10T21:14:03+00:00",
  "seconds_remaining": 2847,
  "refresh_supported": true
}
```

Because `refresh_supported` is always `true`, a client that holds a refresh token can renew
without sending the user back through sign-in. Refresh tokens rotate: every refresh returns
a new refresh token and spends the one it was given, so store the new one each time. A spent
refresh token is refused with `invalid_grant`. Signing out of the Version Story web app does
not revoke a connector's tokens — the connector uses its own OAuth client, so a browser
session ending doesn't tear down a running agent's access.
