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
- Log in to the Typograph Portal.
- Open your organization — Settings → Organizations → your organization.
- Go to the Apps tab and click + Create App.
- 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.
- Save. The portal displays your Client ID and Client Secret once — copy both immediately.
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.
- App with no redirect URIs →
client_credentialsgrant (server-to-server). - App with one or more redirect URIs →
authorization_code+refresh_tokengrants (user-facing).
App logo
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.
- In the Apps tab, open the ⋯ menu of the app and choose Logo.
- 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.
- 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.
- In the Apps tab, open the ⋯ menu of the app and choose IP addresses.
- 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. - Click Save addresses. Allow every address lifts the limit.
Typograph checks the address twice:
- At the token endpoint. From another address,
POST /oauth/tokenanswers401withinvalid_client, for every grant: client credentials, an authorization code and a refresh token. - On every API request. A token used from another address gets
403withip_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"
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_grantwith"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}/membersor/v1/file/organizations/{organizationId}/teams; - the
organization_idquery parameter, such asGET /v1/identity/oauth/clients?organization_id=…; - the
X-Typograph-On-Behalf-Of-Organization-Idheader; - 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_idorproject_idin the body). A form of another organization, and the item a move starts from, answer404, 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 Type | Lifetime |
|---|---|
| Access Token | 2 days (172,800 seconds) |
| Refresh Token | 3 days (259,200 seconds), renewed by every refresh |
| Sign-in | 30 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 Code | 10 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"
}
| Field | Description |
|---|---|
grant_type | How the token was obtained: authorization_code, client_credentials, or refresh_token |
auth_type | Token type: user (has user context) or client (no user context) |
sub | User ID (only present for user tokens) |
client_organization_id | The organization the token reaches (which one) |
organization_binding | token 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_secretin 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
| Error | Description |
|---|---|
invalid_request | Missing or invalid parameter |
invalid_client | Client authentication failed, or the request comes from an address outside the app's IP allowlist |
invalid_grant | Invalid 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_client | Client not authorized for this grant type |
unsupported_grant_type | Grant type not supported |
token_reuse_grace_period | A refresh token sent again within a minute; use the tokens from the first response |
token_reuse_security_breach | A refresh token sent again later; every token of that sign-in is revoked, sign in again |
invalid_scope | Requested scope is invalid or exceeds granted scopes |
Error Response:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
HTTP Status Codes
| Code | Description |
|---|---|
401 Unauthorized | Missing or invalid access token |
403 Forbidden | Token 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 Code | Description |
|---|---|
invalid_token_type | Wrong token type for endpoint (e.g., using client token on user-only endpoint) |
insufficient_scope | Token lacks required OAuth scope |
organization_not_bound | The request names another organization than the one the token reaches (details) |
ip_not_allowed | The token's app has an IP allowlist and the request comes from another address |
Security Best Practices
- Store credentials securely - Never commit secrets to version control
- Use environment variables - Keep credentials out of your codebase
- Request minimal scopes - Only request what your application needs
- Use PKCE - Always use PKCE for Authorization Code flow
- Rotate credentials - Periodically rotate client secrets
- Limit server apps to their addresses - An IP allowlist makes a leaked secret useless elsewhere
- Validate tokens - Use introspection for sensitive operations
- Handle expiration - Implement automatic token refresh
- Use HTTPS - Always use secure connections