# Authentication

Every `/v1` request carries one credential in the `Authorization: Bearer ...` header. Exayard accepts three kinds:

- **Key** (`exa_live_...` or `exa_test_...`). Made in an app on the Developer page. Use it for your own server, scripts
  and the CLI. See [Apps and keys](https://developers.exayard.com/apps-and-keys.md).
- **OAuth access token.** Issued when a person signs in through an OAuth client. If the client is an app made on the
  Developer page, the token acts as that app. Any other client (ChatGPT, Claude, Cursor, a client that registered
  itself) is a [personal connection](#personal-connections) and acts as the person.
- **Session token.** The Exayard web app's own sign-in, used by browser calls.

Older credentials keep working: API keys starting `ak_` and app secrets starting `xa_app_`. No new ones can be made.

## How a call finds its company

Most calls act in one company. Exayard finds it this way:

- **A key set to work in This company** (the default) works in its app's own company. Leave the company out. Naming
  another company is refused with 400 [`company_mismatch`](https://developers.exayard.com/concepts/errors.md#company_mismatch).
- **A key set to work in every company that installed it**, and an app's OAuth token, name the company on each call
  with the `Exayard-Organization-Id` header, or `organizationId` in the query string or JSON body. A call that names
  none is refused with 400 [`company_required`](https://developers.exayard.com/concepts/errors.md#company_required).
- **A person** (a session or a personal connection) names the company with `organizationId`, from the companies they
  belong to. An older company `ak_` key implies its company, like a key set to work in This company.

When the header and `organizationId` are both sent, they must name the same company, or the call is refused with
`company_mismatch`. When an operation takes `organizationId`, the header fills it in for you.

```bash
curl https://api.exayard.com/v1/projects \
  -H "Authorization: Bearer exa_live_..." \
  -H "Exayard-Organization-Id: js79m7b3kfqd3vqzwx0kpcnryx7h2k4e"
```

Four calls are about the app itself and name no company: `GET /v1/me`, `GET /v1/installations`, and the
[customer account](https://developers.exayard.com/concepts/customer-accounts.md) calls `GET`/`POST /v1/organizations` and
`DELETE /v1/organizations/{id}`. Responses to an app carry the company back in the `Exayard-Organization-Id` header,
429s included.

## Let other companies install your app

An app works in your own company from the start. To let other companies use it, they install it, and your app gets only
the access each company grants. An app is free on every plan; only the AI work it starts is charged, to the company it
runs in.

Everything below is in the app's **Let other companies install this app** part, under **Settings → Developer →
Apps**.

### Sign-in addresses

Add **Sign-in addresses** (up to 10) if the app signs people in with OAuth. Each must be `https://...`, or `http://`
on `localhost`, `127.0.0.1` or `[::1]`, with no fragment. Leave them empty if the app calls only with keys.

Saving the first sign-in addresses makes the app's OAuth client. Its **Client secret** is shown once, then; copy it,
because Exayard does not keep it. The **Client ID** is shown under the sign-in addresses. The client is confidential:
your server holds the client secret, and PKCE works with it too.

### Review and Find apps

New apps start unreviewed. An unreviewed app can be installed in up to 25 companies outside your own, with the
standard rate limits. **Ask for review** in the part's menu to lift the install cap and raise the limits; see
[Rate limits](https://developers.exayard.com/concepts/rate-limits.md). Once reviewed, **Show in Find apps** lists the app where other companies look
for apps.

### Install link (OAuth with PKCE)

1. **Copy install link** gives the first line below. Send an admin of the company to it, with the usual OAuth
   authorization parameters added, URL-encoded:

   ```
   https://app.exayard.com/settings/developer?install=<app public id>
     &client_id=<client id>
     &redirect_uri=https://yourapp.com/oauth/callback
     &response_type=code
     &scope=profile email
     &state=<random>
     &code_challenge=<S256 challenge>
     &code_challenge_method=S256
   ```

   `scope` here is only the sign-in identity (`profile email`). What the app may do in the company comes from the
   scopes the install grants.

2. Exayard shows the install dialog: your app, the company, whether the app is reviewed, and the scopes it asks for.
   The person picks a company where they are an admin, then **All projects** or **Only these projects** (see
   [Scopes](https://developers.exayard.com/concepts/scopes.md#project-limited-installs)), and can set a **Monthly AI limit** for the app. Someone who is
   not an admin is told which admins can install it and can copy the link to send them. If `client_id` does not match
   the app, the dialog says the link doesn't match and stops.

3. After **Install**, the browser goes on to the authorization server's `/oauth/authorize` with every parameter you
   sent except `install`, unchanged and in the same order. The person signs in and allows sharing their identity. Your
   `redirect_uri` then receives the `code`.

4. Exchange the `code`, with your `code_verifier` and client secret, at the authorization server's token endpoint. The
   authorization server is listed in
   [`https://api.exayard.com/.well-known/oauth-protected-resource`](https://api.exayard.com/.well-known/oauth-protected-resource)
   (`authorization_servers`); its own metadata lists the token endpoint.

A link with no `client_id` installs the app and stops there. Use that for an app that calls only with keys: once a
company has installed it, make a key set to work in **Every company that installed it**.

A person's OAuth token from your app acts as your app, in companies that installed it and where that person is still a
member. It holds the scopes that company granted to the install.

A company that has not installed the app is refused with 403 `app_not_installed`, which carries `install_url`, the
link to send its admin.

### When access ends

- **An admin removes the app** (**Settings → Developer → Connected apps → Remove**): the next call in that company is
  refused with `app_not_installed`. Installing it again turns the same install back on.
- **You add scopes to the app**: companies keep their current grant until an admin approves the new access (the
  Connected apps row says it asks for more access). **You remove scopes**: they are removed from every install at once.
- **A person leaves a company**: their OAuth tokens from your app stop working there. Keys are not affected.
- **Exayard suspends the app**: every call is refused with `app_suspended`. Installs are kept, so they work again when
  the suspension is lifted.
- **You delete the app**: every install and key is revoked and the OAuth client is deleted. Calls get 401.

### Installations

`GET /v1/installations` returns the companies that have your app installed and active, 50 per page by default
(`limit` up to 100, then `cursor`; see [Pagination](https://developers.exayard.com/concepts/pagination.md)). With a key you see every one. With a
person's OAuth token you see only the companies that person is a member of.

```json
{
  "data": [
    {
      "organizationId": "js79m7b3kfqd3vqzwx0kpcnryx7h2k4e",
      "organizationName": "Acme Roofing",
      "status": "active",
      "grantedScopes": ["read:projects"],
      "scopesVersion": 2,
      "installedAt": 1700000000000
    }
  ],
  "nextCursor": null
}
```

Only an app can call it; anyone else gets 403 `forbidden`.

## Who am I: `GET /v1/me`

With a key, `GET /v1/me` names the app:

```json
{
  "clerkUserId": null,
  "tokenType": "app_secret",
  "memberships": [],
  "app": { "publicId": "app_k3k3k3k3k3k3k3k3k3k3", "name": "Nightly Sync" }
}
```

The answer carries `organizationId` only when you send the `Exayard-Organization-Id` header, even for a key that works
in its own company. The SDK does not send that header. With a person's OAuth token from your app, the answer is the
person's (`clerkUserId`, `tokenType: "oauth_token"`, `memberships`) plus `app`.

## Personal connections

An OAuth client that is not an app made on the Developer page, such as ChatGPT, Claude, Cursor or a client that
registered itself, acts as the person who connected it, in the companies that person belongs to. If you are building
one integration for many companies, make an app instead.

Each person sees their own connections under **Settings → Developer → Connected apps → Your personal connections**,
with when each was first and last used and in which companies. **Remove** stops that connection at once: its next call
gets 403 `forbidden` saying the person removed it. Connecting the same client again does not undo this; **Allow again**
on the same row does. Removing one connection leaves the person's other connections working.

## OAuth for MCP clients

MCP clients find the authorization server from the
[protected resource metadata](https://api.exayard.com/.well-known/oauth-protected-resource) (`authorization_servers`).
That server's own metadata lists its registration endpoint for clients that register themselves. See
[Dynamic Client Registration](https://developers.exayard.com/mcp/dcr.md).

## WWW-Authenticate on 401

A 401 for a missing or invalid credential includes a
`WWW-Authenticate: Bearer realm="Exayard API", resource_metadata="..."` header, so MCP clients can find the
authorization server without hard-coding it.
