Connected apps (OAuth consent)
When a customer already uses MyCLM, your partner application cannot provision their organization: an administrator of that organization approves it instead, on a consent page, like an OAuth “Sign in with…” flow.
The approval sets the maximum scopes of your application in the organization:
- business scopes (
signature,registered_mail,contacts,tasks) are used by delegated tokens, for the people who authorize your application (or for every member when the administrator consents on their behalf); - administration scopes (
nodes:write,members:write) are used by the machine credentials of your application (client_credentials) on this organization.
- POST
/v1/oauth/grants/exchangeExchange an authorization code - GET
/v1/oauth/grantsList the grants of the application
The flow
Section titled “The flow”Your app MyCLM app (consent page) MyCLM API │ 1. redirect the admin ──►│ │ │ │ 2. admin signs in, picks a role, approves │ │◄── 3. redirect_uri?code=…&state=… ─┘ │ │ 4. POST /v1/oauth/grants/exchange { code, redirectUri, codeVerifier } ──►│ │◄──────────────────────────────── grant (orgId, scopes) ──────────────────│ │ 5. call the API on this organization with your application token ───────►│-
Redirect the administrator to the consent page of their provider app:
https://<provider app domain>/oauth/consent?client_id=<your client id>&org_id=<organization id>&scopes=signature:write contacts:read&redirect_uri=https://app.immocrm.fr/integrations/mylegitech/callback&state=<random value you check on return>&code_challenge=<BASE64URL(SHA256(code_verifier))>&code_challenge_method=S256Parameter Description client_idClient id of your partner application. org_idOrganization to connect. Today the administrator gives it to you (it is in the address of the organization pages of the app, /org/<orgId>/…).scopesSpace or comma separated scopes. Only scopes declared on your application are kept; orgs:writecannot be requested here. Omitted: every scope of your application that can be consented.redirect_uriMust be one of the redirect URIs registered on your application (HTTPS, or localhostin development).stateOpaque value returned as is: bind it to the user session to prevent CSRF. code_challenge,code_challenge_methodOptional PKCE ( S256only). Recommended. -
The administrator decides. They need the
org.managepermission on the organization (a direct membership: administrators of a parent organization cannot approve applications on a sub-organization). The page shows your application, the requested scopes, and the roles they may give it (never more rights than theirs). They can also consent on behalf of all members (delegated tokens of every member are then authorized without asking them). With machine credentials, your application gets the administration scopes ∩ role on this organization; members it provisions never get a role above that role, norowner. -
You receive the result on
redirect_uri:- approved:
?code=<authorization code>&state=… - denied:
?error=access_denied&state=…
An unknown
client_idor an unregisteredredirect_urinever redirects (the page shows an error instead). - approved:
-
Exchange the code (single use, valid 10 minutes) with your application token:
Terminal window curl -s -X POST "$MLT_API_URL/v1/oauth/grants/exchange" \-H "Authorization: Bearer $MLT_TOKEN" -H "Content-Type: application/json" \-d '{ "code": "'"$CODE"'", "redirectUri": "https://app.immocrm.fr/integrations/mylegitech/callback", "codeVerifier": "'"$CODE_VERIFIER"'" }'import { createHash, randomBytes } from 'node:crypto';// Before redirecting: keep codeVerifier in the user session, send codeChallenge.const codeVerifier = randomBytes(32).toString('base64url');const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');// On the callback:const grant = await fetch(`${process.env.MLT_API_URL}/v1/oauth/grants/exchange`, {method: 'POST',headers: { Authorization: `Bearer ${appToken}`, 'Content-Type': 'application/json' },body: JSON.stringify({ code, redirectUri: 'https://app.immocrm.fr/integrations/mylegitech/callback', codeVerifier }),}).then((r) => r.json());// { grantId, orgId, orgName, scopes, grantedAt }import base64, hashlib, secrets# Before redirecting: keep code_verifier in the user session, send code_challenge.code_verifier = secrets.token_urlsafe(32)code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).rstrip(b"=").decode()# On the callback:grant = requests.post(f"{api}/v1/oauth/grants/exchange", headers=app_headers, json={"code": code,"redirectUri": "https://app.immocrm.fr/integrations/mylegitech/callback","codeVerifier": code_verifier,}).json()// Before redirecting: keep $codeVerifier in the user session, send $codeChallenge.$codeVerifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');$codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '=');// On the callback:$grant = json_decode((string) $http->post("$api/v1/oauth/grants/exchange", ['headers' => $appHeaders,'json' => ['code' => $_GET['code'], 'redirectUri' => 'https://app.immocrm.fr/integrations/mylegitech/callback', 'codeVerifier' => $codeVerifier],])->getBody(), true);A wrong, expired or reused code, or a mismatching
redirectUri/codeVerifier, answers422 invalid_grant. -
Use the grant. Your machine token now reaches
grant.orgIdfor the administration scopes (nodes, members). To work on the business data (signature requests, contacts…), act for the people of the organization with delegated tokens.GET /v1/oauth/grantslists the organizations your application can currently access.
Revocation
Section titled “Revocation”Administrators see connected apps in Settings → Integrations → Connected apps: the approved scopes, the members who authorized the application, the “all members” consent (on or off), and can revoke access at any time. Calls on that organization then answer 403 consent_required (with details.orgId), for machine and delegated tokens alike, and it disappears from GET /v1/oauth/grants. To reconnect, run the consent flow again.