Developers › Apps & OAuth
Developer Reference OAuth 2.1 / OIDC LeadLogiX is an OAuth 2.1 authorization server. If you are building software that other LeadLogiX customers install, this is how it gets access: the customer approves it on a consent screen, your application receives a token scoped to exactly one workspace, and the customer can withdraw it at any time without involving you.
Build it in this order Five steps, one journey. Each links to the page that owns it. Decide you need OAuth at all If you are writing code against your own workspace, stop here and use an ingestion key — this whole journey is for software other people install . The table below settles it. Get a client_id Register a client . Open registration — no approval, no account with us, no waiting. Work out what you may ask for Scopes & limits , before you write the authorize URL. A requested scope is not a granted one: three independent ceilings apply. Implement the flow The authorization code flow below — PKCE, S256 only — and the complete client at the end of this page. Call something with the token MCP → Hosted server is the data plane; there is no separate REST API for reading leads with an OAuth token. Only signing people in? Go to Sign in with LeadLogiX instead — openid plus workspace:read is enough and no workspace grant is involved. Building your product on that identity? Build on LeadLogiX identity .
When to use this instead of an API key Your situation Use Code you run against your own workspace An ingestion key ( val_key__… ) An application other customers install OAuth — this page An AI agent that should read a customer's leads OAuth + the MCP endpoint Signing people in to your own product OAuth + OpenID Connect The difference that matters is who holds the credential. An API key is the customer's, shared with you, valid for everything that key can do until someone remembers to rotate it. An OAuth token is yours , limited to the permissions the customer approved, bound to one workspace, and revocable from a screen in their own settings. The whole decision, as a picture, is on the Quickstart .
Two planes One switch — the resource parameter — and the single most common integration mistake against this server. One authorization server issues tokens for two planes, and the resource parameter decides which. Ask for openid with no resource and the token is for the identity plane (/idp/userinfo); send resource=https://<host>/mcp without openid and it is for the data plane (POST /mcp). A token minted for one plane is refused by the other. Your request The token is for Use it against scope includes openid , no resource The identity plane /idp/userinfo and the ID token no openid , plus resource=https://<host>/mcp The data plane POST /mcp A token minted for one plane is refused by the other — and the refusal does not say “plane” You get 401 invalid_token with error_reason: token_wrong_audience . If you need both, run two authorizations: one with openid for identity, one with an explicit resource for data. Ask for the resource exactly as you will call it — a trailing slash is a different resource.
Workspace switch Consent succeeds, every call 403s — the failure that wastes an afternoon. Authorized, and still refused: application access is a per-workspace switch Connected-application access is off until a LeadLogiX administrator turns it on for that workspace , and it is not checked during authorization — only on requests. So the sign-in works, the customer approves you, your application appears in their Connected Apps list, the token introspects as valid, and every call to /mcp returns 403 with error_reason: mcp_not_enabled . Retrying and re-authorizing will not fix it. Treat that code as “ask the customer's administrator to have connected applications enabled for this workspace”, never as a broken credential.
Refresh rotation Refresh tokens rotate with no grace window. Read this before you write any retry logic. Presenting a refresh token twice revokes the whole authorization Every refresh returns a new refresh token and invalidates the old one immediately. Replaying a consumed one is treated as theft: the entire grant is revoked, every token issued under it stops working, and the customer must authorize your application again. Redeeming an authorization code twice does the same. Do Never Persist the new refresh token BEFORE you use the access token it arrived with Retry a refresh with the old value after a timeout or a dropped response Treat invalid_grant on refresh as “start a new authorization” Treat invalid_grant as “try again” Serialise refreshes per grant — one in flight at a time Let two workers refresh the same grant concurrently A retry-on-timeout around the refresh call converts one dropped response into a lost integration. This is the one part of an OAuth client worth writing by hand rather than accepting from a code generator — assistants routinely produce exactly that retry loop.
Discovery Read the endpoints from metadata; never copy them from a page. Document Path Authorization server metadata (RFC 8414) /.well-known/oauth-authorization-server OpenID configuration /.well-known/openid-configuration Protected resource metadata (RFC 9728) /.well-known/oauth-protected-resource/mcp The endpoints those documents name Endpoint Path Purpose Authorization /idp/authorize Where you send the customer Token /idp/token Code exchange and refresh Registration /idp/register Dynamic client registration Revocation /idp/revoke Hand a token back when you are done with it Introspection /idp/introspect Check one of your own tokens; never another client's UserInfo /idp/userinfo Claims for the approver, with an openid token Pushed authorization request /idp/par Optional. Takes the authorization parameters and returns a request_uri , keeping a long URL out of browser history, referrers and logs End session /idp/session/end Sign the customer out of LeadLogiX — not a disconnect, see below JWKS /idp/jwks Public signing keys for ID tokens The protected-resource document names the canonical resource identifier you send as resource — a stable identity, not whatever host you happened to call: GET /.well-known/oauth-protected-resource/mcp { "resource": "https://leadval.evas.live/mcp", "authorization_servers": ["https://leadval.evas.live"], "bearer_methods_supported": ["header"], "scopes_supported": ["automations:run", "files:read", "files:write", "identity:lookup", "leads:read", "leads:read.contact", "memory:read", "memory:write", "profiles:read", "profiles:read.contact", "tools:write", "workspace:read", "workspace:read.contact"] } Signing out is not disconnecting /idp/session/end ends the customer's sign-in session; it deliberately does not revoke the access they granted you, so a nightly sync survives the admin who set it up signing out. To give access back, call /idp/revoke ; the customer takes it away from Connected Apps. A post_logout_redirect_uri you did not register is refused rather than followed.
What this server implements Every row was checked against the running server, not against an intention. Specification Status What you can rely on OAuth 2.1 (draft) Implemented Authorization code only. response_types_supported is ["code"] and grant_types_supported is ["authorization_code","refresh_token"] — no implicit, no password, no client credentials. RFC 6749 · OAuth 2.0 core Implemented The framework the above profiles. Error responses use its dialect at every /idp endpoint. RFC 6750 · Bearer tokens Header only Presented in Authorization: Bearer ; never read from the query string or the body. Access tokens are opaque , not JWTs (RFC 9068 is not used) — do not parse one; its meaning is re-derived at every request, which is what lets a revocation or a demotion take effect immediately. RFC 7636 · PKCE Required code_challenge_methods_supported is ["S256"] . There is no plain method. Required for every public client; a LeadLogiX-curated confidential client may omit it only if it sends nonce , which RFC 9700 §2.1.1 names as the alternative. RFC 8414 · AS metadata Implemented Served at the bare origin. issuer is https://leadval.evas.live — the endpoints live under /idp/* but the issuer does not. OpenID Connect Core & Discovery Implemented subject_types_supported: ["public"] , ID tokens signed RS256 , keys at /idp/jwks . Claims for openid , email , profile . RFC 9728 · Protected resource metadata Implemented Describes /mcp and points back at this server, so an MCP client discovers where to authorize instead of being configured with it. RFC 8707 · Resource indicators Implemented The resource parameter selects the audience. It is what separates the identity plane from the data plane. RFC 9207 · Issuer identification Implemented Authorization responses carry iss , including error redirects. A client with more than one provider can tell who answered. RFC 9126 · Pushed authorization requests Optional /idp/par . Every rule on this page applies identically either way. RFC 7591 · Dynamic client registration Open No initial access token. Registering yourself grants no more than any other route would. RFC 7592 · Registration management Not enabled Deliberate. GET on your registration works with its access token; PUT and DELETE return 404 — see Register a client . RFC 7009 · Token revocation Implemented /idp/revoke . Revoking a refresh token ends the authorization. RFC 7662 · Token introspection Implemented /idp/introspect , own tokens only. The fastest way to see the granted scope and audience of a token you hold. RFC 8252 · Native apps Implemented §7.3 loopback redirects only: http://127.0.0.1 and http://[::1] . localhost is refused. OIDC RP-Initiated Logout Implemented end_session_endpoint at /idp/session/end . OIDC Back-Channel Logout Implemented Register a backchannel_logout_uri to be told when a session ends. backchannel_logout_session_supported is true, so tokens carry sid . Model Context Protocol authorization 2025-11-25 A compliant MCP client needs only the endpoint URL; the 401 challenge leads it to everything else. RFC 9700 · OAuth security BCP Followed Exact redirect matching, no plain-HTTP redirect outside loopback, refresh rotation, and §2.1.1's code-injection defence — PKCE for public clients, nonce for a curated confidential one. Failed client authentication at /idp/token is rate limited. RFC 9449 · DPoP Off Deliberately not advertised. The resource server accepts Bearer only, and advertising DPoP would instruct a correct client to send proofs this endpoint would reject. If your library enables DPoP whenever a server seems to offer it, leave it off here. Implicit, hybrid, password and client-credentials grants Refused Every token traces to a person who approved it. Unattended machine-to-machine access uses an ingestion key. Conformance is asserted on every build The table above is not maintained by hand. A test reads the live discovery documents and fails if any advertised value stops matching what this page claims, so a configuration change cannot quietly turn a documented guarantee into a false one.
Choices a specification left open The four that change how a client behaves. Decision What we chose Why it matters to you Client authentication none , client_secret_basic and client_secret_post Public clients are first-class — PKCE is the protection, not a secret embedded in a native app or an SPA. Confidential clients exist for identity products that cannot do PKCE, but LeadLogiX mints those; there is no self-service path to one. Refresh token rotation On, with no grace window Replaying a consumed refresh token revokes the whole authorization — the rule . Audience binding Decided by resource , and by whether openid was asked for A token minted for one plane is refused by the other — two planes . Scope granting Intersection, never a failure Asking for a scope you cannot have completes the flow without it. Read the granted scope from the token response — Scopes & limits .
Verify it yourself Everything above is observable without an account. # Authorization server metadata (RFC 8414) — issuer, endpoints, algorithms curl -s https://leadval.evas.live/.well-known/oauth-authorization-server | jq # OpenID Provider metadata — claims, subject types, logout endpoints curl -s https://leadval.evas.live/.well-known/openid-configuration | jq # Protected resource metadata (RFC 9728) — what /mcp is and who authorizes for it curl -s https://leadval.evas.live/.well-known/oauth-protected-resource | jq # Signing keys (RFC 7517) curl -s https://leadval.evas.live/idp/jwks | jq # Registration management is off (RFC 7592) — both return 404 curl -s -o /dev/null -w '%{http_code}\n' -X PUT https://leadval.evas.live/idp/register/any curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://leadval.evas.live/idp/register/any import requests BASE = "https://leadval.evas.live" meta = requests.get(f"{BASE}/.well-known/oauth-authorization-server", timeout=10).json() print(meta["code_challenge_methods_supported"]) # ["S256"] print(meta["grant_types_supported"]) # ["authorization_code", "refresh_token"] print("dpop_signing_alg_values_supported" in meta) # False — Bearer only # Registration management is off (RFC 7592) — both return 404 for method in ("PUT", "DELETE"): print(method, requests.request(method, f"{BASE}/idp/register/any", timeout=10).status_code)
Token lifetimes And what ends each one early. Token Lifetime Notes Authorization code 60 seconds Single use — a second redemption revokes the grant Access token 1 hour Opaque. Stops working at once on a disconnect, not at expiry ID token 5 minutes A JWT — verify it on receipt; do not hold it Refresh token 30 days Rotated on every use — no grace window Grant 30 days of inactivity Refreshing extends it Customers see every connected application under Settings → Connected Apps and can disconnect it at any moment without telling you. Handle a sudden 401 as a normal state — stop calling and prompt to reconnect rather than retrying — and revoke your own tokens when a customer uninstalls you: curl -X POST https://leadval.evas.live/idp/revoke \ -d token=<access or refresh token> \ -d client_id=<your client id> import requests requests.post("https://leadval.evas.live/idp/revoke", data={"token": refresh_token, "client_id": CLIENT_ID}, timeout=10)
Test against the live server No account of ours, and nothing to ask us for. The authorization server is public and self-service. Register a client with a loopback redirect URI such as http://127.0.0.1:8976/callback ( http://localhost is refused), open the authorization URL in a real browser, approve as a LeadLogiX user, and inspect what comes back. A flow that never reaches consent is almost always a redirect-URI or PKCE mismatch, and the error page names which. curl -s -X POST https://leadval.evas.live/idp/introspect \ -d token=<access-token> \ -d client_id=<your-client-id> # Check scope, aud and exp before writing more code. import requests info = requests.post("https://leadval.evas.live/idp/introspect", data={"token": access_token, "client_id": CLIENT_ID}, timeout=10).json() print(info.get("active"), info.get("scope"), info.get("aud"), info.get("exp")) Before you ship, check Why it bites later Endpoints read from discovery, not copied A moved endpoint breaks every deployed copy of your client at once. A fresh code_verifier per authorization, stored per browser session Reusing one makes the code interceptable — the attack PKCE exists to stop. You store the rotated refresh_token before using the response Rotation means the old one is dead. Keeping it logs the customer out on the next refresh. A 401 triggers a refresh, not a re-authorization Sending the customer back to consent on every expiry is the commonest cause of “this app keeps asking me to sign in”. Your resource matches what you call A token minted for one plane is rejected by the other. You tested a DEMOTION, not only a grant Authority is re-derived per request; a client that caches a role keeps acting on one the person lost.
Getting your application reviewed Every application starts unreviewed , and the consent screen says so. Review changes that label and adds your logo; it grants nothing — see Review is a badge, not a permission . To start one, contact support@evas.live .