TT's APIs use OAuth 2.0 for authentication, backed by a Keycloak-based identity provider. All tokens issued are signed JSON Web Tokens (JWTs).
Migrating from the legacy system? The old opaque-token endpoints at
tt.se/o/oauth2/have been replaced. See the legacy OAuth2 documentation for the old endpoints. The scopes and roles remain the same.
TT offers two authentication models, plus a self-service way to get the credentials you need to use them.
roles and profile claims.| Flow | Use when | Available to self-registered clients |
|---|---|---|
| Authorization Code + PKCE | Single-page apps, mobile apps, any public client | No |
| Authorization Code (confidential) | Server-side web apps with a client secret | No |
| Client Credentials | Service-to-service calls with no user context | Yes |
| Resource Owner Password | Simple scripts and server-side tooling (no browser required) | Yes |
Every flow needs a registered client_id. Users with the OFP_CUSTOMER_ADMIN
role can self-register a client without any involvement
from TT, but self-registered clients are limited to the service-to-service flows
(Client Credentials and Resource Owner Password). If your integration needs a
user to log in (Authorization Code), TT must provision the client manually;
contact TT to request one.
roles scope, and any other scopes you need, so the
token carries the authorization claims TT's APIs check.All endpoints are under the tt realm at login.tt.se:
| Name | URL |
|---|---|
| Authorization endpoint | https://login.tt.se/realms/tt/protocol/openid-connect/auth |
| Token endpoint | https://login.tt.se/realms/tt/protocol/openid-connect/token |
| JWKS endpoint | https://login.tt.se/realms/tt/protocol/openid-connect/certs |
| Userinfo endpoint | https://login.tt.se/realms/tt/protocol/openid-connect/userinfo |
| Client registration | https://login.tt.se/realms/tt/clients-registrations/default |
| Client list | https://login.tt.se/realms/tt/clients |
The full machine-readable OpenID configuration is available at https://login.tt.se/realms/tt/.well-known/openid-configuration
Clients can be registered programmatically via the Keycloak client registration API.
Supported flows for self-registered clients: Self-registered clients support the Client Credentials and Resource Owner Password flows. The Authorization Code flow is not available for self-registered clients. If your integration requires Authorization Code, contact TT to have a client provisioned manually.
Your TT account must have been granted the create-client permission. Contact
TT if you need this access.
If you do not already have a TT-issued access token in your environment, use the
client-registration OAuth2 client, which is pre-configured for command-line
use via the Device Authorization Grant (RFC 8628).
oauth2c is a command-line tool that implements OAuth2 flows,
including device flow. It handles the multi-step protocol — initiating the flow,
polling for approval, and exchanging the device code for a token — in a single
command.
Install it, then run:
oauth2c https://login.tt.se/realms/tt \
--client-id client-registration \
--grant-type urn:ietf:params:oauth:grant-type:device_code \
--scopes openid \
--auth-method noneoauth2c prints a short URL and a user code. Open the URL in a browser, enter
the code, and authenticate with your TT account. Once approved, the access token
is printed to stdout. Capture it:
export TOKEN=<printed access token>This $TOKEN is what you pass as the Bearer token in the registration
requests below.
Alternative tools that support the device flow: oidc-agent
(daemon-based, handles token renewal automatically) and
zitadel-tools.
Send a POST request to the client registration endpoint with your access token
as a Bearer token and a JSON body describing the client.
Required fields:
| Field | Type | Description |
|---|---|---|
name |
string | Display name for the client. Must be unique within your organization. |
Optional fields:
| Field | Type | Description |
|---|---|---|
description |
string | Human-readable description. |
serviceAccountsEnabled |
boolean | Set to true to enable the Client Credentials flow (default: false). |
directAccessGrantsEnabled |
boolean | Set to true to enable the Resource Owner Password flow (default: false). |
redirectUris |
array | Redirect URIs the client is allowed to use in browser-based flows. Only applicable for browser-based flows. |
webOrigins |
array | Allowed CORS origins for browser-based requests to the token endpoint. Only applicable for browser-based flows. |
The clientId is generated automatically in the format
{orgId}-{slugified-name} and cannot be supplied by the caller.
Example — registering a client (Resource Owner Password flow):
curl -s -XPOST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Integration",
"directAccessGrantsEnabled": true
}' \
https://login.tt.se/realms/tt/clients-registrations/defaultExample — registering a service account client (Client Credentials flow):
curl -s -XPOST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Service",
"serviceAccountsEnabled": true
}' \
https://login.tt.se/realms/tt/clients-registrations/defaultA successful registration returns HTTP 201 with the client representation:
{
"id": "3f2a1b4c-...",
"clientId": "12345-my-integration",
"name": "My Integration",
"description": "",
"registrationAccessToken": "eyJhbGciOi...",
"links": {
"view": {
"href": "https://login.tt.se/realms/tt/clients-registrations/default/12345-my-integration",
"method": "GET"
},
"update": {
"href": "https://login.tt.se/realms/tt/clients-registrations/default/12345-my-integration",
"method": "PUT"
},
"delete": {
"href": "https://login.tt.se/realms/tt/clients-registrations/default/12345-my-integration",
"method": "DELETE"
}
}
}The registrationAccessToken is a separate credential from your user access
token. It is scoped exclusively to this one client and authorizes view, update,
and delete operations on it — nothing else. It is not a JWT; treat it as an
opaque secret.
Save it now. The token is single-use in the sense that it rotates on every
successful request: each call to view, update, or delete the client invalidates
the current token and returns a new one in the response. If you lose it, use
GET /realms/tt/clients (see below) — that endpoint issues a fresh token for
each of your clients.
Returns all clients you have registered. Each entry includes a fresh
registrationAccessToken, so this is also the recovery path if you have lost a
token.
curl -s \
-H "Authorization: Bearer $TOKEN" \
https://login.tt.se/realms/tt/clientsHere $TOKEN is your regular user access token, not a registration token.
Use the registrationAccessToken as the Bearer token — not your user
access token:
curl -s -XPUT \
-H "Authorization: Bearer $REGISTRATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "My Integration", "description": "Updated description"}' \
https://login.tt.se/realms/tt/clients-registrations/default/<clientId>curl -s -XDELETE \
-H "Authorization: Bearer $REGISTRATION_TOKEN" \
https://login.tt.se/realms/tt/clients-registrations/default/<clientId>Recommended for browser-based and mobile applications. The client never handles a client secret.
Step 1 — Generate a code verifier and challenge.
CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')Step 2 — Redirect the user to the authorization endpoint.
GET https://login.tt.se/realms/tt/protocol/openid-connect/auth
?response_type=code
&client_id=<client_id>
&redirect_uri=<redirect_uri>
&scope=openid roles
&code_challenge=<CODE_CHALLENGE>
&code_challenge_method=S256
&state=<random_state>Step 3 — Exchange the authorization code for tokens.
After the user authenticates, Keycloak redirects to redirect_uri with a code
parameter. Exchange it for tokens:
curl -s -XPOST \
-d "grant_type=authorization_code" \
-d "client_id=<client_id>" \
-d "code=<authorization_code>" \
-d "redirect_uri=<redirect_uri>" \
-d "code_verifier=$CODE_VERIFIER" \
https://login.tt.se/realms/tt/protocol/openid-connect/tokenFor server-side apps that can keep a client secret. Steps 1–2 are identical to
the PKCE flow above (omit the code_challenge parameters). At step 3, include
the client secret instead of a code verifier:
curl -s -XPOST \
-d "grant_type=authorization_code" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "code=<authorization_code>" \
-d "redirect_uri=<redirect_uri>" \
https://login.tt.se/realms/tt/protocol/openid-connect/tokenFor server-to-server calls where there is no user context. The client authenticates as itself. Requires a service-account-enabled client (see client registration).
curl -s -XPOST \
-d "grant_type=client_credentials" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "scope=openid roles" \
https://login.tt.se/realms/tt/protocol/openid-connect/tokenFor scripts and tooling where a browser redirect is impractical. Sends credentials directly to the token endpoint.
curl -s -XPOST \
-d "grant_type=password" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "username=<username>" \
-d "password=<password>" \
-d "scope=openid roles" \
https://login.tt.se/realms/tt/protocol/openid-connect/tokenAll flows return a JSON response with the same shape:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIi...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIi...",
"expires_in": 300,
"refresh_expires_in": 1800,
"token_type": "Bearer",
"scope": "roles"
}access_token — the JWT to include in API requests.refresh_token — use this to get a new access_token without
re-authenticating (see refreshing tokens).expires_in — seconds until the access_token expires. The access token is
short-lived; always check the exp claim or expires_in and refresh
proactively.curl -s -XPOST \
-d "grant_type=refresh_token" \
-d "client_id=<client_id>" \
-d "client_secret=<client_secret>" \
-d "refresh_token=<refresh_token>" \
https://login.tt.se/realms/tt/protocol/openid-connect/tokenPass the access token as a Bearer token in the Authorization header:
export TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIi...
curl -H "Authorization: Bearer $TOKEN" \
https://api.tt.se/content/v1/text/searchThe scope parameter is a space-separated list of scope names. Include openid
and roles for API access to work; the other scopes are optional.
| Scope | Grants access to |
|---|---|
openid |
ID token and userinfo endpoint access. Required for token verification via the userinfo endpoint. |
roles |
roles claim — an array of role names assumed by the user. Required for API access. |
email |
email — the user's email address. |
user |
name, family_name, given_name, and user_id (if available). |
customer |
customer_id, customer_name, and department (if available). |
profile |
Access to view and update the user profile. |
collection |
Access to view and update user collections. |
admin |
Access to organization user admin endpoints, provided the user has admin privileges. |
The roles claim is an array of role names:
| Role | Meaning |
|---|---|
ROLE_OFP |
Standard TT customer. |
ROLE_OFP_CUSTOMER |
Authenticated as a customer user. |
ROLE_OFP_CUSTOMER_ADMIN |
Customer administrator privileges. |
ROLE_OFP_PHOTOGRAPHER |
Access to photographer-specific features. |
ROLE_MEDIEBANK |
Access to the Mediabank. |
ROLE_ACCESS_PREVIEW |
Permission to access preview-quality renditions. |
ROLE_ACCESS_HIRES |
Permission to access high-resolution renditions. |
Access tokens are signed JWTs (RFC 7519), encoded as three
base64url-encoded segments separated by dots: header.payload.signature. You
can inspect the payload at jwt.io.
| Claim | Description |
|---|---|
sub |
Subject identifier — the authenticated user or client ID, encoded as a URI. OFP customer accounts look like ofp://user/1234; TT employee accounts use core://user/<id>, with keycloak://user/<uuid> as a fallback when neither identifier is available. Client Credentials tokens (no user in the loop) use core://application/<client_id> instead. |
iss |
Issuer — always https://login.tt.se/realms/tt. |
aud |
Audience — the intended recipient(s) of the token. |
exp |
Expiration time (Unix timestamp). |
iat |
Issued-at time (Unix timestamp). |
jti |
JWT ID — a unique identifier for this token. |
| Claim | Description |
|---|---|
org |
Organization identifier, e.g. ofp://org/12345. Present for customer users. |
roles |
Array of role names (populated when roles scope is requested). |
{
"sub": "ofp://user/1234",
"iss": "https://login.tt.se/realms/tt",
"aud": "account",
"exp": 1782480000,
"iat": 1782479700,
"jti": "trtcc:96fd20a6-...",
"org": "ofp://org/12345",
"roles": ["ROLE_OFP", "ROLE_OFP_CUSTOMER"],
"email": "user@example.com",
"name": "Firstname Lastname",
"given_name": "Firstname",
"family_name": "Lastname"
}Tokens can be verified offline using the public keys from the JWKS endpoint:
GET https://login.tt.se/realms/tt/protocol/openid-connect/certsThe response contains the public keys in JWK Set format. Most OAuth2 libraries handle key fetching and verification automatically. Verify that:
iss claim equals https://login.tt.se/realms/ttexp claim is in the futureYou can also validate a live token against the userinfo endpoint:
curl -H "Authorization: Bearer $TOKEN" \
https://login.tt.se/realms/tt/protocol/openid-connect/userinfoA valid token returns HTTP 200 with the claims corresponding to the requested
scopes. An expired or invalid token returns HTTP 401.
| HTTP status | Cause |
|---|---|
400 |
Malformed request — missing or invalid parameters. |
401 |
Invalid, expired, or missing credentials. |
The response body contains a JSON object with error and error_description
fields:
{
"error": "invalid_grant",
"error_description": "Invalid user credentials"
}Common error values from the token endpoint:
| Error | Description |
|---|---|
invalid_grant |
Credentials are wrong, the code has already been used, or the refresh token has expired. |
invalid_client |
Unknown client_id or wrong client_secret. |
unsupported_grant_type |
The grant_type value is not recognized. |
| HTTP status | Cause |
|---|---|
401 |
No Bearer token provided, or the token is invalid. |
403 |
The token does not have the create-client permission. |
400 |
The request body violates a registration policy. |
Common 400 error messages:
| Message | Description |
|---|---|
name is required |
The name field is missing. |
a client with this name already exists for your organization |
Choose a different name. |
Supplying a clientId is not allowed |
Remove the clientId field; it is generated automatically. |
maximum number of clients reached |
You have reached the client limit for your organization. |
Field '{name}' is not permitted |
A field in the request body is not allowed during self-registration. |