View as Markdown

Authentication

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

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:

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.

  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), 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 (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

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.