Skip to main content

Authentication

The Typograph API uses OAuth 2.0 for authentication. Choose the appropriate flow based on your use case.

Quick Start​

For server-to-server integrations, use Client Credentials:

curl -X POST https://api.typograph.nl/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=file document publisher converter"

Response:

{
"access_token": "typ_abc123...",
"token_type": "Bearer",
"expires_in": 172800,
"scope": "file document publisher converter"
}

Getting Credentials​

OAuth Client Registration​

  1. Log in to the Typograph Portal.
  2. Open your organization — Settings → Organizations → your organization.
  3. Go to the Apps tab and click + Create App.
  4. Fill in the form:
    • Name — a human-readable name for the app.
    • Redirect URIs — add one or more callback URLs if you're building a user-facing app that will use the Authorization Code flow. Leave empty for a backend-only app using Client Credentials.
    • Scopes — tick the scopes your app needs (see the Scopes reference). The available scopes depend on your organization's subscription.
  5. Save. The portal displays your Client ID and Client Secret once — copy both immediately.
caution

The Client Secret is shown only once at creation. Store it in a secret manager or environment variable; the portal can't show it again.

Grant type is inferred
  • App with no redirect URIs → client_credentials grant (server-to-server).
  • App with one or more redirect URIs → authorization_code + refresh_token grants (user-facing).

A user-facing app (one with a redirect URI) can show its logo on the consent screen, where people decide whether to give it access to their Typograph account. Without one, they see a grey placeholder.

  1. In the Apps tab, open the ⋯ menu of the app and choose Logo.
  2. Choose an image: a square PNG, JPG or WebP of 128 to 1024 pixels per side, at most 512 KB. 256 × 256 pixels looks sharp everywhere. The portal shows it before you save and says which requirement an image misses.
  3. Click Save logo. Remove logo brings back the placeholder.

Only an owner or admin of the organization can change an app's logo. Typograph does not crop or resize the image, so make it square yourself, with some space around the mark.

When signing in to your app fails​

People who sign in to your app may never have heard of Typograph. When their sign-in fails on Typograph's side, after they have seen the consent screen (granting fails, they decline and your app registered no address to send them back to, or the request expired), the error page sends them to you first:

  • Its button takes them back to your app's website, with the host shown under it.
  • Its help section says Contact <your app>, with a mail to your organization's Contact email that has the error reference filled in. Without one, it points to your website.
  • Typograph's support comes after that, smaller, because Typograph runs the sign-in.

The address and website come from your organization's Settings in the portal (Contact email, Website), so anyone who signs in to one of your apps can see them there. An app that registers itself with a client ID metadata document (its client_id is the document's URL, as MCP clients do) uses its client_uri as the website, but only when it is on the same host as the document. Without a contact email or website, the page offers Typograph's support only, and no button. A broken authorization link (a missing redirect_uri, say) names no app: anyone can make such a link.

IP allowlist​

An app that runs on your own servers (a confidential client) can be limited to the addresses those servers use. Its client credentials and its tokens then work from those addresses only.

  1. In the Apps tab, open the ⋯ menu of the app and choose IP addresses.
  2. Write one address or CIDR range per line, IPv4 or IPv6 (203.0.113.7, 203.0.113.0/24, 2001:db8::/32), at most 50.
  3. Click Save addresses. Allow every address lifts the limit.

Typograph checks the address twice:

  • At the token endpoint. From another address, POST /oauth/token answers 401 with invalid_client, for every grant: client credentials, an authorization code and a refresh token.
  • On every API request. A token used from another address gets 403 with ip_not_allowed. That holds for tokens issued before the list was set too, within a minute of saving.

The address is the one your request reaches Typograph from: behind a NAT gateway or a proxy, that is the gateway's public address. A public client (a browser or mobile app) cannot have a list. Only an owner or admin of the organization can change it, in the portal; the app object shows it as allowed_ips, null without one.

OAuth 2.0 Flows​

Client Credentials Flow​

Best for server-to-server integrations where no user interaction is needed.

curl -X POST https://api.typograph.nl/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=publisher converter document"

Authorization Code Flow with PKCE​

Best for web applications and mobile apps where users authenticate.

Step 1: Generate PKCE Parameters​

// Generate code_verifier (43-128 characters)
const codeVerifier = generateRandomString(64);

// Generate code_challenge
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');

Step 2: Redirect to Authorization​

https://api.typograph.nl/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://your-app.com/callback
&scope=openid profile email file
&state=random_state_string
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256

The consent screen shows the scopes your app asks for and, for a user in more than one organization, a choice under Authorize on behalf of. The token reaches only the organization picked there (which one a token reaches); a user in one organization authorizes for that one.

The user has 30 minutes from this redirect to sign in and answer the consent screen. After that the request has expired: no code is issued, Typograph tells the user so, and its button takes them back to your app at your registered redirect_uri with error=access_denied, your state, and error_reason=authorization_request_expired. The error is the standard one a denied consent gets, so an app that knows only the OAuth error codes keeps working; error_reason tells the two apart, so your app can tell the user that signing in took too long rather than that they cancelled. A denied consent has no error_reason. Read only the values you know, ignore any other, and never show error_description or error_reason to the user as they come: they are in the URL, which anyone can change. Start a new authorization request then. A redirect_uri your app did not register gets no way back; the user is offered the Typograph portal instead.

Step 3: Exchange Code for Tokens​

curl -X POST https://api.typograph.nl/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://your-app.com/callback" \
-d "code_verifier=CODE_VERIFIER"

Send the same redirect_uri as in step 2: without it, or with another one, the answer is 400 invalid_grant. A code works once.

Response:

{
"access_token": "typ_abc123...",
"token_type": "Bearer",
"expires_in": 172800,
"refresh_token": "typ_refresh_xyz789...",
"scope": "openid profile email file"
}

Refresh Token Flow​

Exchange a refresh token for a new access token:

curl -X POST https://api.typograph.nl/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "refresh_token=YOUR_REFRESH_TOKEN"
info

Request the offline_access scope to receive refresh tokens.

A refresh token works once. The response carries a new refresh token: store it in place of the old one, and use it for the next refresh. Refreshing also ends the access token the old refresh token came with.

  • Only the client the refresh token was issued to can use it; any other client gets 400 invalid_grant.
  • Sending the same refresh token again within a minute (a retry after a lost response) gets 400 token_reuse_grace_period; use the tokens from the first response.
  • Sending it again later is treated as theft: every token of that sign-in is revoked, the answer is 400 token_reuse_security_breach, and the user signs in again.
  • A sign-in lasts at most 30 days, however often it is refreshed. A refresh 30 days or more after the user signed in gets 400 invalid_grant with "reason": "sign_in_expired": send the user through the authorization flow again. The access token from the last refresh before then works until it expires.

Using Access Tokens​

Include the access token in the Authorization header:

curl https://api.typograph.nl/v1/publisher/jobs \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Token Types​

The Typograph API distinguishes between user tokens (issued via authorization_code / refresh_token) and client tokens (issued via client_credentials). Each gateway route requires one or the other, or accepts either.

See Token Types for the full breakdown of which endpoints require which token type and how to verify a token's type via introspection.

Which organization a token reaches​

A token reaches one organization: the organization the user picked when they authorized your app, or, when they picked none and for a client token, the organization that owns your app. Introspection names it in client_organization_id.

Every call acts on that organization. A request that names another one is refused with 403 and organization_not_bound, also when the user is a member or an owner of that other organization:

  • an organization in the path, such as /v1/identity/organizations/{id}/members or /v1/file/organizations/{organizationId}/teams;
  • the organization_id query parameter, such as GET /v1/identity/oauth/clients?organization_id=…;
  • the X-Typograph-On-Behalf-Of-Organization-Id header;
  • a team, project, template, comment, media item, font, clip or upload of another organization, named by its own ID, and a move or copy into another organization (team_id or project_id in the body). A form of another organization, and the item a move starts from, answer 404, as an ID that does not exist.

The file service's lists hold only that organization: teams, what is shared with the user, search, trash, favorites, recently opened templates, notifications and the invitations to teams, projects and templates (GET /v1/file/users/me/invitations). The user's invitations into organizations (GET /v1/identity/users/me/invitations) are theirs, not an organization's, and list every one. A person who is only in another organization is not found (404).

An app of another organization, read by its own ID, shows only its public details, and its logo cannot be changed (404).

One way out, for reading only. An organization can let the apps of its guests read its teams (Apps of guests): a user with a role in one of that organization's teams then reads, with your app's token, the teams, projects and templates they have a role on there, and the media, fonts and clips those designs use. Reading means a GET, or asking for a presigned URL to download (action other than PutObject). Everything else in that organization stays refused with organization_not_bound, whatever the user's role there, and the lists then hold those teams too, with organization_name naming their organization. Jobs your app starts are billed to the token's own organization.

{
"error": "organization_not_bound",
"error_description": "This token reaches only the organization it was granted for"
}

To work in another organization, send the user through the authorization code flow again: the consent screen asks every time, and a user who belongs to more than one organization picks under Authorize on behalf of which one the token is for. You then hold a token per organization. Listing the user's organizations (GET /v1/identity/organizations) still shows all of them, with the user's role in each.

Tokens for a resource​

A client may name the resource it wants a token for with the resource parameter (RFC 8707) at /oauth/authorize and /oauth/token; the token then carries it as its audience. The API accepts a token without an audience, and one whose audience is the API (https://api.typograph.nl) or the MCP server (https://mcp.typograph.nl), with or without a path. A token for any other resource is not meant for this API and is refused:

{
"error": "invalid_audience",
"error_description": "Token audience does not match this API"
}

with 401. Leave resource out, or name one of these two, when the token is for the Typograph API.

Token Management​

Token Lifetimes​

Token TypeLifetime
Access Token2 days (172,800 seconds)
Refresh Token3 days (259,200 seconds), renewed by every refresh
Sign-in30 days from when the user signed in: refreshing no longer extends it after that
Authorization request (from the redirect to the authorization endpoint until the consent)30 minutes
Authorization Code10 minutes

Token Introspection​

Verify token validity and get metadata. Authenticate as your client, with client_id and client_secret in the body or with HTTP Basic; without them the answer is 401 invalid_client. Only a confidential client (one with a secret) can introspect: a public client gets 401 invalid_client. You learn about your own client's tokens only: another client's token answers {"active": false}, as an unknown one does.

curl -X POST https://api.typograph.nl/oauth/introspect \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=YOUR_ACCESS_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"

Response:

{
"active": true,
"client_id": "019b28fb-a11e-7641-a28f-e978f892ec06",
"token_type": "Bearer",
"grant_type": "authorization_code",
"auth_type": "user",
"scope": "file document publisher",
"exp": 1734567890,
"iat": 1734395090,
"sub": "019b28fb-a11e-7641-a28f-e978f892ec07",
"client_organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"organization_binding": "token"
}
FieldDescription
grant_typeHow the token was obtained: authorization_code, client_credentials, or refresh_token
auth_typeToken type: user (has user context) or client (no user context)
subUser ID (only present for user tokens)
client_organization_idThe organization the token reaches (which one)
organization_bindingtoken when the user picked that organization at consent, client when it is the organization that owns your app

Token Revocation​

Invalidate a token when no longer needed:

curl -X POST https://api.typograph.nl/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=YOUR_ACCESS_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"

A public client (an app without a client secret, such as a desktop app or an MCP client using PKCE) revokes its own tokens with its client_id alone:

curl -X POST https://api.typograph.nl/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=YOUR_ACCESS_TOKEN" \
-d "client_id=YOUR_CLIENT_ID"

A client can only revoke tokens issued to it. The answer is 200 either way, also for a token that is unknown or belongs to another client.

Single Sign-On (SSO)​

Typograph's own login screen supports SSO via Google and Microsoft Azure AD. From your application's perspective, SSO is invisible — you use the standard Authorization Code + PKCE flow exactly as described above. When the user hits the Typograph login screen they see the "Continue with Google" / "Continue with Microsoft" buttons next to signing in with a code sent by email, pick whichever they prefer, and are redirected back to your redirect_uri with an authorization code as normal.

You don't need to build anything SSO-specific into your app.

User Information​

Get the authenticated user's profile (requires openid scope):

curl https://api.typograph.nl/oauth/userinfo \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
"sub": "019b28fb-a11e-7641-a28f-e978f892ec07",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}

OAuth Discovery​

Typograph implements OAuth 2.0 Authorization Server Metadata (RFC 8414):

curl https://api.typograph.nl/.well-known/oauth-authorization-server

Response:

{
"issuer": "https://api.typograph.nl",
"authorization_endpoint": "https://api.typograph.nl/oauth/authorize",
"token_endpoint": "https://api.typograph.nl/oauth/token",
"revocation_endpoint": "https://api.typograph.nl/oauth/revoke",
"introspection_endpoint": "https://api.typograph.nl/oauth/introspect",
"scopes_supported": [
"openid", "profile", "email", "offline_access",
"authorization", "health",
"identity", "identity:read", "identity:write", "identity:delete",
"file", "file:read", "file:write", "file:delete",
"document", "document:read", "document:write",
"publisher", "publisher:read", "publisher:write",
"converter", "converter:read", "converter:write",
"webhook", "webhook:read", "webhook:write", "webhook:delete",
"subscription", "subscription:read", "subscription:write"
],
"response_types_supported": ["code"],
"response_modes_supported": ["query"],
"grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true,
"revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"service_documentation": "https://docs.typograph.nl"
}

The full scope catalog — including descriptions and the parent/child hierarchy — is documented in Scopes.

Client Types​

Confidential Clients​

Server-side applications that can securely store credentials.

  • Use Client Credentials or Authorization Code flow
  • Must include client_secret in token requests
  • Suitable for: Backend services, server-side web apps

Public Clients​

Client-side applications that cannot securely store credentials.

  • Must use Authorization Code flow with PKCE
  • Cannot use client_secret
  • Suitable for: SPAs, mobile apps, desktop applications

Error Handling​

OAuth Errors​

ErrorDescription
invalid_requestMissing or invalid parameter
invalid_clientClient authentication failed, or the request comes from an address outside the app's IP allowlist
invalid_grantInvalid authorization code or refresh token. With "reason": "sign_in_expired": the sign-in is older than 30 days, and the user signs in again
unauthorized_clientClient not authorized for this grant type
unsupported_grant_typeGrant type not supported
token_reuse_grace_periodA refresh token sent again within a minute; use the tokens from the first response
token_reuse_security_breachA refresh token sent again later; every token of that sign-in is revoked, sign in again
invalid_scopeRequested scope is invalid or exceeds granted scopes

Error Response:

{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}

HTTP Status Codes​

CodeDescription
401 UnauthorizedMissing or invalid access token
403 ForbiddenToken lacks required scope, wrong token type, the request names an organization the token does not reach, or comes from an address outside the app's IP allowlist

Token Type Errors​

Error CodeDescription
invalid_token_typeWrong token type for endpoint (e.g., using client token on user-only endpoint)
insufficient_scopeToken lacks required OAuth scope
organization_not_boundThe request names another organization than the one the token reaches (details)
ip_not_allowedThe token's app has an IP allowlist and the request comes from another address

Security Best Practices​

  1. Store credentials securely - Never commit secrets to version control
  2. Use environment variables - Keep credentials out of your codebase
  3. Request minimal scopes - Only request what your application needs
  4. Use PKCE - Always use PKCE for Authorization Code flow
  5. Rotate credentials - Periodically rotate client secrets
  6. Limit server apps to their addresses - An IP allowlist makes a leaked secret useless elsewhere
  7. Validate tokens - Use introspection for sensitive operations
  8. Handle expiration - Implement automatic token refresh
  9. Use HTTPS - Always use secure connections