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:
- Your backend calls
POST /partner/authentication/sso/initto obtain a short-lived init token for a Tidely user. This call is authenticated with your normal partner OAuth 2.0 access token. - 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
}
| Header | Required | Description |
|---|---|---|
Authorization | Yes | OAuth 2.0 bearer token from the client credentials flow. |
X-Account-Id | Yes | The Tidely account (tenant) the user belongs to. |
Content-Type | Yes | application/json. |
| Body field | Type | Required | Description |
|---|---|---|---|
userId | integer | Yes | The 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"
}
| Field | Type | Description |
|---|---|---|
initToken | string | The init token. Pass this to the iframe via postMessage. |
expiresIn | integer | Seconds until the token expires. Always between 30 and 60. |
expiresAt | string | Absolute 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"
}
| HTTP | error | Meaning | What to do |
|---|---|---|---|
| 400 | INVALID_REQUEST | Body malformed or userId missing/non-numeric. | Fix the request payload. |
| 400 | MISSING_ACCOUNT_HEADER | X-Account-Id header missing. | Always send X-Account-Id (see Tenant Context). |
| 401 | INVALID_CREDENTIALS | Partner access token missing, expired, or invalid. | Refresh your OAuth 2.0 access token. |
| 403 | SSO_NOT_ENABLED | The partner does not have the embedded SSO feature enabled. | Contact partnerships@tidely.com to enable embedded integration. |
| 404 | USER_NOT_FOUND | The 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.) |
| 409 | USER_INACTIVE | The user exists but is disabled, deleted, or otherwise blocked from login. | Reactivate the user or pick a different one. |
| 429 | RATE_LIMITED | Too many init-token requests for this partner. | Back off and retry. The Retry-After header gives the wait time. |
| 500 | INTERNAL_ERROR | Unexpected 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/initrequires 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 youpostMessagethe 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