Skip to main content

Identity Service

The Identity Service exposes user identity and organization data to third-party applications through /v1/identity/*. Everything here requires a user token obtained via the Authorization Code + PKCE flow.

Base URL: /v1/identity/

Scopes: identity (full access), identity:read, identity:write, and identity:delete for deleting the person's own account, or an organization they own, which identity does not include.

Current User​

Get current user​

GET /v1/identity/users/me

Scopes: identity or identity:read

curl https://api.typograph.nl/v1/identity/users/me \
-H "Authorization: Bearer $USER_TOKEN"

Response:

{
"id": "019b28fb-a11e-7641-a28f-e978f892ec06",
"image_url": "https://cdn.typograph.nl/avatars/johndoe.png",
"handle": "johndoe",
"first_name": "John",
"last_name": "Doe",
"email": "user@example.com",
"phone": null,
"settings": {
"send_notification_email": true,
"enable_dark_mode": false,
"theme": "system",
"language": "en",
"time_zone": "system",
"browser_time_zone": "Europe/Amsterdam"
},
"created_at": "<ISO 8601 timestamp>",
"updated_at": "<ISO 8601 timestamp>",
"last_sign_in_at": "<ISO 8601 timestamp>",
"analytics_consent": null,
"analytics_consent_updated_at": null,
"initial": "J",
"full_name": "John Doe"
}

theme is the Typograph portal's theme: light, dark, or system to follow the setting of the person's computer or browser. enable_dark_mode is deprecated: it is true only while the theme is dark. Read theme instead.

time_zone is the zone the Typograph portal and the person's emails show times in: system (the default) follows the person's browser, or an IANA name such as Europe/London that the person chose. browser_time_zone is the zone the person's browser last reported, an IANA name, and null until one did; emails use it while time_zone is system.

analytics_consent is the person's answer to product analytics in the Typograph portal: granted, declined, or null while they have not answered. analytics_consent_updated_at is when that answer last changed, and null until the first.

Update current user​

PATCH /v1/identity/users/me

Scopes: identity or identity:write

Send only the fields you want to change: a field the body leaves out keeps its value. The answer is the whole profile, as GET /v1/identity/users/me returns it.

curl -X PATCH https://api.typograph.nl/v1/identity/users/me \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"first_name": "John", "last_name": "Smith", "handle": "johnsmith"}'
FieldTypeNotes
handlestringUp to 255 characters, not empty
first_namestringUp to 255 characters, not empty
last_namestringUp to 255 characters, not empty
phonestring or nullUp to 255 characters; null removes it
image_urlstring or nullA URL of up to 2000 characters; null goes back to the generated avatar
settingsobjectAny of send_notification_email (boolean), theme (light, dark, or system to follow the person's device) language (en or nl), time_zone (system or an IANA time zone name such as Europe/London) and browser_time_zone (an IANA time zone name). The settings you send are merged into the current ones: {"settings": {"language": "nl"}} changes the language only. enable_dark_mode (boolean) is deprecated: true sets the theme to dark, false turns dark into light and leaves light or system as they are; with a theme in the same body, the theme wins
analytics_consentstring or nullgranted or declined: whether the person allows product analytics in the portal. null takes it back to unanswered. Change it only when the person asks; the same answer again keeps analytics_consent_updated_at

The email address does not change through the profile: an email in the body is ignored. A null for a field that always has a value (handle, first_name, last_name, settings or one of the settings) answers 422 validation_error, as does a value outside the rules above; nothing changes then.

Delete the current user's account​

An app can delete the signed-in person's own account, but only with a code from that person's mailbox. It takes two steps: the app asks Typograph to mail the code, and then sends the code the person gives it.

Scopes: identity:delete, on a user token. identity does not include it: your app asks for it on its own, and the person sees it on the consent screen. Every plan includes it.

1. Mail the person their code​

POST /v1/identity/users/me/deletion-code
curl -X POST https://api.typograph.nl/v1/identity/users/me/deletion-code \
-H "Authorization: Bearer $USER_TOKEN"

Answers 204 without a body. The mail, in the person's language, carries a six-digit code that works for 15 minutes, and says plainly that it deletes their account. The code does nothing else: it signs nobody in. Asking again within a few seconds answers 429.

2. Delete with that code​

DELETE /v1/identity/users/me
curl -X DELETE https://api.typograph.nl/v1/identity/users/me \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"verification_code": "123456", "reason": "switching_tool"}'
FieldRequiredDescription
verification_codeyesThe six-digit code from the mail
reasonnoWhy the person is leaving, when they said so: no_longer_needed, missing_features, too_expensive, too_difficult, switching_tool or other. Another value is a 422. There is no free text: ask the person, never guess

Response:

{
"message": "Account scheduled for deletion. Your data will be permanently removed in 31 days.",
"deletion_scheduled_at": "<ISO 8601 timestamp>"
}

The account stops working at once: every token of the person is revoked, yours included. It is removed for good at deletion_scheduled_at, the first daily removal once 30 days have passed. Until then the person can keep it by signing in again. Everything that belongs to them alone goes with it: their profile and the organizations they are the only member of.

Ask the person for the code; never guess one or use another code. A wrong or expired code answers 400 with invalid_request, and too many wrong ones lock it for a while (429).

When the person owns a shared organization​

While the person owns an organization that has other members, both steps answer 409 with organization_owner_with_members, and no code is sent. Its error_description names the organizations and what to do, so you can show it as it is:

{
"error": "organization_owner_with_members",
"error_description": "You own Acme, which has other members. Make another member the owner, or remove the other members, and then delete your account. Need help? See the support guide: https://docs.typograph.nl/guides/support/",
"request_id": "019397ab-cdef-7890-abcd-ef1234567890"
}

All Scopes​

GET /v1/identity/oauth/scopes

Scopes: identity or identity:read

Every scope Typograph knows, as { "data": [{ "scope": "file", "description": "Full file access", "deprecated_by": null, "children": ["file:read", "file:write", "file:delete"] }] }. children lists the narrower scopes a parent scope includes; a deprecated scope names its replacement in deprecated_by. Use it to explain what the allowed_scopes of an app mean.

Scopes an Organization's Plan Allows​

GET /v1/identity/oauth/scopes/me?organization_id={organizationId}

Scopes: identity or identity:read

Returns the scopes an app may be given for the organization: the ones its subscription plan allows, each with its description, as { "data": [{ "scope": "file", "description": "Full file access", "deprecated_by": null, "children": null }] }. Without organization_id, or for an organization without a subscription, data is empty. Only a member of the organization gets an answer: anyone else gets a 403 with access_denied, and another organization than the one the token reaches a 403 with organization_not_bound (which one). When the plan cannot be read, the answer is a 503 with subscription_service_unavailable. It does not describe the current token: for that, use token introspection.

Organizations​

Every OAuth client and every API request is attributed to an organization. Organizations carry the subscription plan and the rate-limit quotas.

EndpointMethodScopes
/v1/identity/organizationsGETidentity / identity:read
/v1/identity/organizationsPOSTidentity / identity:write
/v1/identity/organizations/{id}GETidentity / identity:read
/v1/identity/organizations/{id}PATCHidentity / identity:write
/v1/identity/organizations/{id}/deletion-codePOSTidentity:delete
/v1/identity/organizations/{id}DELETEidentity:delete

List organizations​

curl https://api.typograph.nl/v1/identity/organizations \
-H "Authorization: Bearer $USER_TOKEN"

Returns the organizations the authenticated user is a member of. Uses standard pagination.

Create an organization​

POST /v1/identity/organizations creates another organization, for another company or a client. The caller becomes its owner, and it starts on the free plan with its own members, apps, limits and billing. Send a name; slug, description and website_url are optional. Without a slug, one is derived from the name: accents become their plain letter, anything else a hyphen, and -1, -2 is added when the slug is taken. A slug you send that another organization already has is a 409 with slug_conflict; a property that fails validation is a 422. The answer is the new organization, with role owner.

A person owns one organization on the free plan. Someone who owns one already is refused with a 409 and free_organization_limit_reached: a second organization needs a paid plan, which you ask us for (see Rate Limiting). When the plan cannot be checked, the answer is a 503 with subscription_service_unavailable; try again in a moment.

curl -X POST https://api.typograph.nl/v1/identity/organizations \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Press Room"}'

It needs identity or identity:write. An MCP client that registered itself, such as Claude Desktop, does not get identity:write, so it cannot create organizations; an app of your organization that holds the scope can.

Update an organization​

PATCH /v1/identity/organizations/{id} changes the properties in the body and leaves the rest: name, slug, description, image_url, website_url, contact_email and the billing_* details. It takes the owner or admin role in the organization. A slug another organization already has is a 400 with slug_conflict; a property that fails validation is a 422.

curl -X PATCH https://api.typograph.nl/v1/identity/organizations/$ORG_ID \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"website_url": "https://www.example.com"}'

Delete an organization​

The owner can delete an organization, confirmed with a code from the owner's mailbox, the way an account is deleted. It takes two steps: the app asks Typograph to mail the code, and then sends the code the person gives it. A person always keeps at least one organization.

Scopes: identity:delete, on a user token. identity does not include it: your app asks for it on its own, and the person sees it on the consent screen.

1. Mail the owner their code​

POST /v1/identity/organizations/{id}/deletion-code
curl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/deletion-code \
-H "Authorization: Bearer $USER_TOKEN"

Answers 204 without a body. The mail, in the owner's language, names the organization, says what deleting it means, and carries a six-digit code that works for 15 minutes. The code deletes this organization and nothing else: not another organization, not the account, and it signs nobody in. Asking again within a few seconds answers 429 with verification_code_rate_limit_exceeded.

2. Delete with that code​

DELETE /v1/identity/organizations/{id}
curl -X DELETE https://api.typograph.nl/v1/identity/organizations/$ORG_ID \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"verification_code": "123456"}'

Response:

{ "message": "Organization deleted" }

The API cannot undo it. Every member loses access at once, the tokens bound to the organization are revoked, its webhook subscriptions are deleted, and its subscription ends, a paid plan included, without waiting for the end of the period. Its teams, projects, templates, forms and files cannot be opened any more, and are deleted for good 30 days later, with the answers entered in the forms. Until then, support can restore the organization on the owner's request (Deleted it by mistake?); the tokens and webhook subscriptions do not come back. Your other organizations stay as they are.

Ask the person for the code; never guess one or use another code.

StatusErrorWhen
400invalid_requestThe code is wrong or expired, or was issued for another organization
403access_deniedThe caller is not the owner
404not_foundThe organization does not exist
409last_organizationIt is the only organization the owner is a member of
422validation_errorverification_code is missing or not six digits
429verification_rate_limit_exceededToo many wrong codes: wait, then ask for a new one

last_organization comes before a code is sent and before a code is used up. Its error_description says what to do, so you can show it as it is:

{
"error": "last_organization",
"error_description": "This is the only organization you are a member of, and you always keep at least one. Create or join another organization first, and then delete this one.",
"request_id": "019397ab-cdef-7890-abcd-ef1234567890"
}

Organization members​

EndpointMethodScopes
/v1/identity/organizations/{id}/membersGETidentity / identity:read
/v1/identity/organizations/{orgId}/members/{memberId}GETidentity / identity:read
/v1/identity/organizations/{orgId}/members/{memberId}PATCHidentity / identity:write
/v1/identity/organizations/{orgId}/members/{memberId}DELETEidentity / identity:write
/v1/identity/organizations/{orgId}/members/transfer-ownershipPOSTidentity / identity:write

The member list uses standard pagination: limit (default 50, max 100) and offset, with a meta object.

Member object:

{
"id": "019b28fb-a11e-7641-a28f-e978f892ec07",
"user_id": "019b28fb-a11e-7641-a28f-e978f892ec06",
"user_email": "user@example.com",
"user_name": "John Doe",
"user_image_url": null,
"role": "admin",
"can_use_quota": true,
"created_at": "<ISO 8601 timestamp>",
"invited_by_id": "019b28fb-a11e-7641-a28f-e978f892ec05"
}

memberId in the paths is the membership's id, not the user's. Any member can list and read the members; changing them takes the owner or admin role. There is no route that adds someone: a person becomes a member only by accepting an invitation, so nobody is put into an organization without agreeing to it.

  • Change (PATCH .../members/{memberId}) role, can_use_quota or both.
  • Remove (DELETE .../members/{memberId}) answers { "message": "Member removed" }. Members may also remove themselves.

An organization has one owner. The owner cannot be demoted or removed (cannot_demote_owner, cannot_remove_owner), and nobody is made owner with a PATCH (cannot_promote_to_owner): the owner transfers ownership instead. Only the owner changes or removes the owner.

Transfer ownership​

POST .../members/transfer-ownership hands the organization to another member. Only the owner can call it, with the new owner's user ID (user_id, not the membership's id). In one step the new owner becomes owner and the previous owner becomes admin: they keep managing members and settings, but no longer manage the subscription or delete the organization, and cannot take ownership back themselves.

curl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/members/transfer-ownership \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "019b28fb-a11e-7641-a28f-e978f892ec06"}'

The answer holds both memberships, as member objects:

{
"message": "Ownership transferred",
"new_owner": { "user_id": "019b28fb-a11e-7641-a28f-e978f892ec06", "role": "owner", "...": "..." },
"previous_owner": { "user_id": "019b28fb-a11e-7641-a28f-e978f892ec05", "role": "admin", "...": "..." }
}
StatusErrorWhen
400invalid_requestuser_id is missing or not a UUID
400not_a_memberThe user is not a member of this organization
400already_ownerThe user is the owner already, for example the caller
403access_deniedThe caller is not the owner
404not_foundThe organization does not exist
409member_scheduled_for_deletionThe member is deleting their account and cannot become the owner

Other services can see the old roles for up to a minute after the transfer.

Organization invitations​

An owner or admin invites someone by email address. Typograph mails them a link to the portal that works for 7 days; they sign in, or create an account, with that address and join. Someone without a Typograph account can be invited too.

EndpointMethodScopes
/v1/identity/organizations/{id}/invitationsPOSTidentity / identity:write / identity:invitations
/v1/identity/organizations/{id}/invitationsGETidentity / identity:read
/v1/identity/organizations/{id}/invitations/{invitationId}/resendPOSTidentity / identity:write / identity:invitations
/v1/identity/organizations/{id}/invitations/{invitationId}DELETEidentity / identity:write / identity:invitations
/v1/identity/users/me/invitationsGETidentity / identity:read
/v1/identity/users/me/invitations/lookupPOSTidentity / identity:read
/v1/identity/users/me/invitations/{invitationId}/acceptPOSTidentity / identity:write
/v1/identity/users/me/invitations/{invitationId}/declinePOSTidentity / identity:write

All of them take a user token. Inviting, listing, sending again and revoking take the owner or admin role in the organization; anyone else gets a 403 with access_denied.

Invitation object:

{
"id": "019b28fb-a11e-7641-a28f-e978f892ec10",
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"organization_name": "Example Print",
"email": "alex@example.com",
"role": "member",
"invited_by_id": "019b28fb-a11e-7641-a28f-e978f892ec05",
"invited_by_name": "John Doe",
"created_at": "<ISO 8601 timestamp>",
"sent_at": "<ISO 8601 timestamp>",
"expires_at": "<ISO 8601 timestamp>",
"is_expired": false
}

Invite someone​

curl -X POST https://api.typograph.nl/v1/identity/organizations/$ORG_ID/invitations \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "alex@example.com", "role": "member"}'

role is member (the default) or admin. Nobody is invited as owner: an organization has one owner, and ownership moves only by a transfer. The answer is 201 with the invitation, and the mail goes out a moment later. The mail is in the invited person's language when they have an account, otherwise in English.

StatusErrorWhen
409already_memberThe address belongs to a member of the organization
409already_invitedThe address has an invitation already, expired or not: send that one again instead
409member_limit_reachedMembers and open invitations together are at the plan's limit of team members
409too_many_invitationsThe organization has 50 open invitations, the most it can have at once
422validation_erroremail is not an email address, or role is not member or admin
429invitation_recently_sentThe organization mailed this address less than a minute ago, also when that invitation was revoked since; Retry-After says how long to wait
429invitation_mail_limitThe address has had 10 invitation mails today, from all organizations, or you have sent 100; Retry-After runs until midnight UTC
503subscription_service_unavailableThe plan could not be checked; try again in a moment

Every open invitation holds a place under the plan's limit, so accepting normally finds room. Only a plan that shrank after the invitation was sent answers the accept with 409 member_limit_reached.

List, send again and revoke​

GET .../invitations lists the organization's invitations that nobody answered yet, newest first, with standard pagination. Expired ones are included, with "is_expired": true.

POST .../invitations/{invitationId}/resend mails the invitation again with a new link that works for another 7 days; the link of the earlier mail stops working. That is also how an expired invitation is renewed, which needs room under the plan's limit again (409 member_limit_reached). Within a minute of the last mail it answers 429 with invitation_recently_sent and a Retry-After header, and past the daily limits above with invitation_mail_limit.

DELETE .../invitations/{invitationId} revokes an invitation (204): its link stops working at once.

Answer an invitation​

These routes act for the signed-in person, on the invitations sent to their account's email address.

  • GET /v1/identity/users/me/invitations lists the person's open invitations, with pagination. Expired ones are left out.
  • POST /v1/identity/users/me/invitations/lookup with {"token": "typ_inv_..."} answers the invitation a mail's link points at; the token is the last part of the link. It is sent in the body so it stays out of access logs. A link that was revoked, answered or replaced by a newer mail is a 404. An expired invitation is answered with "is_expired": true. When the link was sent to another address, the answer is a 403 with invite_recipient_mismatch and a recipient_hint such as a•••@example.com: sign in with that address.
  • POST /v1/identity/users/me/invitations/{invitationId}/accept makes the person a member with the invitation's role and answers the new member object. The link then stops working. It answers 409 with invitation_expired for an expired invitation, already_member when the person is a member already, or member_limit_reached when the organization's plan has no room left; the invitation is kept in that last case.
  • POST /v1/identity/users/me/invitations/{invitationId}/decline removes the invitation (204).

An invitation addressed to someone else is a 404 on accept and decline. Accepting needs identity:write, like leaving an organization: it changes the person's memberships. Apps that register themselves through the MCP server, such as Claude Desktop, do not get identity:write, so through them a person can list invitations but answers them in the portal.

Connected Apps​

An organization decides which third-party apps its members may connect. Every app is allowed until an admin sets a policy for it.

EndpointMethodScopes
/v1/identity/organizations/{orgId}/connected-appsGETidentity / identity:read
/v1/identity/organizations/{orgId}/connected-apps/{clientId}GETidentity / identity:read
/v1/identity/organizations/{orgId}/connected-apps/{clientId}/policyPUTidentity / identity:write

All three take the owner or admin role. The list holds the apps that have a policy; reading one app without a policy reports it as allow. clientId is the app's OAuth client ID.

curl -X PUT https://api.typograph.nl/v1/identity/organizations/$ORG_ID/connected-apps/$CLIENT_ID/policy \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "block", "reason": "Not approved for customer data"}'

status is one of:

StatusEffect
allowMembers can authorize the app.
blockMembers cannot authorize the app, and every token it holds for the organization is revoked at once. The organization's other apps keep working.
pending_approvalNew authorizations wait for an admin's approval.

An app object:

{
"client_id": "019b28fb-a11e-7641-a28f-e978f892ec10",
"client_name": "Example Connector",
"client_kind": "standard",
"cimd_url": null,
"status": "block",
"reason": "Not approved for customer data",
"policy_set_at": "<ISO 8601 timestamp>"
}

The PUT answers with client_id, status, reason and policy_set_at.

Apps of guests​

A guest is someone from another organization with a role in one of your teams. An app a guest connects, such as an AI assistant, is tied to the guest's own organization, so it does not reach your teams. Your admins can let it read them:

EndpointMethodScopes
/v1/identity/organizations/{orgId}/guest-appsGETidentity / identity:read
/v1/identity/organizations/{orgId}/guest-appsPUTidentity / identity:write

Both take the owner or admin role. PUT takes {"allow_read": true} or false, and it is off until you turn it on. While it is on:

  • An app of a guest reads the teams, projects and templates its person has a role on here, together with the files a design needs to be drawn: its media, fonts and clips.
  • It never changes anything in your teams, whatever role its person has. Writes are refused with organization_not_bound.
  • Its PDFs and other jobs are billed to the guest's own organization.
  • An app you blocked, or hold for approval, under Connected Apps reads nothing.

Turning it off stops the reads within a minute. Both routes answer the setting with the apps that were let read, the latest first. Each app is one of your connected apps, so you block one there:

{
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"allow_read": true,
"apps": [
{
"client_id": "019b28fb-a11e-7641-a28f-e978f892ec10",
"client_name": "Example Connector",
"client_kind": "cimd_virtual",
"cimd_url": "https://connector.example.com/oauth/client.json",
"status": "allow",
"last_guest_read_at": "<ISO 8601 timestamp>"
}
]
}

last_guest_read_at is when the app was last let read for a guest, to the hour. client_kind and cimd_url say where the app comes from, as under Connected Apps. An app that registered itself (cimd_virtual) chose its client_name itself, so two apps can carry one name: the host of cimd_url tells them apart.

The assistant​

The assistant in the Typograph portal works for each person in every organization they belong to, as a member or as a guest. Your owners and admins decide what it may do in your organization, for everyone who works there, and whether guests may spend your organization's AI credits on work in it (AI Assistant Settings):

EndpointMethodScopes
/v1/identity/organizations/{orgId}/assistantGETidentity / identity:read
/v1/identity/organizations/{orgId}/assistantPUTidentity / identity:write

Every member reads it. PUT takes the owner or admin role, and ai_access, guests_use_credits or both: what you leave out stays as it is. Both answer:

{
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"ai_access": "read_write",
"guests_use_credits": false,
"can_change": true,
"can_turn_off": false,
"changed_at": "<ISO 8601 timestamp>",
"is_allowed": true
}
  • ai_access: read_write (the default) lets the assistant read and, after a plan the person confirms, change things; read_only lets it only read, and refuses every change; off keeps it out entirely. A lower setting takes effect within a minute.
  • guests_use_credits: whether people with a role on your teams who are not members may use your organization's AI credits for work in it; false by default. Editors and admins who are members always may; viewers and commenters never do.
  • can_change: whether you may change it, as an owner or admin.
  • can_turn_off: whether off can be chosen now. off is part of the Enterprise plan; an organization that is off already keeps it on every plan.
  • changed_at: when an owner or admin last changed either setting; null when nobody did.
  • is_allowed: deprecated, read ai_access. true unless ai_access is off. As input, false sets ai_access to off, and true sets read_write where the assistant is off, and changes nothing otherwise.

PUT answers 400 for an unknown ai_access, a guests_use_credits that is not a boolean, an empty body, or max_preset, which is gone: what the assistant may do is now the person's own rights within ai_access. It answers 403 forbidden to a member who is no owner or admin, and 403 plan_does_not_allow_ai_off for off on Free or Pro, unless the organization is off already.

Apps​

An organization's apps are the OAuth clients it builds on Typograph. Any member can list and read them, and an owner or admin can give an app the logo its consent screen shows. Creating an app, changing its name, scopes or redirect URIs, deleting it and regenerating its secret happen in the portal only (see Managing OAuth Clients), so no answer here carries a client secret.

EndpointMethodScopes
/v1/identity/oauth/clients?organization_id={orgId}GETidentity / identity:read
/v1/identity/oauth/clients/{clientId}GETidentity / identity:read
/v1/identity/oauth/clients/{clientId}/logoPUTidentity / identity:write
/v1/identity/oauth/clients/{clientId}/logoDELETEidentity / identity:write

List and read apps​

curl "https://api.typograph.nl/v1/identity/oauth/clients?organization_id=$ORG_ID" \
-H "Authorization: Bearer $USER_TOKEN"

The list uses standard pagination: limit (default 10, max 50) and offset, with a meta object. Only a member of the organization gets an answer: anyone else gets a 403 with access_denied, and another organization than the one the token reaches a 403 with organization_not_bound (which one).

An app object:

{
"id": "019b28fb-a11e-7641-a28f-e978f892ec10",
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"name": "Print Shop",
"redirect_uris": ["https://shop.example.com/oauth/callback"],
"allowed_scopes": ["file:read", "publisher"],
"grant_types": ["authorization_code", "refresh_token"],
"is_confidential": true,
"is_active": true,
"allowed_ips": null,
"created_at": "<ISO 8601 timestamp>",
"updated_at": "<ISO 8601 timestamp>",
"logo_url": "https://api.typograph.nl/v1/identity/oauth/clients/019b28fb-a11e-7641-a28f-e978f892ec10/logo?v=<timestamp>"
}

allowed_ips lists the addresses and CIDR ranges the app's credentials and tokens work from, or is null when any address may; an owner or admin sets it in the portal (IP allowlist).

GET /v1/identity/oauth/clients/{clientId} answers a member of the app's organization with the whole app object, when the token reaches that organization. Anyone else gets only what the consent screen shows: id, name, kind, cimd_url, logo_uri, homepage_uri, is_first_party and logo_url. logo_url is null for an app without a logo.

curl -X PUT https://api.typograph.nl/v1/identity/oauth/clients/$CLIENT_ID/logo \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: image/png" \
--data-binary @logo.png

The image is the request body: a square PNG, JPEG or WebP of 128 to 1024 pixels per side, at most 512 KB. Only an app that signs people in (the authorization_code grant) shows a consent screen, so only such an app takes a logo. The answer is the app object with a new logo_url. An image that misses a requirement is a 400 with invalid_logo, and error_description says which one.

DELETE /v1/identity/oauth/clients/{clientId}/logo removes the logo and answers 204; the consent screen shows the placeholder again.

Both take the owner or admin role in the app's organization: a plain member gets a 403, someone outside it a 404, as does a token that reaches another organization. The portal does the same under Apps → ⋯ → Logo (see Authentication → App logo).

Error Responses​

StatusErrorDescription
401unauthorizedMissing or invalid token
400invalid_requestThe body is not valid JSON, not a JSON object, or holds a key the endpoint does not know, named in error_description; or the deletion code is wrong or has expired
400user_already_deletedThe account is already scheduled for deletion
400invalid_logoThe app logo is refused; error_description says why
403access_deniedMissing scope for the requested endpoint
403invalid_token_typeClient token used on a user-only endpoint
403forbiddenPUT /v1/identity/organizations/{orgId}/assistant: the caller is a member but no owner or admin of the organization
403plan_does_not_allow_ai_offPUT /v1/identity/organizations/{orgId}/assistant: ai_access off is part of the Enterprise plan
404not_foundUser, organization, member or app does not exist
409organization_owner_with_membersThe person owns an organization that has other members: they make another member the owner, or remove the others, before deleting their account
409free_organization_limit_reachedPOST /v1/identity/organizations: the person owns an organization on the free plan already, and a second one needs a paid plan
422validation_errorField validation failed
429verification_code_rate_limit_exceededA deletion code was sent moments ago
429verification_rate_limit_exceededToo many wrong deletion codes: locked for a while
503subscription_service_unavailableThe organization's plan could not be read: by GET /v1/identity/oauth/scopes/me, and by POST /v1/identity/organizations, which checks the plans of the organizations the person owns

Standard error envelope (see API Overview → Error Handling):

{
"error": "validation_error",
"error_description": "Request validation failed",
"request_id": "019397ab-cdef-7890-abcd-ef1234567890",
"details": [
{ "field": "name", "code": "required", "message": "Name is required." }
]
}

Managing OAuth Clients​

OAuth applications (client ID / client secret) are created, changed and deleted in the Typograph Portal, not via the public API: that is also where a secret is shown and regenerated. Through the API you can list and read them and change their logo. See Authentication → Getting Credentials.