Authentication
Every /v1 request carries one credential in the Authorization: Bearer ... header. Exayard accepts three kinds:
- Key (
exa_live_...orexa_test_...). Made in an app on the Developer page. Use it for your own server, scripts and the CLI. See Apps and keys. - 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 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. - 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-Idheader, ororganizationIdin the query string or JSON body. A call that names none is refused with 400company_required. - A person (a session or a personal connection) names the company with
organizationId, from the companies they belong to. An older companyak_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.
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 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. Once reviewed, Show in Find apps lists the app where other companies look for apps.
Install link (OAuth with PKCE)
-
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=S256scopehere is only the sign-in identity (profile email). What the app may do in the company comes from the scopes the install grants. -
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), 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_iddoes not match the app, the dialog says the link doesn't match and stops. -
After Install, the browser goes on to the authorization server's
/oauth/authorizewith every parameter you sent exceptinstall, unchanged and in the same order. The person signs in and allows sharing their identity. Yourredirect_urithen receives thecode. -
Exchange the
code, with yourcode_verifierand client secret, at the authorization server's token endpoint. The authorization server is listed inhttps://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). With a key you see every one. With a
person's OAuth token you see only the companies that person is a member of.
{
"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:
{
"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 (authorization_servers).
That server's own metadata lists its registration endpoint for clients that register themselves. See
Dynamic Client Registration.
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.