Authentication
Every request carries an OAuth 2 access token in the Authorization header:
GET /v1/me HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6...Tokens are issued by the identity provider of your MyCLM provider (each provider has its own, e.g. https://<provider>.auth.<domain>), never by the API itself. The identity provider authenticates; the API authorizes. It verifies the signature, issuer and audience of every token, and the OAuth client that obtained it (client_id): only the MyCLM apps, API keys, partner applications and their registered OAuth clients are accepted (otherwise 401 unknown_client).
Choose your mechanism
Section titled “Choose your mechanism”| API key | Partner application (machine) | Connected app (delegated OAuth) | |
|---|---|---|---|
| For | A customer’s own integration | A vendor provisioning its customers | A vendor working for signed-in people |
| Created by | An organization administrator, in the app | MyCLM, on request | MyCLM, on request (OAuth client of the partner application) |
| Grant | client_credentials |
client_credentials |
Authorization code + PKCE, private_key_jwt, after the consent |
| Acts as | Its service account | Its service account | The person who authorized it |
| Sees | One organization, from the node of the key, with its role | The organizations it provisioned or that approved it | The organizations of the person that approved the app and where the person authorized it |
| Rights | The role of the key | Provisioning / administration scopes ∩ role of the grant | Business scopes authorized ∩ rights of the person |
| Rate limit | 600 requests / minute | Per application (600 by default) | Per application and person |
People signed in to the MyCLM apps use the authorization code flow with PKCE (S256) on the same identity provider. A third-party application that works for people uses delegated OAuth: same flow, started on the MyCLM consent page.
All access tokens are valid for 10 minutes.
API keys
Section titled “API keys”An API key is a service account of your organization: attached to a node (organization, entity or group) with a role, like a team member. It sees what that role allows at that node and below, never more than the person who created it.
Create keys in Settings → Integrations → API keys (permission integrations.manage). At creation you get:
| Field | Use |
|---|---|
| Client ID | OAuth client id (client_id). |
| Client secret | OAuth client secret. Shown once. |
| Token URL | Token endpoint of your provider: https://<provider auth domain>/oauth/v2/token. |
| Scope | openid urn:zitadel:iam:org:project:id:<projectId>:aud. The last part sets the token audience to the MyCLM API: without it the API answers 401 invalid_token. |
Options: an expiry date (then 401 api_key_expired) and an IP allowlist (IPv4 / IPv6 addresses and IPv4 CIDR blocks, otherwise 403 ip_not_allowed). Every write request made with a key is recorded in its log, visible in the app.
Get a token
Section titled “Get a token”curl -s -X POST "$MLT_TOKEN_URL" \ -u "$MLT_CLIENT_ID:$MLT_CLIENT_SECRET" \ -d grant_type=client_credentials \ --data-urlencode "scope=$MLT_SCOPE"/** Cached client_credentials token, renewed one minute before it expires. */let cached: { token: string; expiresAt: number } | null = null;
export async function accessToken(): Promise<string> { if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token; const basic = Buffer.from(`${process.env.MLT_CLIENT_ID}:${process.env.MLT_CLIENT_SECRET}`).toString('base64'); const response = await fetch(process.env.MLT_TOKEN_URL!, { method: 'POST', headers: { Authorization: `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', scope: process.env.MLT_SCOPE! }), }); if (!response.ok) throw new Error(`Token request failed: ${response.status} ${await response.text()}`); const body = (await response.json()) as { access_token: string; expires_in: number }; cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 }; return cached.token;}import os, time, requests
_cache = {"token": None, "expires_at": 0}
def access_token() -> str: """Cached client_credentials token, renewed one minute before it expires.""" if _cache["token"] and time.time() < _cache["expires_at"] - 60: return _cache["token"] response = requests.post( os.environ["MLT_TOKEN_URL"], auth=(os.environ["MLT_CLIENT_ID"], os.environ["MLT_CLIENT_SECRET"]), data={"grant_type": "client_credentials", "scope": os.environ["MLT_SCOPE"]}, timeout=10, ) response.raise_for_status() body = response.json() _cache.update(token=body["access_token"], expires_at=time.time() + body["expires_in"]) return body["access_token"]<?phpfunction accessToken(GuzzleHttp\Client $http): string{ static $cache = ['token' => null, 'expiresAt' => 0]; if ($cache['token'] && time() < $cache['expiresAt'] - 60) { return $cache['token']; } $response = $http->post(getenv('MLT_TOKEN_URL'), [ 'auth' => [getenv('MLT_CLIENT_ID'), getenv('MLT_CLIENT_SECRET')], 'form_params' => ['grant_type' => 'client_credentials', 'scope' => getenv('MLT_SCOPE')], ]); $body = json_decode((string) $response->getBody(), true); $cache = ['token' => $body['access_token'], 'expiresAt' => time() + $body['expires_in']]; return $cache['token'];}Tokens are JWTs valid for 10 minutes (expires_in: 600). Cache the token and request a new one when expires_in elapses or when the API answers 401 invalid_token; do not request a token per API call.
Rotate and revoke
Section titled “Rotate and revoke”- Generate a new secret (rotation): the previous secret stays valid for 24 hours, the time to deploy the new one.
- Revoke: every call with the key is refused immediately (
401 api_key_revoked). It cannot be undone.
Partner applications
Section titled “Partner applications”A partner application belongs to a software vendor and works across many organizations of the provider. MyCLM creates it for you with:
- the scopes it may use (e.g.
orgs:write,members:write,signature:write), see Scopes & permissions; - its redirect URIs (HTTPS, or
localhostfor development) for the consent flows; - its rate limit per minute.
It has two kinds of credentials:
- Machine credentials (client id and secret), used exactly like an API key (
client_credentials, same token URL and scope), for provisioning and administration only (orgs:write,nodes:write,members:write): partner provisioning on the organizations it creates, and on the organizations that approved it. - An OAuth client (
private_key_jwt) for the business data (signature requests, registered mail, contacts, tasks), on behalf of the people who authorized it: delegated OAuth.
A call targeting an organization the application has no access to answers 403 consent_required.
Errors
Section titled “Errors”| Status | Code | What to do |
|---|---|---|
| 401 | unauthenticated |
Send Authorization: Bearer <token>. |
| 401 | invalid_token |
Get a new token; check that it was requested with the audience scope. |
| 401 | unknown_issuer |
The token comes from an identity provider other than the one of your provider. |
| 401 | unknown_client |
The token was obtained by an OAuth client MyCLM does not accept: use your own credentials. |
| 401 | api_key_revoked / api_key_expired / partner_app_revoked |
Create or ask for new credentials. |
| 403 | no_account |
The token is valid but has no account in this provider. |
| 403 | ip_not_allowed |
Call from an allowed address, or update the allowlist of the key. |
| 403 | consent_required |
The partner application has no grant on this organization, or the person has not authorized it (delegated token: send them to details.authorizeUrl). |
See the full error catalog.