Integrating a Third-Party App
Guide for third-party applications that want to authenticate users via the Calimatic Auth platform using standard OAuth 2.0 / OpenID Connect.
This guide is for Category 3 apps: new third-party applications that want to let users sign in with their Calimatic account.
Prerequisites
- A web application with an HTTPS-capable callback URL
- An OAuth 2.0 / OIDC client library for your language/framework (any standard library works)
- Contact with a Calimatic administrator (optional -- dynamic registration is available)
1. Register as a Third-Party OAuth Client
Dynamic Registration
Register your application by calling the registration endpoint:
curl -X POST https://auth.calimatic.com/api/v1/oidc/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Third-Party App",
"redirect_uris": [
"https://myapp.com/auth/callback",
"http://localhost:5173/auth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"scope": "openid profile email",
"client_uri": "https://myapp.com",
"logo_uri": "https://myapp.com/logo.png"
}'
Response (201 Created):
{
"client_id": "cca_aBcDeFgHiJkL...",
"client_secret": "ccas_xYzAbCdEfGhI...",
"client_secret_expires_at": 0,
"client_name": "My Third-Party App",
"redirect_uris": [
"https://myapp.com/auth/callback",
"http://localhost:5173/auth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_post",
"scope": "openid profile email"
}
Store the client_secret securely. It is returned only once.
Rate limit: 5 registrations per minute per IP address.
Public Clients (SPAs / Mobile Apps)
If your app runs entirely in the browser (no server-side secret storage), register as a public client:
curl -X POST https://auth.calimatic.com/api/v1/oidc/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My SPA",
"redirect_uris": ["https://myapp.com/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "none",
"scope": "openid profile email"
}'
Public clients do not receive a client_secret and must always use PKCE.
2. Standard OIDC Integration
Discovery URL
Point your OIDC library to the discovery document and it will auto-configure all endpoints:
https://auth.calimatic.com/.well-known/openid-configuration
This advertises:
| Endpoint | URL |
|---|---|
| Authorization | https://auth.calimatic.com/api/v1/oidc/authorize |
| Token | https://auth.calimatic.com/api/v1/oidc/token |
| UserInfo | https://auth.calimatic.com/api/v1/oidc/userinfo |
| JWKS | https://auth.calimatic.com/api/v1/oidc/jwks |
| End Session | https://auth.calimatic.com/api/v1/oidc/end-session |
| Introspection | https://auth.calimatic.com/api/v1/oidc/introspect |
| Revocation | https://auth.calimatic.com/api/v1/oidc/revoke |
Scopes
Third-party apps receive the standard OIDC scopes:
| Scope | Claims Granted |
|---|---|
openid | sub (required) |
profile | name, given_name, family_name, picture, updated_at |
email | email, email_verified |
phone | phone_number |
Third-party apps cannot request organization or permissions scopes unless explicitly approved by a Calimatic administrator.
PKCE (Required)
PKCE (Proof Key for Code Exchange) is required for all clients. Use the S256 method:
code_verifier = random 43-128 character string
code_challenge = BASE64URL(SHA256(code_verifier))
Authorization Flow
1. Your app redirects the user to /api/v1/oidc/authorize
with client_id, redirect_uri, response_type=code,
scope, state, code_challenge, code_challenge_method=S256
2. User authenticates on the Calimatic login page
3. User approves consent (first-time only)
4. Calimatic redirects to your callback:
https://myapp.com/auth/callback?code=abc123&state=xyz
5. Your server exchanges the code for tokens:
POST /api/v1/oidc/token
with code, redirect_uri, client_id, client_secret, code_verifier
6. You receive access_token, id_token, refresh_token
7. Fetch user profile:
GET /api/v1/oidc/userinfo
Authorization: Bearer <access_token>
Example: Node.js / Express
import { randomBytes, createHash } from "crypto";
import express from "express";
const app = express();
const ISSUER = "https://auth.calimatic.com";
const CLIENT_ID = process.env.CALIMATIC_CLIENT_ID!;
const CLIENT_SECRET = process.env.CALIMATIC_CLIENT_SECRET!;
const REDIRECT_URI = "https://myapp.com/auth/callback";
// Generate PKCE pair
function generatePKCE() {
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
// Login route
app.get("/login", (req, res) => {
const { verifier, challenge } = generatePKCE();
const state = randomBytes(16).toString("hex");
// Store in session for callback verification
req.session.oauthState = state;
req.session.codeVerifier = verifier;
const url = new URL(`${ISSUER}/api/v1/oidc/authorize`);
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("redirect_uri", REDIRECT_URI);
url.searchParams.set("response_type", "code");
url.searchParams.set("scope", "openid profile email");
url.searchParams.set("state", state);
url.searchParams.set("code_challenge", challenge);
url.searchParams.set("code_challenge_method", "S256");
res.redirect(url.toString());
});
// Callback route
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send("Invalid state");
}
// Exchange code for tokens
const tokenRes = await fetch(`${ISSUER}/api/v1/oidc/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code as string,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: req.session.codeVerifier,
}),
});
const tokens = await tokenRes.json();
// Fetch user info
const userRes = await fetch(`${ISSUER}/api/v1/oidc/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = await userRes.json();
// Create session in your app
req.session.user = user;
req.session.accessToken = tokens.access_token;
res.redirect("/dashboard");
});
Example: Python / Flask
import os, hashlib, base64, secrets
from flask import Flask, redirect, request, session
app = Flask(__name__)
ISSUER = "https://auth.calimatic.com"
CLIENT_ID = os.environ["CALIMATIC_CLIENT_ID"]
CLIENT_SECRET = os.environ["CALIMATIC_CLIENT_SECRET"]
REDIRECT_URI = "https://myapp.com/auth/callback"
def generate_pkce():
verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
return verifier, challenge
@app.route("/login")
def login():
verifier, challenge = generate_pkce()
state = secrets.token_hex(16)
session["oauth_state"] = state
session["code_verifier"] = verifier
url = (
f"{ISSUER}/api/v1/oidc/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope=openid+profile+email"
f"&state={state}"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
return redirect(url)
@app.route("/auth/callback")
def callback():
if request.args.get("state") != session.get("oauth_state"):
return "Invalid state", 400
import requests
tokens = requests.post(f"{ISSUER}/api/v1/oidc/token", data={
"grant_type": "authorization_code",
"code": request.args["code"],
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code_verifier": session["code_verifier"],
}).json()
user = requests.get(f"{ISSUER}/api/v1/oidc/userinfo", headers={
"Authorization": f"Bearer {tokens['access_token']}"
}).json()
session["user"] = user
return redirect("/dashboard")
3. What Data Third-Party Apps Receive
With the standard openid profile email scopes, your app receives:
{
"sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Jane Smith",
"given_name": "Jane",
"family_name": "Smith",
"email": "jane@school.com",
"email_verified": true,
"picture": "https://lh3.googleusercontent.com/photo.jpg",
"updated_at": 1700000000
}
What You Do NOT Receive
- Organization details (
organization_id,organization_memberships) - Platform permissions or roles
- Phone number (unless
phonescope is requested) - Internal user type
To request extended scopes, contact a Calimatic administrator to have your client approved for organization or permissions scopes.
4. User Provisioning via API
If your app needs to create users within a Calimatic organization (e.g., onboarding users from your platform), use the Provisioning API:
curl -X POST https://auth.calimatic.com/api/v1/users/provision \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d '{
"email": "newuser@example.com",
"firstName": "New",
"lastName": "User",
"organizationId": "org-uuid-here",
"role": "member",
"sendInviteEmail": true
}'
const response = await fetch("https://auth.calimatic.com/api/v1/users/provision", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": CLIENT_ID,
"x-client-secret": CLIENT_SECRET,
},
body: JSON.stringify({
email: "newuser@example.com",
firstName: "New",
lastName: "User",
organizationId: orgId,
role: "member",
sendInviteEmail: true,
}),
});
When your app client provisions a user, it automatically receives a license for your application.
For the full API reference including bulk provisioning, see User Management API.
5. Set Up Webhooks for User Events
Subscribe to user lifecycle events so your app stays in sync with Calimatic Auth:
curl -X POST https://auth.calimatic.com/api/v1/admin/webhooks \
-H "Content-Type: application/json" \
-H "x-client-id: cca_your_client_id" \
-H "x-client-secret: ccas_your_client_secret" \
-d '{
"url": "https://myapp.com/webhooks/calimatic",
"events": [
"user.created",
"user.updated",
"user.deleted",
"user.suspended",
"user.reactivated"
],
"secret": "whsec_your_webhook_secret_here",
"description": "Sync user changes to MyApp"
}'
Available Event Types
| Event | Triggered When |
|---|---|
user.created | A new user is provisioned |
user.updated | User profile is updated |
user.deleted | User is deleted |
user.suspended | User account is suspended |
user.reactivated | Suspended user is reactivated |
user.deactivated | User account is deactivated |
user.password_reset | Password reset is triggered |
user.invited | User receives an invitation |
organization.created | New organization is created |
organization.updated | Organization details are updated |
license.assigned | App license is assigned to a user |
license.revoked | App license is revoked from a user |
Webhook Payload Format
{
"event": "user.created",
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"userId": "uuid-...",
"email": "jane@school.edu",
"firstName": "Jane",
"lastName": "Smith",
"organizationId": "org-uuid-...",
"isNewUser": true,
"createdAt": "2025-01-15T10:30:00.000Z"
}
}
Verifying Webhook Signatures
If you provided a secret, each delivery includes an X-Webhook-Signature header. Verify it using HMAC-SHA256:
import { createHmac } from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(payload)
.digest("hex");
return signature === expected;
}
// In your webhook handler
app.post("/webhooks/calimatic", (req, res) => {
const signature = req.headers["x-webhook-signature"] as string;
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
// Process the event
const { event, data } = req.body;
// ...
res.status(200).send("OK");
});
For the full webhook API reference, see Webhooks API.
6. SCIM Integration for Enterprise Provisioning
For enterprise customers that need automated user provisioning and deprovisioning, Calimatic Auth supports SCIM 2.0.
SCIM (System for Cross-domain Identity Management) allows identity providers like Azure AD, Okta, and OneLogin to automatically sync users with your application via Calimatic Auth.
For SCIM endpoints, schema mapping, and configuration details, see the SCIM API Reference.
7. Rate Limits and Best Practices
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
| Token endpoint | 20 requests | 1 minute per client |
| Authorization endpoint | 30 requests | 1 minute per IP |
| UserInfo endpoint | 60 requests | 1 minute per token |
| Introspection endpoint | 60 requests | 1 minute per client |
| Registration endpoint | 5 requests | 1 minute per IP |
When rate limited, you receive a 429 response with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers.
Best Practices
Token management:
- Cache access tokens and reuse them until they expire (default: 1 hour)
- Refresh tokens proactively 5 minutes before expiry
- Never store refresh tokens in localStorage for browser apps
Security:
- Always use PKCE (S256), even for confidential clients
- Always validate the
stateparameter on callback to prevent CSRF - Verify JWTs locally using the JWKS endpoint instead of calling introspection for every request
- Store
client_secretin environment variables, never in source code
Error handling:
- Handle
access_deniedgracefully (user may deny consent) - On
invalid_token, attempt a token refresh before prompting re-login - Implement exponential backoff for
429responses
User data:
- Cache user profile data in your own database to reduce calls to userinfo
- Use webhooks to stay synchronized instead of polling
Redirect URIs:
- Register exact redirect URIs (no wildcards)
- Always use HTTPS in production
- Register separate URIs for each environment (production, staging, development)
Related Documentation
- OIDC API Reference -- Full reference for all OAuth/OIDC endpoints
- Integration Guide -- Detailed integration examples for Next.js, React SPA, Python, PHP, and more
- User Management API -- Provisioning and managing users
- Webhooks API -- Webhook registration and event types
- SCIM API Reference -- SCIM 2.0 endpoints for enterprise provisioning