Skip to main content

Partner Single Sign-On

Using the single sign-on for partners, you may log in a specific user to Tidely, without requiring the user to go through a log-in form.
The single sign-on consists of a two-leg flow:

  1. Your backend calls POST /partner/authentication/sso/init to obtain a short-lived init token for a Tidely user. This call is authenticated with your normal partner OAuth 2.0 access token.
  2. Your frontend passes the init token to the embedded Tidely iframe via postMessage. Tidely's frontend exchanges the init token for an access + refresh token pair on Tidely's backend, and the user is then logged in.

The init token is a single-use, short-lived bearer token. It must never be exposed beyond the iframe handshake — treat it like a password.

Sequence

The full message protocol used in steps 7–10 is described in Frontend Messaging.

Endpoint: POST /partner/authentication/sso/init

Mints an init token for a Tidely user that the partner is authorized to act on behalf of.

Request

POST /partner/v2/authentication/sso/init HTTP/1.1
Host: api.tidely.com
Authorization: Bearer YOUR_PARTNER_ACCESS_TOKEN
X-Account-Id: 12345
Content-Type: application/json

{
"userId": 67890
}
HeaderRequiredDescription
AuthorizationYesOAuth 2.0 bearer token from the client credentials flow.
X-Account-IdYesThe Tidely account (tenant) the user belongs to.
Content-TypeYesapplication/json.
Body fieldTypeRequiredDescription
userIdintegerYesThe Tidely user ID to log in. Must belong to X-Account-Id.

Response — 200 OK

{
"initToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2Nzg5MCIsImFjY291bnRJZCI6MTIzNDUsImlzcyI6Imh0dHBzOi8vYXV0aC50aWRlbHkuY29tIiwiYXVkIjoidGlkZWx5LWZyb250ZW5kIiwiaWF0IjoxNzMwOTk5OTk5LCJleHAiOjE3MzEwMDAwNTksImp0aSI6IjAxOTI4ZTUwLTAwMDAtNzAwMC04MDAwLWFiY2RlZjAxMjM0NSJ9...",
"expiresIn": 60,
"expiresAt": "2026-05-08T12:34:56Z"
}
FieldTypeDescription
initTokenstringThe init token. Pass this to the iframe via postMessage.
expiresInintegerSeconds until the token expires. Always between 30 and 60.
expiresAtstringAbsolute expiry, ISO 8601 (UTC).

Token Properties

  • Format: signed JWT (RS256). The signature is verified by Tidely's backend; partners do not need to validate it themselves.
  • Lifetime: 30–60 seconds. Mint a fresh token every time you load the iframe; do not cache.
  • Single-use: the token is consumed on exchange. A second exchange attempt returns TOKEN_ALREADY_USED.
  • Audience: bound to the Tidely frontend. It cannot be used as an API access token.

For reference, the JWT payload contains:

{
"iss": "https://auth.tidely.com",
"aud": "tidely-frontend",
"sub": "67890",
"accountId": 12345,
"partnerId": "your-partner-client-id",
"iat": 1730999999,
"exp": 1731000059,
"jti": "01928e50-0000-7000-8000-abcdef012345"
}

The exact set of claims may grow over time. Treat unknown claims as opaque.

Error Responses

All errors follow the standard ExceptionDetails shape. The error field carries a stable, machine-readable code so partners can react programmatically without parsing the human-readable message. The HTTP status is on the response status line (not duplicated in the body).

{
"timestamp": 1730999999000,
"error": "USER_NOT_FOUND",
"message": "User 67890 does not belong to account 12345"
}
HTTPerrorMeaningWhat to do
400INVALID_REQUESTBody malformed or userId missing/non-numeric.Fix the request payload.
400MISSING_ACCOUNT_HEADERX-Account-Id header missing.Always send X-Account-Id (see Tenant Context).
401INVALID_CREDENTIALSPartner access token missing, expired, or invalid.Refresh your OAuth 2.0 access token.
403SSO_NOT_ENABLEDThe partner does not have the embedded SSO feature enabled.Contact partnerships@tidely.com to enable embedded integration.
404USER_NOT_FOUNDThe userId does not exist, or the account/user is not accessible to you.Check that the user has been provisioned in the target account. (Returned instead of 403 so existence cannot be probed.)
409USER_INACTIVEThe user exists but is disabled, deleted, or otherwise blocked from login.Reactivate the user or pick a different one.
429RATE_LIMITEDToo many init-token requests for this partner.Back off and retry. The Retry-After header gives the wait time.
500INTERNAL_ERRORUnexpected server error.Retry with backoff; if it persists, contact support.

error codes are part of the API contract — they will not be renamed within a major version. New codes may be added, so treat unknown codes as a generic failure.

Code Example — Minting an Init Token

curl -X POST https://api.tidely.com/partner/v2/authentication/sso/init \
-H "Authorization: Bearer YOUR_PARTNER_ACCESS_TOKEN" \
-H "X-Account-Id: 12345" \
-H "Content-Type: application/json" \
-d '{ "userId": 67890 }'
// Server-side (Node.js). Never call this from the browser.
async function mintInitToken({ partnerAccessToken, accountId, userId }) {
const res = await fetch('https://api.tidely.com/partner/v2/authentication/sso/init', {
method: 'POST',
headers: {
Authorization: `Bearer ${partnerAccessToken}`,
'X-Account-Id': String(accountId),
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
});

if (!res.ok) {
const err = await res.json();
throw new Error(`${err.error}: ${err.message}`);
}

return res.json(); // { initToken, expiresIn, expiresAt }
}

Security Notes

  • Mint server-side only. POST /partner/authentication/sso/init requires your partner client credentials. Never call it from a browser.
  • Deliver the init token over HTTPS only, both from your backend to your frontend and from your frontend into the iframe.
  • Mint per-load. Always mint a fresh init token when (re)loading the iframe. Do not reuse, persist, or pre-mint tokens — they expire fast and are single-use.
  • Pin targetOrigin. When you postMessage the token to the iframe, always pass the exact iframe origin (e.g. https://app.tidely.com) — never *. See Frontend Messaging.
  • One token per user. Each init token is bound to a single Tidely user and account. Do not attempt to switch users by re-using a token.

Next Steps

Frontend Messaging — how the parent page hands the init token to the iframe