For Node.js and TypeScript projects, the cliodot npm package (v1.2.6+) provides OAuthAppClient — a typed wrapper around the public and server-to-server HTTP endpoints documented above.
OAuthAppClient is for developer integration only. It does not manage OAuth Apps, provider templates, portal connections, or webhooks. Configure those in the Cliodot portal.
HTTP integration remains fully supported and is the canonical API contract.
Install#
npm install cliodot
Client setup#
import { OAuthAppClient } from "cliodot";
const client = new OAuthAppClient({
baseUrl: "https://api.cliodot.com",
appId: "oauth_app_507f1f77bcf86cd799439011",
appApiKey: "YOUR_APP_API_KEY",
// appSecret: "a1b2c3d4...", // alternative to appApiKey
// debug: false,
});
| Config field | Required | Description |
|---|---|---|
baseUrl |
yes | Public API origin without trailing slash. SDK calls {baseUrl}/oauth/... |
appId |
yes | OAuth app _id from the portal |
appApiKey |
no* | Sends Authorization: Bearer oak_... on S2S calls |
appSecret |
no* | Sends X-Cliodot-App-Id + X-Cliodot-App-Secret on S2S calls |
debug |
no | Reserved for diagnostic logging (default false) |
One of appApiKey or appSecret is required for exchange and connections.*. Connect methods (connect.start, connect.poll, connect.buildUrl) do not send credentials.
Method overview#
| Method | Auth | HTTP equivalent |
|---|---|---|
client.connect.start(input) |
None | POST /oauth/apps/:appId/connect |
client.connect.poll(input) |
None | POST /oauth/apps/:appId/connect/poll |
client.connect.buildUrl(input) |
None | GET /oauth/apps/:appId/connect (URL builder, no request) |
client.connect.isSamlPostBinding(response) |
— | Local helper |
client.connect.buildSamlPostForm(response) |
— | Local helper |
client.exchange(input) |
S2S | POST /oauth/token/exchange |
client.connections.get(connectionId) |
S2S | GET /oauth/connections/:connectionId |
client.connections.getToken(connectionId) |
S2S | GET /oauth/connections/:connectionId/token |
client.connections.sync(connectionId) |
S2S | Alias of getToken |
client.connections.refresh(connectionId) |
S2S | POST /oauth/connections/:connectionId/refresh |
client.connections.revoke(connectionId, input?) |
S2S | POST /oauth/connections/:connectionId/revoke |
client.connections.delete(connectionId) |
S2S | DELETE /oauth/connections/:connectionId |
client.connect.start(input)#
Starts an OAuth connect flow programmatically. Maps to POST /oauth/apps/:appId/connect. No authentication headers are sent.
Input (OAuthConnectStartInput)
| Field | Required | Type | Description |
|---|---|---|---|
provider |
yes | string | Provider alias attached to your OAuth App (e.g. google) |
redirect_uri |
yes | string | Customer callback URL; must be in the app's allowlist |
scope |
no | string | Space-delimited scopes (alternative to scopes) |
scopes |
no | string[] | Scope list; sent as scopes in POST body and joined as scope in GET URL |
state |
no | string | CSRF token echoed on customer callback |
external_user_id |
no | string | Your user key stored on the connection |
connection_storage |
no | "persistent" |
"ephemeral" |
appId |
no | string | Override the client's default appId for this call |
assertion |
no | string | JWT or SAML2 assertion for bearer grant types |
subject_token |
no | string | Subject token for RFC 8693 token exchange |
subject_token_type |
no | string | Token type URI for subject_token |
requested_token_type |
no | string | Requested issued token type |
actor_token |
no | string | Actor token for delegation |
actor_token_type |
no | string | Token type URI for actor_token |
audience |
no | string | Target audience for token exchange |
Returns (OAuthConnectStartResponse)
| Field | When present |
|---|---|
protocol |
Always on success (oauth2, oidc, saml) |
grant_type |
OAuth2/OIDC flows |
authorization_url |
Authorization code / OIDC redirect flows |
sso_binding, saml_request, relay_state |
SAML flows |
device_code, user_code, verification_uri, verification_uri_complete, poll_interval, expires_in, poll_state |
Device authorization grant |
exchange_code, connection_id, redirect_url |
Immediate grants (client_credentials, assertion grants, token exchange) |
Example — authorization code
const start = await client.connect.start({
provider: "google",
redirect_uri: "https://friendsconnect.com/auth/callback",
scopes: ["openid", "email", "profile"],
state: "csrf-token-from-your-app",
external_user_id: "user_12345",
});
window.location.href = start.authorization_url!;
Example — client credentials (immediate)
import { isImmediateConnectResponse } from "cliodot";
const immediate = await client.connect.start({
provider: "m2m",
redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (isImmediateConnectResponse(immediate)) {
const exchanged = await client.exchange({
code: immediate.exchange_code!,
connection_id: immediate.connection_id!,
});
}
Example — JWT bearer
await client.connect.start({
provider: "m2m-api",
redirect_uri: "https://friendsconnect.com/auth/callback",
assertion: "eyJhbGciOiJSUzI1NiIs...",
});
client.connect.poll(input)#
Polls device authorization until complete. Maps to POST /oauth/apps/:appId/connect/poll. Retries automatically on 428 / authorization_pending using the interval below.
Input (OAuthConnectPollInput)
| Field | Required | Type | Default | Description |
|---|---|---|---|---|
provider |
yes | string | — | Same provider alias used in connect.start |
poll_state |
yes | string | — | poll_state from the device connect response |
appId |
no | string | client appId |
Override app ID for this poll |
pollIntervalMs |
no | number | 5000 |
Milliseconds to wait between 428 retries |
maxAttempts |
no | number | 120 |
Maximum poll attempts before timeout |
Returns (OAuthConnectPollResponse)
| Field | Description |
|---|---|
exchange_code |
Short-lived code to redeem via client.exchange |
connection_id |
Connection ID paired with the exchange code |
redirect_url |
Optional pre-built customer callback URL |
Example
import { isDeviceConnectResponse } from "cliodot";
const device = await client.connect.start({
provider: "cli-tv",
redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (isDeviceConnectResponse(device)) {
const done = await client.connect.poll({
provider: "cli-tv",
poll_state: device.poll_state!,
pollIntervalMs: (device.poll_interval ?? 5) * 1000,
});
await client.exchange({
code: done.exchange_code!,
connection_id: done.connection_id!,
});
}
client.connect.buildUrl(input)#
Builds a browser redirect URL for GET /oauth/apps/:appId/connect. Accepts the same fields as connect.start except grant-specific body fields (assertion, subject_token, etc.) — those require connect.start (POST).
| Field | Required | Notes |
|---|---|---|
provider |
yes | |
redirect_uri |
yes | |
scope / scopes |
no | Joined into a single scope query param |
state |
no | |
external_user_id |
no | |
connection_storage |
no | |
appId |
no |
const url = client.connect.buildUrl({
provider: "google",
redirect_uri: "https://friendsconnect.com/auth/callback",
scopes: ["openid", "email", "profile"],
state: "csrf-token",
});
window.location.href = url;
client.connect.isSamlPostBinding(response) / client.connect.buildSamlPostForm(response)#
Helpers for SAML POST binding after connect.start.
| Method | Returns |
|---|---|
isSamlPostBinding(response) |
true when sso_binding === "post" and SAML fields are present |
buildSamlPostForm(response) |
{ action, samlRequest, relayState } or null |
const saml = await client.connect.start({
provider: "google-sso",
redirect_uri: "https://friendsconnect.com/auth/callback",
});
if (client.connect.isSamlPostBinding(saml)) {
const form = client.connect.buildSamlPostForm(saml)!;
}
client.exchange(input)#
Redeems an exchange code server-side. Maps to POST /oauth/token/exchange. Requires S2S credentials.
Input (OAuthExchangeInput)
| Field | Required | Type | Description |
|---|---|---|---|
code |
yes | string | Exchange code (ex_...) from connect callback or immediate grant |
connection_id |
yes | string | Connection ID paired with the code |
Returns (OAuthExchangeResponse)
| Field | When included |
|---|---|
connection |
Always — sanitized connection metadata |
protocol |
OAuth2, OIDC, or SAML |
oauth |
Provider token/userinfo/revocation URLs when available |
access_token, refresh_token, token_type, expires_at, scope, id_token |
Per app token_exposure_policy and connection storage |
name_id, saml_attributes, assertion_expires_at |
SAML connections |
provider_token_response |
Raw provider token payload when policy allows |
const result = await client.exchange({
code: "ex_a1b2c3d4e5f6789012345678901234567890ab",
connection_id: "conn_507f1f77bcf86cd799439014",
});
client.connections.get(connectionId)#
Returns sanitized connection metadata. Maps to GET /oauth/connections/:connectionId.
| Parameter | Required | Type |
|---|---|---|
connectionId |
yes | string |
Returns: { ok: true, connection: OAuthSanitizedConnection }
const { connection } = await client.connections.get("conn_507f1f77bcf86cd799439014");
client.connections.getToken(connectionId) / client.connections.sync(connectionId)#
Fetches access token (and related fields per policy). Maps to GET /oauth/connections/:connectionId/token. sync is an alias for getToken.
| Parameter | Required | Type |
|---|---|---|
connectionId |
yes | string |
Returns (OAuthTokenResponse)
| Field | When included |
|---|---|
connection |
Optional sanitized connection |
access_token, refresh_token, token_type, expires_at, scope, id_token |
Per token_exposure_policy |
name_id, saml_attributes, assertion_expires_at |
SAML connections |
oauth |
Provider URLs when available |
const token = await client.connections.getToken("conn_507f1f77bcf86cd799439014");
const synced = await client.connections.sync("conn_507f1f77bcf86cd799439014");
client.connections.refresh(connectionId)#
Refreshes tokens using the stored refresh token. Maps to POST /oauth/connections/:connectionId/refresh.
| Parameter | Required | Type |
|---|---|---|
connectionId |
yes | string |
Returns (OAuthConnectionWithTokenResponse): connection plus refreshed token fields.
const refreshed = await client.connections.refresh("conn_507f1f77bcf86cd799439014");
client.connections.revoke(connectionId, input?)#
Revokes a connection at the provider when supported. Maps to POST /oauth/connections/:connectionId/revoke.
| Parameter | Required | Type | Description |
|---|---|---|---|
connectionId |
yes | string | |
input.reason |
no | string | Optional audit reason |
Returns: { ok: true, revoked: true }
await client.connections.revoke("conn_507f1f77bcf86cd799439014", {
reason: "user_logged_out",
});
client.connections.delete(connectionId)#
Deletes the connection record. Maps to DELETE /oauth/connections/:connectionId.
| Parameter | Required | Type |
|---|---|---|
connectionId |
yes | string |
Returns: { ok: true, deleted: true }
await client.connections.delete("conn_507f1f77bcf86cd799439014");
Response type guards and utilities#
Import from cliodot alongside OAuthAppClient:
| Export | Purpose |
|---|---|
isBrowserAuthorizationResponse(response) |
true when authorization_url is set (redirect to IdP) |
isDeviceConnectResponse(response) |
true for device-code grant with poll_state and device_code |
isImmediateConnectResponse(response) |
true when exchange_code and connection_id are returned immediately |
isDeviceAuthorizationPendingError(err) |
true for 428 / authorization_pending poll errors |
isSamlProtocol(protocol) |
true when protocol is saml |
isOidcProtocol(protocol) |
true when protocol is oidc |
normalizeOAuthGrantType(grantType) |
Normalizes grant type strings to canonical values |
pollDeviceConnectWithBackoff(client, input) |
Standalone poll helper with the same retry semantics as connect.poll |
import {
OAuthAppClient,
isImmediateConnectResponse,
isDeviceConnectResponse,
pollDeviceConnectWithBackoff,
} from "cliodot";
Error handling#
SDK methods throw CliodotApiError on failure with message, status, and code (when the API returns one).
import { CliodotApiError } from "cliodot";
try {
await client.exchange({ code: "ex_bad", connection_id: "conn_1" });
} catch (err) {
if (err instanceof CliodotApiError) {
console.error(err.code);
}
}
Common codes: OAUTH_EXCHANGE_CODE_INVALID, OAUTH_UNAUTHORIZED_APP, OAUTH_CONNECTION_REVOKED, OAUTH_CONNECTION_EXPIRED, OAUTH_PROVIDER_ERROR.