Reference
OAuth
Sign in with Tabbio: the authorization code flow with PKCE, plus token refresh, revocation and introspection.
Authorization server metadata
/.well-known/oauth-authorization-serverThe RFC 8414 discovery document. Standard OAuth clients read this to find the authorize, token, revocation and introspection endpoints, so you rarely need to hardcode them. Returned as plain JSON, not the API envelope.
Response
| Field | Type |
|---|---|
| issueralways | string |
| authorization_endpointalways | string |
| token_endpointalways | string |
| revocation_endpointalways | string |
| introspection_endpointalways | string |
| userinfo_endpointalways | string |
| scopes_supportedalways | string[] |
| response_types_supportedalways | string[] |
| grant_types_supportedalways | string[] |
| code_challenge_methods_supportedalways | string[] |
| token_endpoint_auth_methods_supportedalways | string[] |
| service_documentationalways | string |
Status codes
- 200
The metadata document.
curl https://server.tabbio.com/.well-known/oauth-authorization-serverconst response = await fetch("https://server.tabbio.com/.well-known/oauth-authorization-server");
const payload = await response.json();import requests
response = requests.get("https://server.tabbio.com/.well-known/oauth-authorization-server")
response.raise_for_status()
payload = response.json(){
"issuer": "<issuer>",
"authorization_endpoint": "<authorization_endpoint>",
"token_endpoint": "<token_endpoint>",
"revocation_endpoint": "<revocation_endpoint>",
"introspection_endpoint": "<introspection_endpoint>",
"userinfo_endpoint": "<userinfo_endpoint>",
"scopes_supported": [
"<scopes_supported>"
],
"response_types_supported": [
"<response_types_supported>"
],
"grant_types_supported": [
"<grant_types_supported>"
],
"code_challenge_methods_supported": [
"<code_challenge_methods_supported>"
],
"token_endpoint_auth_methods_supported": [
"<token_endpoint_auth_methods_supported>"
],
"service_documentation": "<service_documentation>"
}Start the authorization flow
/oauth/authorizeSend the user’s browser here to begin "Sign in with Tabbio". The endpoint redirects to the Tabbio consent screen with every parameter intact; the user signs in if needed, chooses which CV to share and what to include, and is sent back to your redirect_uri with code and state. PKCE is required for every client, public and confidential.
Query parameters
| Field | Type | Description |
|---|---|---|
| response_typerequired | string | Always One of |
| client_idrequired | string | Your app’s client id ( |
| redirect_urirequired | string | One of the redirect URIs registered for the app, matched exactly. |
| scoperequired | string | Space separated list of: profile, email, cv:read, cv:contact. |
| state | string | Opaque value echoed back verbatim. Use it to defeat CSRF. |
| code_challengerequired | string | BASE64URL(SHA256(code_verifier)). PKCE is required for every client. |
| code_challenge_methodrequired | string | Always One of |
Status codes
- 302
Redirect to the Tabbio consent screen.
curl "https://server.tabbio.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=$SCOPE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"const response = await fetch(`https://server.tabbio.com/oauth/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`);
const payload = await response.json();import requests
response = requests.get(f"https://server.tabbio.com/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&code_challenge={code_challenge}&code_challenge_method=S256")
response.raise_for_status()
payload = response.json()Introspect a token
/oauth/introspectRFC 7662. Only the app that owns the token learns anything about it; every other case answers { "active": false }. Like the token endpoint, this answers the flat RFC body rather than the API envelope, with active at the top level and Cache-Control: no-store, and reports errors in the RFC 6749 { error, error_description } shape.
Request body
Sent as application/x-www-form-urlencoded.
| Field | Type | Description |
|---|---|---|
| tokenrequired | string | The access or refresh token to act on. |
| token_type_hint | string | |
| client_id | string | |
| client_secret | string |
Response
| Field | Type | Description |
|---|---|---|
| activealways | boolean | |
| scope | string | |
| client_id | string | |
| token_type | string | One of |
| sub | string | The Tabbio user id. |
| exp | integer or null | Expiry as a Unix timestamp. |
| connection_id | string |
Status codes
- 200
The token state, as the flat RFC 7662 body.
- 400
The request was malformed.
- 401
Client authentication failed.
curl -X POST https://server.tabbio.com/oauth/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'token=tbo_at_9QpX2fR8kLm4TnQ7vWxZ1bC'const response = await fetch(
"https://server.tabbio.com/oauth/introspect",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
"token": "tbo_at_9QpX2fR8kLm4TnQ7vWxZ1bC"
}),
},
);
const payload = await response.json();import requests
body = {
"token": "tbo_at_9QpX2fR8kLm4TnQ7vWxZ1bC",
}
response = requests.post("https://server.tabbio.com/oauth/introspect", data=body)
response.raise_for_status()
payload = response.json()A live token this app owns.
{
"active": true,
"scope": "profile email cv:read cv:contact",
"client_id": "tbo_ci_2Kd81aQ7vXpL9mNr3TzW",
"token_type": "Bearer",
"sub": "usr_2f8a91",
"exp": 1788000000,
"connection_id": "con_9d21f4"
}Unknown, expired, revoked, or a live token that belongs to another app.
{
"active": false
}Revoke a token
/oauth/revokeRFC 7009. Revoking a refresh token also revokes its rotation family, the connection’s live access tokens and its durable CV links, so a partner ending the session keeps no bearer PDF URL; revoking an access token revokes only that token. Always answers success, even for a token that was already unknown. RFC 7009 specifies no success body and tells clients to ignore whatever arrives with the 200, so this endpoint keeps the standard Tabbio { data, error, meta } envelope, unlike the token and introspection endpoints, which answer the flat OAuth bodies their RFCs define.
Request body
Sent as application/x-www-form-urlencoded.
| Field | Type | Description |
|---|---|---|
| tokenrequired | string | The access or refresh token to act on. |
| token_type_hint | string | |
| client_id | string | |
| client_secret | string |
Response
These fields sit inside data.
| Field | Type | Description |
|---|---|---|
| revokedalways | boolean | One of |
Status codes
- 200
The token is no longer usable. The body is the API envelope; RFC 7009 clients may ignore it.
- 400
The request was malformed.
- 401
Client authentication failed.
curl -X POST https://server.tabbio.com/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'token=tbo_rt_4Kd81aQ7vXpL9mNr3TzWyB6'const response = await fetch(
"https://server.tabbio.com/oauth/revoke",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
"token": "tbo_rt_4Kd81aQ7vXpL9mNr3TzWyB6"
}),
},
);
const payload = await response.json();import requests
body = {
"token": "tbo_rt_4Kd81aQ7vXpL9mNr3TzWyB6",
}
response = requests.post("https://server.tabbio.com/oauth/revoke", data=body)
response.raise_for_status()
payload = response.json(){
"data": {
"revoked": true
},
"error": null,
"meta": null
}Exchange a code or refresh a token
/oauth/tokenThe RFC 6749 token endpoint. Authenticate with HTTP Basic (client_id:client_secret) or with the credentials in the body; a public client sends client_id alone. Refresh tokens rotate on every use: the old one is revoked, and presenting it again revokes the whole family, so store the new one before you drop the old one. This endpoint answers the flat OAuth body, not the API envelope.
Request body
Sent as application/x-www-form-urlencoded.
| Field | Type | Description |
|---|---|---|
| grant_typerequired | string |
One of |
| code | string | The authorization code, for |
| redirect_uri | string | The redirect URI from the authorization request. Must match exactly. |
| code_verifier | string | The PKCE verifier whose challenge was sent to the authorize endpoint. |
| refresh_token | string | The refresh token, for |
| scope | string | Optional on refresh, and may only narrow the granted scope. |
| client_id | string | Required unless the client authenticates with HTTP Basic. |
| client_secret | string | Confidential clients only. Prefer HTTP Basic. |
Response
| Field | Type | Description |
|---|---|---|
| access_tokenalways | string | |
| token_typealways | string | One of |
| expires_inalways | integer | Seconds until the access token expires. |
| refresh_tokenalways | string | |
| scopealways | string | The scopes actually granted, space separated. |
Status codes
- 200
A new access and refresh token pair.
- 400
The grant was refused.
- 401
Client authentication failed.
curl -X POST https://server.tabbio.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=authorization_code&code=tbo_ac_yLh0dJ8Q1s5B&redirect_uri=https%3A%2F%2Fpartner.example%2Fcallback&code_verifier=M25iVXpKU3puUjFaYWg3T1NDTDQtcW1ROUY5YXlwalNoc0hhakxifmZH'const response = await fetch(
"https://server.tabbio.com/oauth/token",
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
"grant_type": "authorization_code",
"code": "tbo_ac_yLh0dJ8Q1s5B",
"redirect_uri": "https://partner.example/callback",
"code_verifier": "M25iVXpKU3puUjFaYWg3T1NDTDQtcW1ROUY5YXlwalNoc0hhakxifmZH"
}),
},
);
const payload = await response.json();import requests
body = {
"grant_type": "authorization_code",
"code": "tbo_ac_yLh0dJ8Q1s5B",
"redirect_uri": "https://partner.example/callback",
"code_verifier": "M25iVXpKU3puUjFaYWg3T1NDTDQtcW1ROUY5YXlwalNoc0hhakxifmZH",
}
response = requests.post("https://server.tabbio.com/oauth/token", data=body)
response.raise_for_status()
payload = response.json(){
"access_token": "tbo_at_9QpX2f",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tbo_rt_4Kd81a",
"scope": "profile email cv:read"
}Base URL https://server.tabbio.com. Every response outside the token endpoint uses the { data, error, meta } envelope.