Skip to main content

Connectors & OAuth

Apps & API Keys covers first-party access — you creating an App on your own tenant. Connectors are the other direction: a third-party product that connects to your customers' Lexabit data, with each customer's explicit consent, over standard OAuth 2.0 (authorization code + PKCE). This is "Connect with Lexabit."

The model: Connector, Connection, App, Token

The flow separates four things — keeping them distinct is what makes access safe and revocable:

ObjectWhat it is
ConnectorYour product's OAuth doorway — a client registration (client_id, redirect URIs, branding). Registered once per product. Holds no access by itself.
ConnectionThe record that a user authorized your Connector on a specific entity — the audit + revocation ledger.
AppThe machine-actor entity minted by that authorization. It holds the granted roles on one entity and is what your token acts as. (Same kind of App as first-party, just connection-born.)
TokenThe bearer token you store and send on every call. Bound to one App.

A Connector is your doorway; a user authorizing it on an entity mints a new App (scoped to that entity) and a Token bound to it.

Isolation is structural

Because a Token is bound to one App, and that App holds roles on exactly one entity, the Token can only ever reach that entity — by construction, not by you "sending the right scope." Two customers authorizing the same Connector get two separate Apps and two separate tokens; neither can see the other's data.

Environments: build against staging

Develop and test your integration against staginghttps://api.staging.lexabit.com. The examples below show the production host (https://api.lexabit.com), which is reserved for applications running against the live environment: substitute the staging host until you go live. Connector registrations are per environment — the client_id/client_secret you receive for staging will not work in production; you get separate production credentials at go-live. Each environment serves its own discovery document at /.well-known/oauth-authorization-server, so an OAuth-aware client only ever needs the right base URL.

Step 0: Register your Connector

Connector registration is administrative — you don't self-register today. Contact Lexabit to register your product; you'll provide a name, logo, homepage, and the exact redirect_uri(s) your product will use, and receive a client_id (and a client_secret, unless you use PKCE only). Redirect URIs are matched exactly at authorization time, with one deliberate exception: http loopback URIs (127.0.0.1 / localhost) match on any port (RFC 8252), so a local app that binds a random port at runtime registers one portless URI. Register every non-loopback URI you'll use.

Registration also settles what your Connector may ask for. Together with Lexabit you agree the set of roles (and, where relevant, fine-grained capability keys) your Connector may request at authorize time, and usually a default set that applies when you omit scope entirely. Because access is granted as roles rather than individual permissions, it scales with the platform: when a role's definition grows alongside new product features, every connection holding that role follows automatically — you never need to track individual permissions or ask your customers to re-consent to stay current.

Local apps can skip registration entirely

An app running on the user's own machine (a CLI tool, a local AI assistant) doesn't need its own Connector — use the shared public client documented in Connecting an MCP.

Step 1: Send the user to authorize

Redirect the user's browser to the authorization endpoint with your client_id, a registered redirect_uri, the roles (scope) you're requesting, and a PKCE code_challenge:

GET https://api.lexabit.com/oauth/v1/authorize
?client_id=<your-connector-client_id>
&redirect_uri=https://your-app.example/callback
&response_type=code
&scope=viewer
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
&state=<your-csrf-state>

Lexabit validates the Connector is active and the redirect_uri matches, has the user log in if needed, and shows a consent screen: "Acme wants viewer access to <entity name>." The target entity comes from the user's own scope — not anything your product sends — so a Connector can never widen its own reach.

The scope parameter is a space-delimited list of role names you are requesting on the target entity — viewer (read-only) is the right default for most integrations. The platform-wide list of consentable roles is published as scopes_supported in the discovery document; your Connector requests from within the set agreed at registration. Tokens outside that set — including typos — are dropped silently rather than rejected (only a request that resolves to nothing at all fails, with error=invalid_scope), so a mistyped role simply shows the user a narrower consent screen than you designed. If you omit scope entirely, your Connector's registered default set is requested instead. What each role permits is covered in Users, Roles & Permissions. Request the least access that works — the consent screen shows the user exactly what you asked for. The consentable set itself is operator-managed and can change — see Policy changes.

Step 2: Receive the authorization code

On approval, Lexabit redirects back to your redirect_uri with a one-time code (and your state):

https://your-app.example/callback?code=<authorization-code>&state=<your-csrf-state>

Step 3: Exchange the code for a token

From your server, exchange the code — plus your PKCE code_verifier and client authentication — at the token endpoint:

curl -X POST "https://api.lexabit.com/oauth/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<authorization-code>" \
-d "redirect_uri=https://your-app.example/callback" \
-d "client_id=<your-connector-client_id>" \
-d "client_secret=<your-connector-client_secret>" \
-d "code_verifier=<pkce-verifier>"

The response is RFC-flat (not the {data,meta} envelope), so any OAuth client library can read it:

{
"access_token": "1|9x7K…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt_4Kf…"
}

That access_token authenticates as the App the consent minted — scoped to the one entity the user authorized. It is valid for one hour (expires_in). The refresh_token is how you get the next hour without involving the user again — see Keeping access alive. Store both server-side; treat the refresh token like a password.

Step 3b: Keeping access alive (refresh)

Access tokens expire after one hour. Before (or when) that happens, exchange your refresh token for a new pair — no user interaction:

curl -X POST "https://api.lexabit.com/oauth/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=rt_4Kf…" \
-d "client_id=<your-connector-client_id>" \
-d "client_secret=<your-connector-client_secret>"

The response has the same shape as Step 3: a new access_token and a new refresh_token. Rules your implementation must follow:

  • Refresh tokens are single-use. Every refresh returns a new one; always replace the stored token with the one you just received. The old access token stops working at the same moment.
  • Re-using an old refresh token revokes the connection's tokens. Lexabit treats it as a sign the token was stolen: the whole token family and the live access token are invalidated. You will need to refresh again with your newest token — or, if that one was also invalidated, send the user through consent again.
  • Idle expiry: 90 days. A refresh token that is not used for 90 days expires; each successful refresh restarts the window. An active integration never needs to re-consent; an abandoned one does.
  • It dies with the consent. Refreshing fails with invalid_grant as soon as the user revokes the connection, the connection's entity admin removes it or strips the App's roles, your Connector or the App is suspended, or (for a personal connection) the sponsoring user loses access to the entity. On invalid_grant, stop retrying and restart at Step 1.
  • Make the refresh call idempotent on your side. If a refresh request times out after Lexabit already rotated the token, retrying with the old token looks like a replay and invalidates the family — persist the new pair atomically as soon as you receive it, and on a timeout prefer re-reading your store before retrying.
  • Public (PKCE-only) clients refresh with client_id only — no secret.

Most OAuth client libraries do all of this for you once they see refresh_token in the token response; the discovery document lists refresh_token in grant_types_supported.

Disconnecting (revoke)

To let a customer disconnect from your side, revoke the refresh token (RFC 7009). The revocation endpoint acts on tokens issued through a Connector's authorization-code flow; first-party client_credentials tokens are managed through your App's credentials instead.

curl -X POST "https://api.lexabit.com/oauth/v1/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=rt_4Kf…" \
-d "token_type_hint=refresh_token" \
-d "client_id=<your-connector-client_id>" \
-d "client_secret=<your-connector-client_secret>"

Revoking a refresh token removes the whole connection — the App, its access tokens and its refresh tokens — and it disappears from the user's Connected apps. Revoking an access token (pass it as token) invalidates only that bearer; your refresh token remains valid. The endpoint always answers 200 for an authenticated client, whether or not the token was known.

Step 4: Call the API

Send the token like any other, with the scope the App holds (scopeId):

curl "https://api.lexabit.com/clients/v1" \
-H "Authorization: Bearer 1|9x7K…" \
-H "X-Scope: 42"

Don't guess or hardcode the X-Scope value: after the token exchange, call GET /scopes/v1 with your new token — it lists the scope(s) your App can act in (for a connection-born App, exactly one). Send that scopeId back verbatim on every scoped request; it's an opaque token, not the entity's UUID, and it cannot be constructed client-side.

Requesting delegated access

By default your App sees only what it was directly granted. If your product should instead see what the consenting user sees — the right model for assistants and agents working with a user — request delegated access: add the literal token delegate to the scope parameter in Step 1 (for example scope=viewer delegate). The consent screen then offers it as one option in the user's required choice about what kind of connection this is ("My own tool — seeing my view"), and GET /oauth/v1/consent/{authorizationId} reports it as delegationRequested. Your connector must be enabled for delegate mode at registration, otherwise the authorize request is rejected with error=invalid_scope. Everything else — token exchange, calling the API — is unchanged. See Delegated Access for the full model.

Discovery

Lexabit publishes an RFC 8414 discovery document, so OAuth-aware clients can find the endpoints automatically:

GET https://api.lexabit.com/.well-known/oauth-authorization-server

scopes_supported in that document is the live, authoritative list of role scopes currently grantable via consent — the set is managed by Lexabit operators at runtime, not fixed at your Connector's registration. Fine-grained capability keys and the delegate token are not yet listed there (planned); confirm those with Lexabit at registration in the meantime. The document also lists refresh_token under grant_types_supported and advertises the revocation_endpoint.

Discovery is cached

The discovery response is served with Cache-Control: public, max-age=3600. Honor that header and re-fetch rather than caching your own copy indefinitely or hardcoding a scope list — a client that respects it may still observe a policy change up to an hour after it takes effect. That lag is accepted behavior, not a bug to work around.

Policy changes

The roles (and, where applicable, capability keys) Lexabit exposes for third-party consent can change over time as operators manage platform policy. A few things follow from that:

  • owner and admin are never grantable to a Connector, regardless of policy — that floor is unconditional.
  • Narrowing is future-grants-only. If a role or capability is removed from the consentable set, connections that already hold it keep working; only new authorizations are affected.
  • A request that no longer resolves to anything fails loudly. As described in Step 1, a scope token that isn't currently grantable is dropped the same way an unrecognized one is; if that leaves nothing left to request, the authorize request fails with error=invalid_scope instead of proceeding with an empty grant. A narrower race is also handled: if a role or capability was consentable when the user reached the consent screen but is de-exposed before they approve, submitting that consent is rejected server-side with the same invalid_scope error rather than silently granting a stale scope.

Re-fetch scopes_supported rather than hardcoding a role list in your integration, and treat a shrinking grant on re-authorization as expected platform behavior, not an error in your code.

Managing connected apps (for users)

A signed-in user can see and revoke the connectors they've authorized:

# List the current user's connected apps
curl "https://api.lexabit.com/oauth/v1/consents" \
-H "Authorization: Bearer $LEXABIT_TOKEN"

# Revoke one — destroys its App and kills its access AND refresh tokens immediately
curl -X DELETE "https://api.lexabit.com/oauth/v1/consents/{consentId}" \
-H "Authorization: Bearer $LEXABIT_TOKEN"

Revoking a connection destroys the App it created (its access token stops working at once and its refresh token can no longer be used) and removes its access, while keeping the connection row as an audit record.

Where to go next