The OAuth and
.well-known endpoints sit at the root of api.keebai.com, not under /v1. The metadata and signing-key endpoints answer CORS requests from any origin. The token and revocation endpoints answer them only for public clients, and only from the origin of one of the client’s registered redirect URIs.
Discovery
1
Call the server without a token
POST /v1/mcp without Authorization, or with an invalid or expired token, returns 401 with a pointer to the resource metadata:2
Read the protected resource metadata
GET https://api.keebai.com/.well-known/oauth-protected-resource/v1/mcp (also served at /.well-known/oauth-protected-resource):3
Read the authorization server metadata
GET https://api.keebai.com/.well-known/oauth-authorization-server (the same document is served at /.well-known/openid-configuration):scopes_supported lists every scope a connection can be granted: the active scopes that belong to a permission group, plus offline_access. The user approves them in groups, not one by one. Both documents read the list from the same catalog, which is cached for up to one minute.
Identify your client
You have two options. Neither needs anyone at Keebai to do anything.- Client ID metadata document
- Dynamic client registration
Host a JSON document over HTTPS and use its URL as
client_id. Keebai fetches it when the user starts the flow.client_idinside the document must equal the URL it is served from, andredirect_urisis required.- The client is public: set
token_endpoint_auth_methodtonone. Shared-secret methods,client_secretandclient_secret_expires_atare rejected in a metadata document. - The URL must be
httpson the default port, with a domain name as host. IP addresses,localhostand hosts ending in.local,.localhost,.internal,.lanor.home.arpaare rejected without being fetched, and a host that resolves to a private or special-use address is refused when Keebai connects. - The document must answer
200within 2.5 seconds and weigh at most 5 KB. Redirects are not followed. client_uri,logo_uriand the other URL fields must behttps.- Keebai caches the document for the
max-ageof itsCache-Controlheader, between 30 seconds and one day; without the header, for 30 seconds.
Redirect URIs
- Matching is exact, character for character. The one exception is loopback: any port is accepted, as RFC 8252 requires for native apps. Register the URI with or without a port and use whichever port you get at runtime.
httpis allowed only for loopback hosts.- A private-use scheme must be in reverse-domain form (
com.example.agent).cursor:is the only other scheme accepted. javascript:,data:,file:,blob:,about:,vbscript:,ftp:,ws:andwss:are always rejected.
400 invalid_redirect_uri.
Authorize
Send the user’s browser to:
PKCE with
S256 is mandatory for every client, public or confidential.
offline_access is accepted and ignored. Whether you get a refresh token depends only on the client allowing the refresh_token grant, which it does by default.
Keebai sends the browser to accounts.keebai.com, where the user signs in, chooses a company and approves permission groups. The granted scopes are the scopes your request asked for that belong to an approved group and that the user’s role allows. The user has 10 minutes to finish.
The redirect back
On approval:iss against https://api.keebai.com before using the code (RFC 9207). It is always present, on success and on error.
If the client_id or the redirect_uri is invalid — an unknown client, a metadata document that cannot be fetched or fails validation, a redirect URI that is not registered — Keebai does not redirect to your client, because it cannot trust the address. The user sees an error page on accounts.keebai.com instead. Every other error goes back to your redirect_uri with error set.
The code is single-use and expires after 60 seconds.
Exchange the code
POST https://api.keebai.com/oauth/token, as application/x-www-form-urlencoded.
redirect_uri must be the same one you sent to /oauth/authorize, and resource, if sent, must be the MCP resource. Confidential clients from dynamic registration also authenticate, with HTTP Basic (client_secret_basic) or client_secret in the body (client_secret_post), whichever they registered.
200 OK
Cache-Control: no-store. Read scope to know what the user actually granted — it can be less than you asked for.
Then call the server:
/v1/mcp. The rest of the public API rejects it with 401; use a Personal Access Token there.
Refresh
scope is optional and can only narrow what the connection already has.
If two parts of your client refresh at the same time, one of them presents an already-used token and both lose the connection. Serialize refreshes.
Every refresh checks the user again. If their account was deactivated, or they no longer have access to the company of the connection, the refresh fails with invalid_grant.
Tokens
The connection itself lasts one year from the user’s last approval. After that, refreshing fails with
invalid_grant and the user signs in again.
The access token is a JWT for a single audience, https://api.keebai.com/v1/mcp, issued by https://api.keebai.com and signed with a key published at /oauth/jwks. It carries sub (the Keebai user), client_id, scope, company, project_id, email, grant_id (the connection), jti, iat and exp.
Your client does not need to decode it — treat it as opaque, and read the granted scopes from the token response. Keebai does not store access tokens, so they cannot be revoked one by one: ending a connection stops refreshes, and the access token still in hand expires on its own.
Revoke
POST https://api.keebai.com/oauth/revoke with token=<refresh token>, as application/x-www-form-urlencoded, authenticating the same way as on the token endpoint. Revoking a refresh token ends the whole connection: every refresh token issued from that sign-in stops working.
200 with an empty body, whether or not the token existed. Call it when the user disconnects in your app. Access tokens are JWTs and cannot be revoked here — sending one returns 400 unsupported_token_type. The one in hand expires within 15 minutes.
Users can also disconnect from the portal, under Developers › Connected apps (Desarrolladores › Apps conectadas). That deletes the connection with its refresh tokens: the next refresh fails with invalid_grant, and the current access token keeps working until it expires, at most 15 minutes later.
Errors
Authorize
Errors that Keebai can send back to your client arrive on theredirect_uri as error and error_description query parameters, with state and iss.
An unknown
client_id, a metadata document that cannot be fetched or is invalid, and an unregistered redirect_uri are shown to the user on an error page at accounts.keebai.com. Your client receives nothing.
Token, registration and revocation
Errors are JSON, following RFC 6749:On /v1/mcp
A tool call that lacks a scope does not change the HTTP status: it returns
200 with a tool result carrying isError: true and INSUFFICIENT_SCOPE, as described in how scopes are enforced.
What a connection can reach
- One company. The user picks it on the consent screen. Like a Personal Access Token, the token decides the tenant, and nothing your client sends can change it. Writes go to the company’s default project.
- The user’s own reach. The groups and scopes the user can approve are limited by their Keebai role.
- One connection per client, user and company. If the same user authorizes your client again for the same company, the new approval replaces the previous scopes.