OAuth
Sign people in with Coline and call the API on their behalf.
Use OAuth when your app acts for a person: it signs them in with their Coline account, they approve what your app can do, and your app gets a token that can reach what that person can reach, limited to the scopes they approved. Coline is an OAuth 2.1 and OpenID Connect provider: authorization code flow with PKCE, dynamic client registration, client ID metadata documents, ID tokens and UserInfo.
If you're connecting an assistant to the MCP server, you don't need any of this. MCP clients register and sign in on their own.
Endpoints
| URL | |
|---|---|
| Authorization | https://coline.app/api/v1/oauth/authorize |
| Token | https://coline.app/api/v1/oauth/token |
| Registration | https://coline.app/api/v1/oauth/register |
| User info | https://coline.app/api/v1/oauth/userinfo |
| Device authorization | https://coline.app/api/v1/oauth/device/code |
| Revocation | https://coline.app/api/v1/oauth/revoke |
| JSON Web Keys | https://coline.app/.well-known/jwks.json |
| Metadata | https://coline.app/.well-known/oauth-authorization-server |
| OpenID Connect discovery | https://coline.app/.well-known/openid-configuration |
Both metadata documents are the same. A 401 from the API carries WWW-Authenticate: Bearer resource_metadata="https://coline.app/.well-known/oauth-protected-resource", so a client that starts from an API call can find everything else.
Get a client ID
There are three ways, and the person approves your app on the same consent screen with each.
Register dynamically
Apps that connect to many workspaces, such as assistants, register themselves (RFC 7591):
curl https://coline.app/api/v1/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Example Assistant",
"redirect_uris": ["https://example.com/oauth/callback"],
"token_endpoint_auth_method": "none",
"client_uri": "https://example.com",
"logo_uri": "https://example.com/logo.png"
}'{
"client_id": "col_dyn_...",
"client_id_issued_at": 1792364651,
"client_name": "Example Assistant",
"redirect_uris": ["https://example.com/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}token_endpoint_auth_methodisnone(a public client using PKCE),client_secret_basic(the default) orclient_secret_post. Confidential clients also get aclient_secret, shown once.- Redirect URIs must be HTTPS, a loopback address (
http://127.0.0.1,http://[::1]orhttp://localhost, any port), or the app's own URL scheme. - Registration grants nothing by itself. It's limited to 30 registrations per hour from one address.
Clients that register themselves, this way or with a metadata document, are treated as assistants: whatever scope they send, the person is asked to approve everything they can do in Coline, openid profile email workspace.read mail.read mail.write messages.read messages.write taskboards.read taskboards.write calendar.read calendar.write files.read files.write tools.read tools.write, and the token carries all of it. ai.invoke and apps.* are added only when asked for. Unknown scopes are ignored. The token response's scope lists what was granted.
Use a client ID metadata document
Instead of registering, a client can use an HTTPS URL that serves its metadata as its client_id. Coline reads the document on first use and refreshes it hourly. It must include client_id (the same URL) and redirect_uris, may include client_name, client_uri and logo_uri, and must allow the none token endpoint auth method. Redirects aren't followed and documents are limited to 16 KB.
Create one in the developer console
- Open the developer console, choose a workspace, and go to OAuth clients.
- Name the client and add every redirect URL it will use. Redirects must match exactly.
- Copy the client ID and client secret. The secret is shown once.
1. Send the person to authorize
Create a random code_verifier, derive code_challenge = BASE64URL(SHA256(code_verifier)), and redirect to:
https://coline.app/api/v1/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://example.com/callback
&scope=openid%20profile%20email%20workspace.read%20calendar.read
&state=RANDOM_STATE
&nonce=RANDOM_NONCE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256| Parameter | |
|---|---|
response_type | Always code |
client_id | Your client ID |
redirect_uri | One of the client's registered redirect URLs |
scope | Space-separated scopes. Defaults to openid profile email |
state | Returned unchanged; check it to prevent CSRF |
code_challenge | 43 to 128 characters |
code_challenge_method | Must be S256 |
nonce | Optional. Echoed in the ID token |
login_hint | Optional. An email address to pre-fill sign-in |
prompt | Optional. none returns login_required or consent_required, because Coline always asks before an app connects |
resource | Optional (RFC 8707). Must be a Coline URL |
The person signs in if needed and sees a consent screen with your app's name, logo and site, the account they're signing in with, and what your app will be able to do. When they approve, Coline redirects to redirect_uri with code, state and iss. Codes expire after 10 minutes and work once.
Once the client and redirect check out, every outcome goes back to your redirect, errors included, with error, error_description, state and iss (RFC 9207): access_denied when the person cancels, invalid_scope, invalid_request, invalid_target or unsupported_response_type. Compare iss with https://coline.app. An unknown client or unregistered redirect never redirects; the consent screen explains the problem instead.
2. Exchange the code
curl https://coline.app/api/v1/oauth/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=col_code_... \
-d redirect_uri=https://example.com/callback \
-d code_verifier=YOUR_CODE_VERIFIERThe token endpoint accepts form or JSON bodies. Confidential clients send the secret with HTTP Basic auth or as client_secret in the body; public clients send client_id alone.
{
"access_token": "col_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "col_rt_...",
"scope": "openid profile email workspace.read calendar.read",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}Access tokens last one hour. id_token is present when the scopes include openid.
3. Call the API
curl "https://coline.app/api/v1/workspaces/acme/calendar/events?start=2026-10-12T00:00:00Z&end=2026-10-19T00:00:00Z" \
-H "Authorization: Bearer col_at_..."A token isn't tied to one workspace. Pass the workspace slug or ID in the path; the person must be a member of it. GET /api/v1/me/workspaces (scope workspace.read) lists their workspaces.
The exception is apps.read and apps.write: the consent screen asks the person to pick a workspace they administer, and the token works only there. Routes without a workspace in the path, such as the Sites API, use that workspace; a path naming another one fails with OAUTH_WORKSPACE_MISMATCH. If the person stops administering the workspace, the token loses those scopes.
Refresh
curl https://coline.app/api/v1/oauth/token \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d grant_type=refresh_token \
-d refresh_token=col_rt_...Each refresh returns a new access token, a new refresh token, and a new ID token for openid grants. The old refresh token stops working, so store the new one. A refresh token unused for 90 days expires; the person then connects your app again.
ID tokens
ID tokens are JWTs signed with RS256. Verify them with the keys at https://coline.app/.well-known/jwks.json (match the kid header) and check:
| Claim | |
|---|---|
iss | https://coline.app |
aud | Your client ID |
sub | The person's stable user id |
exp, iat | Valid for one hour |
auth_time | When the person last signed in, when known |
nonce | The nonce from your authorization request |
email, email_verified | With the email scope |
name, given_name, family_name, picture, preferred_username | With the profile scope |
User info
GET or POST /api/v1/oauth/userinfo with the access token returns the signed-in person, limited to the identity scopes the token carries:
{
"sub": "user id",
"email": "dana@example.com",
"email_verified": true,
"name": "Dana Kim",
"given_name": "Dana",
"family_name": "Kim",
"picture": "https://...",
"preferred_username": "dana",
"scope": "openid profile email workspace.read",
"coline_workspace": { "id": "...", "slug": "acme", "name": "Acme" }
}email and email_verified need the email scope; the name fields need profile. coline_workspace is present only for a token bound to one workspace (apps.* scopes). An invalid token gets 401 with WWW-Authenticate: Bearer error="invalid_token".
Revoke
curl https://coline.app/api/v1/oauth/revoke \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d token=col_rt_...Revoking either token of a pair ends both (RFC 7009). The endpoint answers 200 for unknown tokens too. Public clients send client_id in the body.
People can also disconnect your app themselves under Settings → Account → Connections → Apps with access. That ends every token your app holds for them.
Devices without a browser
coline-app login uses the device authorization grant (RFC 8628) when no browser can open: it posts to the device authorization endpoint, shows the person a code, and polls the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code while they approve it at https://coline.app/device. Polls before approval answer authorization_pending, too-frequent polls slow_down, and a denial access_denied. Codes expire after 15 minutes. Device sign-in is open to Coline's own clients only for now.
Errors
The token, revocation and registration endpoints answer with standard OAuth errors, { "error": "...", "error_description": "..." }, and Cache-Control: no-store:
| Error | Meaning |
|---|---|
invalid_request | A parameter is missing or invalid |
invalid_client | Unknown client, or the secret doesn't match (401) |
invalid_grant | The code, verifier or refresh token is wrong, expired, used, or for another client or redirect |
invalid_scope | A scope isn't supported or allowed for this client |
unsupported_grant_type | Use authorization_code, refresh_token or the device code grant |
invalid_redirect_uri, invalid_client_metadata | Registration was refused |
API routes use Coline's error shape:
| Code | Meaning |
|---|---|
INVALID_TOKEN | The access token is unknown or expired; refresh it |
OAUTH_SCOPE_FORBIDDEN | The token lacks a scope the route needs |
OAUTH_WORKSPACE_MISMATCH | The token is bound to another workspace (apps.* scopes) |
WORKSPACE_REQUIRED | apps.* scopes need a workspace picked on the consent screen |