Business Platforms
Embed LLC formation, EIN applications, and registered agent services into your SaaS.
Legal Tech
Let users generate NDAs, operating agreements, and contracts from your app.
CRMs & HR Tools
Pull user documents and templates into your CRM or team management platform.
Marketplaces
Offer "Sign in with EasyLegal AI" so users carry their legal vault with them.
Automation
Trigger document generation via API when business events occur (new hire, new client, etc.).
Zero Cost
Free to build. Free to ship. Usage is billed to the end user, never to you.
Platform Overview
EasyLegal AI is an AI-powered legal document platform serving small businesses, freelancers, and entrepreneurs. Our API lets third-party applications tap into the full EasyLegal AI ecosystem:
330+ Legal Templates
NDAs, contracts, operating agreements, lease agreements, and more — all AI-customizable.
AI Document Generation
Smart questionnaires generate personalized, jurisdiction-aware legal documents in minutes.
E-Signature System
Built-in electronic signatures with audit trails, multi-party signing, and verification.
Business Formation
LLC formation, EIN applications, registered agent services, and foreign qualification.
Document Vault
Secure cloud storage with folders, tags, reminders, and sharing controls.
Multi-Region Support
Templates and services localized for US, UK, Nigeria, and more regions.
Authentication Model
We use OAuth 2.0 Authorization Code with optional PKCE (Proof Key for Code Exchange) for public clients. This is the same industry-standard flow used by Google, GitHub, and Stripe.
Base URLs
| Service | URL |
|---|---|
| Authorization | https://easylegal-ai.com/oauth/authorize |
| Token Exchange | https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token |
| User Info | https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo |
| Token Revocation | https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-revoke |
| Discovery (dynamic) | https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-discovery |
| Discovery (static) | https://easylegal-ai.com/.well-known/openid-configuration |
OpenID Connect Discovery
We publish a standard .well-known/openid-configuration document for automatic client configuration. Libraries like openid-client (Node.js), authlib (Python), and oidc-client-ts (browser) can auto-discover all endpoints from this single URL:
https://easylegal-ai.com/.well-known/openid-configurationRequired Headers
All API calls (token, userinfo, revoke) require the apikey header:
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8YQuick Start — 5 Minutes
Go from zero to fetching user data in five steps
Register your app
Go to the Developer Portal and click "Register App". Choose your scopes and add your redirect URI. It's free — no credit card required.
Wait for approval
Our team reviews new apps within 24-48 hours. You'll receive your client_id and client_secret upon approval.
Redirect users to authorize
Send users to our authorization URL. They'll see a consent screen showing what permissions you're requesting.
Exchange code for tokens
After the user approves, we redirect back to your app with an authorization code. Exchange it for access + refresh tokens.
Call the API
Use the access token to fetch user data, documents, templates, and business service orders.
Minimal Example (JavaScript)
// Redirect user to EasyLegal AI
const authorizeUrl = 'https://easylegal-ai.com/oauth/authorize?' + new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.com/callback',
scope: 'profile documents:read',
state: crypto.randomUUID(),
});
window.location.href = authorizeUrl;
// --- In your /callback handler ---
// Exchange code for tokens
const tokens = await fetch('https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'apikey': 'YOUR_ANON_KEY' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: new URLSearchParams(location.search).get('code'),
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
}),
}).then(r => r.json());
// Fetch user profile
const user = await fetch('https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo', {
headers: { Authorization: 'Bearer ' + tokens.access_token, apikey: 'YOUR_ANON_KEY' },
}).then(r => r.json());
console.log(user.name, user.email, user.plan);Authorization Code Flow (+ PKCE)
Industry-standard OAuth 2.0 with optional PKCE for public clients (SPAs, mobile apps)
When to use PKCE vs. Client Secret:
- PKCE (recommended): Single-page apps, mobile apps, or any client where you can't securely store a secret.
- Client Secret: Server-side apps (Node.js, Python, Ruby) where the secret stays on your backend.
Step 1: Generate PKCE Challenge (optional but recommended)
const verifier = crypto.randomUUID() + crypto.randomUUID();
const encoder = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Store verifier — you'll need it in Step 3
sessionStorage.setItem('pkce_verifier', verifier);Step 2: Redirect to Authorization
GET https://easylegal-ai.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=profile documents:read
&state=RANDOM_CSRF_TOKEN
&code_challenge=BASE64URL_SHA256_HASH # PKCE only
&code_challenge_method=S256 # PKCE only| Parameter | Required | Description |
|---|---|---|
| response_type | Yes | Must be code |
| client_id | Yes | Your app's client ID |
| redirect_uri | Yes | Must match a registered redirect URI |
| scope | No | Space-separated scopes (defaults to profile) |
| state | Recommended | Random string for CSRF protection |
| code_challenge | PKCE only | Base64url-encoded SHA-256 hash of the verifier |
| code_challenge_method | PKCE only | Must be S256 |
Step 3: Exchange Code for Tokens
After the user approves, they're redirected to redirect_uri?code=AUTH_CODE&state=YOUR_STATE.
POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token
Content-Type: application/json
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y
{
"grant_type": "authorization_code",
"code": "AUTHORIZATION_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_SECRET", // Server-side apps
"code_verifier": "ORIGINAL_VERIFIER" // PKCE apps
}Success Response (200):
{
"access_token": "a1b2c3d4e5f6...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "x9y8z7w6v5u4...",
"scope": "profile documents:read"
}Step 4: Refresh Tokens
Access tokens expire after 1 hour. Use the refresh token to get a new pair:
POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token
Content-Type: application/json
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y
{
"grant_type": "refresh_token",
"refresh_token": "x9y8z7w6v5u4...",
"client_id": "YOUR_CLIENT_ID"
}Token Lifetimes
| Token | Lifetime | Notes |
|---|---|---|
| Authorization Code | 10 minutes | Single-use, invalidated after exchange |
| Access Token | 1 hour | Used in Authorization: Bearer header |
| Refresh Token | 30 days | Rotated on each refresh (old token is revoked) |
/oauth-discoveryOpenID Connect-style discovery document with all endpoint URLs and supported features
/oauth-tokenExchange authorization code for tokens, or refresh existing tokens
/oauth-userinfoRetrieve user profile, documents, templates, and business orders based on granted scopes
/oauth-revokeRevoke an access token or refresh token (RFC 7009)
GET /oauth-userinfo
Returns user data filtered by the scopes granted to your access token
GET https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo
Authorization: Bearer ACCESS_TOKEN
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8YFull Response (all scopes granted):
{
// Always returned
"sub": "550e8400-e29b-41d4-a716-446655440000",
// scope: profile
"email": "jane@example.com",
"name": "Jane Smith",
"picture": "https://storage.example.com/avatars/jane.png",
"plan": "paid",
"region": "US",
"business_type": "LLC",
// scope: documents:read
"documents": [
{
"id": "doc-uuid",
"document_name": "Smith LLC Operating Agreement",
"template_title": "LLC Operating Agreement",
"created_at": "2025-06-15T10:30:00Z",
"updated_at": "2025-06-15T10:30:00Z"
}
],
// scope: templates:read
"templates": [
{
"id": "tpl-uuid",
"title": "Non-Disclosure Agreement (NDA)",
"category": "Business",
"region": "US",
"language": "en"
}
],
// scope: business_services
"business_orders": [
{
"id": "order-uuid",
"service_name": "LLC Formation",
"status": "processing",
"business_name": "Smith LLC",
"created_at": "2025-06-01T09:00:00Z"
}
]
}POST /oauth-token
Exchange codes for tokens, or refresh expired tokens
Supports both application/json and application/x-www-form-urlencoded request bodies per the OAuth 2.0 spec.
Grant Types:
| grant_type | Required Parameters |
|---|---|
| authorization_code | code, redirect_uri, client_id, and either client_secret or code_verifier |
| refresh_token | refresh_token, client_id |
POST /oauth-revoke
Revoke tokens per RFC 7009
POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-revoke
Content-Type: application/json
apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y
{
"token": "ACCESS_OR_REFRESH_TOKEN",
"token_type_hint": "access_token" // or "refresh_token"
}Always returns 200 OK per RFC 7009, regardless of whether the token was found. This prevents token enumeration attacks.
Scopes & Data Access
Request only the scopes your application needs. Users see exactly what you're asking for.
profileRead the user's email, display name, avatar URL, subscription plan, region, and business type. This is the default scope if none is specified.
Response fields:
sub (user ID)emailnamepictureplan (free | paid | trial)region (US | UK | NG | ...)business_typedocuments:readList all documents the user has generated. Returns the most recent 50 documents with names, template types, and timestamps.
Response fields:
documents[].iddocuments[].document_namedocuments[].template_titledocuments[].created_atdocuments[].updated_atdocuments:writeCreate, update, and delete documents on the user's behalf, plus their folders, tags, reminders, and share links. Grants everything documents:read does. Use the /api-v1 CRUD endpoints with the user's access token.
Response fields:
POST /api-v1/user_documentsPUT /api-v1/user_documents/:idDELETE /api-v1/user_documents/:idPOST /api-v1/document_foldersPOST /api-v1/document_reminderstemplates:readBrowse the EasyLegal AI template library. Returns up to 100 templates with titles, categories, regions, and languages. Useful for building template pickers in your UI.
Response fields:
templates[].idtemplates[].titletemplates[].categorytemplates[].regiontemplates[].languagebusiness_servicesView the user's business service orders — LLC formation, EIN applications, registered agent services, and foreign qualification filings. Returns the most recent 20 orders.
Response fields:
business_orders[].idbusiness_orders[].service_namebusiness_orders[].statusbusiness_orders[].business_namebusiness_orders[].created_atError Handling
All errors follow OAuth 2.0 error response format (RFC 6749)
{
"error": "error_code",
"error_description": "Human-readable explanation"
}Authorization Errors (returned via redirect):
| Error | HTTP | Meaning |
|---|---|---|
| invalid_request | 400 | Missing or invalid parameter (client_id, redirect_uri) |
| unauthorized_client | 401 | Client not found or not yet approved |
| invalid_scope | 400 | Requested scope not valid or not authorized for this client |
| login_required | 401 | User is not authenticated or token is expired |
| access_denied | — | User denied the authorization request |
| unsupported_response_type | 400 | Only response_type=code is supported |
Token Errors:
| Error | HTTP | Meaning |
|---|---|---|
| invalid_grant | 400 | Code expired, already used, redirect_uri mismatch, or PKCE failed |
| invalid_client | 401 | Client secret is incorrect |
| unsupported_grant_type | 400 | Only authorization_code and refresh_token are supported |
| invalid_token | 401 | Access token is invalid, expired, or revoked (userinfo endpoint) |
| server_error | 500 | Internal error — retry with exponential backoff |
| rate_limit_exceeded | 429 | Too many requests — check Retry-After header (60s) |
Best Practices
- • Always validate the
stateparameter to prevent CSRF attacks - • Store tokens securely (httpOnly cookies or secure storage, never localStorage)
- • Implement automatic token refresh — don't wait for a 401
- • Handle
invalid_tokenby redirecting the user to re-authorize - • Revoke tokens when users disconnect your app or log out
JavaScript / TypeScript (PKCE)
Recommended for single-page apps and frontend frameworks
// === PKCE Helper ===
async function generatePKCE() {
const verifier = crypto.randomUUID() + crypto.randomUUID();
const hash = await crypto.subtle.digest(
'SHA-256', new TextEncoder().encode(verifier)
);
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return { verifier, challenge };
}
// === Step 1: Start OAuth Flow ===
async function signInWithEasyLegal() {
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);
window.location.href = 'https://easylegal-ai.com/oauth/authorize?' +
new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: window.location.origin + '/callback',
scope: 'profile documents:read templates:read',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
}
// === Step 2: Handle Callback ===
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
// CSRF check
if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch — possible CSRF attack');
}
// Check for errors
if (params.get('error')) {
throw new Error(params.get('error_description') || params.get('error'));
}
const res = await fetch('https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y',
},
body: JSON.stringify({
grant_type: 'authorization_code',
code: params.get('code'),
redirect_uri: window.location.origin + '/callback',
client_id: 'YOUR_CLIENT_ID',
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error_description || err.error);
}
return res.json();
// { access_token, refresh_token, expires_in, scope }
}
// === Step 3: Fetch User Data ===
async function getUserProfile(accessToken: string) {
const res = await fetch('https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo', {
headers: {
'Authorization': 'Bearer ' + accessToken,
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y',
},
});
return res.json();
// { sub, email, name, picture, plan, documents, templates, ... }
}
// === Step 4: Refresh Token ===
async function refreshTokens(refreshToken: string) {
const res = await fetch('https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'YOUR_CLIENT_ID',
}),
});
return res.json();
}Python (Server-Side)
For Django, Flask, FastAPI, or any Python backend
import requests
API_BASE = "https://rygushwlvmieeahockgj.supabase.co/functions/v1"
ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y"
CLIENT_ID = "YOUR_CLIENT_ID"
CLIENT_SECRET = "YOUR_CLIENT_SECRET"
def exchange_code(auth_code: str, redirect_uri: str) -> dict:
"""Exchange authorization code for access + refresh tokens."""
response = requests.post(
f"{API_BASE}/oauth-token",
headers={"apikey": ANON_KEY},
json={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": redirect_uri,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
)
response.raise_for_status()
return response.json()
def get_user_info(access_token: str) -> dict:
"""Fetch user profile and data based on granted scopes."""
response = requests.get(
f"{API_BASE}/oauth-userinfo",
headers={
"Authorization": f"Bearer {access_token}",
"apikey": ANON_KEY,
},
)
response.raise_for_status()
return response.json()
def refresh_tokens(refresh_token: str) -> dict:
"""Refresh an expired access token."""
response = requests.post(
f"{API_BASE}/oauth-token",
headers={"apikey": ANON_KEY},
json={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": CLIENT_ID,
},
)
response.raise_for_status()
return response.json()
def revoke_token(token: str) -> None:
"""Revoke an access or refresh token."""
requests.post(
f"{API_BASE}/oauth-revoke",
headers={"apikey": ANON_KEY},
json={"token": token},
)
# --- Usage ---
tokens = exchange_code("AUTH_CODE_FROM_CALLBACK", "https://yourapp.com/callback")
user = get_user_info(tokens["access_token"])
print(f"Welcome, {user['name']}! Plan: {user['plan']}")
print(f"Documents: {len(user.get('documents', []))}")cURL
Quick testing from your terminal
# Exchange authorization code for tokens
curl -X POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token \
-H "Content-Type: application/json" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y" \
-d '{
"grant_type": "authorization_code",
"code": "AUTH_CODE",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}'
# Fetch user info with access token
curl https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y"
# Refresh expired token
curl -X POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token \
-H "Content-Type: application/json" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "YOUR_REFRESH_TOKEN",
"client_id": "YOUR_CLIENT_ID"
}'
# Revoke a token
curl -X POST https://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-revoke \
-H "Content-Type: application/json" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y" \
-d '{"token": "TOKEN_TO_REVOKE", "token_type_hint": "access_token"}'Billing Model
We bill the end user, not the developer. Building on EasyLegal AI is free.
For Developers
- ✓ Free to register an OAuth application
- ✓ Free API access during development and production
- ✓ No per-request charges — ever
- ✓ No monthly fees or minimum commitments
- ✓ Full access to all scopes and endpoints
For End Users
- • Users must have an EasyLegal AI account
- • Free-tier users can preview documents
- • Paid plan required to download / generate documents
- • Business services billed per service to the user
- • Credit-based actions deducted from the user's balance
Rate Limits
| Scope | Default Limit | Notes |
|---|---|---|
| All endpoints (per client) | 60 req/min | Shared across token exchange, userinfo, and revoke. Configurable per app. |
When rate-limited, you'll receive a 429 response with error: "rate_limit_exceeded" and a Retry-After: 60 header. Need higher limits? Contact us at info@easylegal-ai.com.
Support & Fair Use
- Email: info@easylegal-ai.com
- Response time: Business-day responses within 24 hours
- Fair use: We reserve the right to throttle or suspend clients that abuse the API (automated scraping, excessive polling, etc.)
Testing Your Integration
How to develop and test against the EasyLegal AI API
No Sandbox — Use Your Real Account
There is no separate sandbox environment. Testing is done against the production API using your own EasyLegal AI account. Since all API usage is billed to the end user (not the developer), there's no cost to you during development.
Step-by-Step Testing
Register & get approved
Create your app at /developers. Approval takes 24-48 hours. Once approved, you'll have a client_id.
Set redirect_uri to localhost
Add http://localhost:3000/callback (or your local dev port) as a redirect URI in your app settings.
Test the full flow manually
Open the authorize URL in your browser, log in with your EasyLegal AI account, approve the consent screen, and verify the code appears in your callback URL.
Exchange the code via cURL
Use the cURL examples from our Code Examples tab to exchange the code for tokens and test the userinfo endpoint.
Integrate into your app
Once the manual flow works, integrate it into your application code. Use our JS/Python examples as a starting point.
Test token refresh
Wait for the access token to expire (1 hour), or use the refresh_token grant immediately to verify refresh works.
Test error handling
Intentionally send invalid codes, expired tokens, and wrong redirect_uris to ensure your error handling is robust.
Testing Checklist
Debugging Tips
Check the response body: All errors include error and error_description fields with specific details about what went wrong.
PKCE issues: Ensure you're using SHA-256 hashing and base64url encoding (not standard base64). The challenge must match exactly.
CORS errors: If calling from a browser, ensure your domain is listed in "Allowed Origins" in your app settings. All API endpoints include permissive CORS headers.
Missing apikey header: All API calls (token, userinfo, revoke) require the apikey header with the Supabase anon key.
Redirect URI mismatch: The redirect_uri in your token exchange must exactly match the one used during authorization — including trailing slashes and query parameters.
Frequently Asked Questions
Does it cost anything to build with the EasyLegal AI API?
No. Registration is free, API access is free, and there are no per-request charges. All usage costs (document generation, business services) are billed to the end user based on their EasyLegal AI subscription, not to you.
How long does app approval take?
Typically 24-48 hours on business days. You'll receive an email when your app is approved. Once approved, your client_id is active immediately.
Can I use this in a mobile app?
Yes. Use the PKCE flow (recommended for all public clients). Generate the code challenge on-device, open the authorization URL in the system browser or an in-app browser tab, and handle the callback via a custom URL scheme or universal link.
What happens when the access token expires?
Access tokens expire after 1 hour. Use the refresh_token grant to get a new access token without requiring user interaction. Refresh tokens are valid for 30 days and rotate on each use (the old refresh token is revoked).
Can I request multiple scopes?
Yes. Pass a space-separated list in the scope parameter (e.g., "profile documents:read templates:read business_services"). The user will see all requested permissions on the consent screen.
What if the user has already authorized my app?
If a user has already granted the same (or broader) scopes to your app, the consent screen is automatically skipped for a seamless redirect. They get a new authorization code instantly.
How do users revoke access to my app?
Users can revoke access at any time from their Profile → Connected Apps section in EasyLegal AI. When access is revoked, all existing tokens are invalidated immediately.
Do I need the apikey header?
Yes. All API endpoints (oauth-token, oauth-userinfo, oauth-revoke) require the apikey header. This is the Supabase anon key published in our docs — it's a public key, safe to include in client-side code.
Can I use application/x-www-form-urlencoded for the token endpoint?
Yes. The token endpoint supports both application/json and application/x-www-form-urlencoded request bodies, per the OAuth 2.0 spec (RFC 6749).
Is there a webhook system for real-time events?
Not yet. Webhooks for events like "document created", "order status changed", and "user plan upgraded" are on our roadmap. Contact us if this is critical for your integration.
What data does the "profile" scope include?
The profile scope returns: sub (user ID), email, name, picture (avatar URL), plan (free/paid/trial), region (US/UK/NG/etc.), and business_type. It's the default scope if none is specified.
Can my app create documents on behalf of the user?
Yes. Request the documents:write scope during authorization, then POST to /api-v1/user_documents with the user's access token. You can also update and delete documents, and manage folders, tags, and reminders. Every write is attributed to the user and lands in their document vault immediately. Requests made with a token that lacks the scope return 403 insufficient_scope naming the scope you need.
How do I handle the user denying authorization?
When the user clicks "Deny", they're redirected to your redirect_uri with error=access_denied and error_description as query parameters. Handle this gracefully in your callback handler.
Is there a test/sandbox environment?
No — the API is the same in development and production. Since costs are billed to the end user (not the developer), there's no financial risk during development. Use your own EasyLegal AI account for testing.
What's the difference between oauth-discovery and .well-known/openid-configuration?
They return identical content. The .well-known/openid-configuration is a static file served from the frontend (standard OIDC path), while oauth-discovery is a dynamic edge function. Use whichever your OAuth library expects.