/ Developer Docs
APIs
TTNinjs
TTNewsML
Metadata
Namespaces
Tools
Overview
Supported flows
Obtaining a client
Typical integration path
Authentication Endpoints
Client registration
Prerequisites
Authenticating from the command line
Registering a client
Registration response
Listing your clients
Updating a client
Deleting a client
Obtaining an access token
Authorization Code with PKCE
Authorization Code (confidential client)
Client Credentials
Resource Owner Password
Token response
Refreshing tokens
Making authenticated requests
Scopes
Roles
Token format
Standard claims
TT-specific claims
Example decoded payload
Token verification
Error handling
Token errors
Client registration errors

Authentication

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.


Overview

TT offers two authentication models, plus a self-service way to get the credentials you need to use them.

  • User-delegated — a TT user logs in and your application acts on their behalf. Use the Authorization Code flow (with PKCE for public clients, or a client secret for confidential ones). The resulting token carries that user's roles and profile claims.
  • Service-to-service — your backend calls TT APIs with no user in the loop. Use Client Credentials, or Resource Owner Password for simple scripts. The token represents your client's own service account.

Supported flows

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

Obtaining a client

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.

Typical integration path

  1. Register a client, or contact TT and request a provisioned client if you need Authorization Code.
  2. Obtain an access token using the flow that matches your use case.
  3. Request the roles scope, and any other scopes you need, so the token carries the authorization claims TT's APIs check.
  4. Call the API with the access token as a Bearer token, refreshing before it expires.

Authentication Endpoints

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


Client registration

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.

Prerequisites

Your TT account must have been granted the create-client permission. Contact TT if you need this access.

Authenticating from the command line

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 none

oauth2c 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.

Registering a client

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/default

Example — 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/default

Registration response

A 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.

Listing 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/clients

Here $TOKEN is your regular user access token, not a registration token.

Updating a client

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>

Deleting a client

curl -s -XDELETE \
  -H "Authorization: Bearer $REGISTRATION_TOKEN" \
  https://login.tt.se/realms/tt/clients-registrations/default/<clientId>

Obtaining an access token

Authorization Code with PKCE

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/token

Authorization Code (confidential client)

For 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/token

Client Credentials

For 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/token

Resource Owner Password

For 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/token

Token response

All 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.

Refreshing tokens

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/token

Making authenticated requests

Pass 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/search

Scopes

The 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.

Roles

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.

Token format

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.

Standard claims

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.

TT-specific claims

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).

Example decoded payload

{
  "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"
}

Token verification

Tokens can be verified offline using the public keys from the JWKS endpoint:

GET https://login.tt.se/realms/tt/protocol/openid-connect/certs

The response contains the public keys in JWK Set format. Most OAuth2 libraries handle key fetching and verification automatically. Verify that:

  • the iss claim equals https://login.tt.se/realms/tt
  • the exp claim is in the future
  • the signature is valid against one of the published keys

You can also validate a live token against the userinfo endpoint:

curl -H "Authorization: Bearer $TOKEN" \
  https://login.tt.se/realms/tt/protocol/openid-connect/userinfo

A valid token returns HTTP 200 with the claims corresponding to the requested scopes. An expired or invalid token returns HTTP 401.


Error handling

Token errors

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.

Client registration errors

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.