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:
| Object | What it is |
|---|---|
| Connector | Your product's OAuth doorway — a client registration (client_id, redirect URIs, branding). Registered once per product. Holds no access by itself. |
| Connection | The record that a user authorized your Connector on a specific entity — the audit + revocation ledger. |
| App | The 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.) |
| Token | The 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.
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.
Develop and test your integration against staging — https://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.
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_grantas 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. Oninvalid_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_idonly — 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.
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:
ownerandadminare 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
scopetoken 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 witherror=invalid_scopeinstead 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 sameinvalid_scopeerror 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
- Connecting an MCP — MCP clients use exactly this flow.
- Apps & API Keys — the first-party side, and what an "App" is.
- API Reference → OAuth & Connectors.