Skip to main content
Use OAuth 2.0 when you’re building a partner application — a Salesforce app, a HubSpot extension, a third-party tool — that acts on behalf of individual Yotel users rather than your own tenant. For your own server-to-server automation inside your tenant, API keys are simpler. OAuth is for when users from other Yotel tenants install your app.

Flows

Yotel supports three grant types:
Grant typeWhen to use
authorization_code (with PKCE)Interactive — user consents in a browser. SPAs, installed web apps, Salesforce, HubSpot. Default choice.
refresh_tokenRenew an expired access token without user reauth. Rotated on every refresh.
urn:ietf:params:oauth:grant-type:jwt-bearerServer-to-server. No user browser. Partner signs a short-lived JWT with their private key.

Discovery

Our provider implements OpenID Connect Discovery. Fetch /.well-known/openid-configuration to auto-configure any OIDC client library:
curl https://api.yotel.in/.well-known/openid-configuration
Key endpoints:
EndpointPurpose
/oauth/authorizeRedirect here to start interactive flow
/oauth/tokenExchange code / refresh / JWT assertion for tokens
/oauth/introspectRFC 7662 — check if a token is still active
/oauth/revokeRFC 7009 — explicitly invalidate a token
/oauth/userinfoOIDC — fetch the user’s profile
/.well-known/jwks.jsonRSA public keys for offline JWT verification

Registering a client

Tenant admins register partner apps in the Yotel dashboard (Connected Apps settings). Each client gets:
  • client_id — public identifier, safe to ship in app metadata
  • client_secret — confidential; show once at creation time
  • An allowlist of redirect_uris
  • An allowlist of scopes this client may request
  • (Optional) A registered public key PEM for JWT Bearer

Authorization Code + PKCE

The interactive flow for SPAs and traditional web apps. PKCE (RFC 7636) is required for all clients — no plain method accepted.
from yotel.oauth import AuthorizationCodeFlow, generate_code_verifier

flow = AuthorizationCodeFlow(
    client_id="sfdc_yotel",
    client_secret=os.environ["YOTEL_CLIENT_SECRET"],
    redirect_uri="https://app.your-crm.com/oauth/callback",
    scopes=["campaigns:read", "leads:write", "openid", "offline_access"],
)

# Step 1 — build the URL to redirect the user's browser to
verifier = generate_code_verifier()
url = flow.build_authorize_url(state="csrf-123", code_verifier=verifier)
# (save `verifier` + `state` in the user's session cookie)

# Step 2 — callback handler on your server
tokens = flow.exchange_code(
    code=request.query["code"],
    code_verifier=verifier,
)
# tokens.access_token, tokens.refresh_token, tokens.expires_at
Same shape in TypeScript:
import { AuthorizationCodeFlow, generateCodeVerifier } from "@yotel/client";

const flow = new AuthorizationCodeFlow({
  clientId: "sfdc_yotel",
  clientSecret: process.env.YOTEL_CLIENT_SECRET!,
  redirectUri: "https://app.your-crm.com/oauth/callback",
  scopes: ["campaigns:read", "leads:write", "openid", "offline_access"],
});

const verifier = generateCodeVerifier();
const url = await flow.buildAuthorizeUrl({ state: "csrf-123", codeVerifier: verifier });
// ... redirect, callback ...
const tokens = await flow.exchangeCode({ code, codeVerifier: verifier });

Required parameters

ParamValue
response_typecode
client_idFrom Connected Apps
redirect_uriMust exact-match one of the registered redirect URIs
scopeSpace-separated scopes — must be subset of client’s allowed_scopes
stateYour CSRF token. Verify on callback
code_challengeS256 hash of your verifier
code_challenge_methodS256
nonceOptional but recommended when using OIDC openid scope

JWT Bearer (RFC 7523)

Salesforce’s async Apex jobs, batch integrations, any server-to-server context where a user browser isn’t available.
from yotel.oauth import JWTBearerFlow

flow = JWTBearerFlow(
    client_id="sfdc_yotel",
    issuer="https://my-salesforce-org.my.salesforce.com",
    subject="user-uuid-to-impersonate",
    tenant_id="tenant-uuid",
    private_key_pem=open("private.pem").read(),
)
tokens = flow.request_token()
# No refresh token issued — re-assert on expiry (5-min TTL default)
The assertion is an RS256 JWT with claims:
  • iss — your registered issuer
  • sub — the user to impersonate
  • aud — your client_id
  • tenant_id — the Yotel tenant
  • iat / exp — 5-minute TTL enforced server-side

Refresh

Tokens default to 1-hour TTL. Refresh before expiry using the refresh_token returned with the access token:
tokens = tokens.refresh()  # returns a new TokenSet with rotated refresh
Rotation policy: every refresh invalidates the old refresh_token and issues a new one. Reusing an old refresh token triggers a cascade-revoke of the entire token family — so you can detect when a refresh_token has leaked.

Token format

Access tokens are RS256-signed JWTs. Claims include:
{
  "sub": "user-uuid",
  "tenant_id": "tenant-uuid",
  "scope": "campaigns:read leads:write",
  "iss": "https://api.yotel.in",
  "aud": "your-client-id",
  "exp": 1718000000,
  "iat": 1717996400,
  "jti": "unique-token-id"
}
You can verify tokens offline using our JWKS endpoint — no introspection round-trip needed.

Scopes

See Scope reference for the full catalog. Common ones:
  • openid profile email — OIDC identity claims
  • offline_access — required to receive a refresh_token
  • campaigns:read, campaigns:write
  • leads:read, leads:write
  • calls:read, calls:write
  • agents:read

Security review

Partners embedding our OAuth in their AppExchange / marketplace listings: our provider satisfies the baseline requirements —
  • OAuth 2.1 + PKCE (no plain method)
  • HTTPS-only endpoints
  • Refresh-token rotation with reuse detection
  • Per-client allowlisted scopes and redirect URIs
  • Token introspection + revocation per RFCs 7662 / 7009
  • Pen-test artifact available under NDA