API · OAuth 2.0 + PKCE

API documentation.

Give your users access to AI-powered legal documents, 330+ templates, e-signatures, and business formation — all through a single integration.

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

ServiceURL
Authorizationhttps://easylegal-ai.com/oauth/authorize
Token Exchangehttps://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-token
User Infohttps://rygushwlvmieeahockgj.supabase.co/functions/v1/oauth-userinfo
Token Revocationhttps://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-configuration

Required Headers

All API calls (token, userinfo, revoke) require the apikey header:

apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ5Z3VzaHdsdm1pZWVhaG9ja2dqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTIwNjA4NTUsImV4cCI6MjA2NzYzNjg1NX0.H1oA1NBWwZ19Ka0KQj38aYl_uPttI4sDmMdtmaL5s8Y

Quick Start — 5 Minutes

Go from zero to fetching user data in five steps

1

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.

2

Wait for approval

Our team reviews new apps within 24-48 hours. You'll receive your client_id and client_secret upon approval.

3

Redirect users to authorize

Send users to our authorization URL. They'll see a consent screen showing what permissions you're requesting.

4

Exchange code for tokens

After the user approves, we redirect back to your app with an authorization code. Exchange it for access + refresh tokens.

5

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
ParameterRequiredDescription
response_typeYesMust be code
client_idYesYour app's client ID
redirect_uriYesMust match a registered redirect URI
scopeNoSpace-separated scopes (defaults to profile)
stateRecommendedRandom string for CSRF protection
code_challengePKCE onlyBase64url-encoded SHA-256 hash of the verifier
code_challenge_methodPKCE onlyMust 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

TokenLifetimeNotes
Authorization Code10 minutesSingle-use, invalidated after exchange
Access Token1 hourUsed in Authorization: Bearer header
Refresh Token30 daysRotated on each refresh (old token is revoked)
GET
/oauth-discovery

OpenID Connect-style discovery document with all endpoint URLs and supported features

POST
/oauth-token

Exchange authorization code for tokens, or refresh existing tokens

GET
/oauth-userinfo

Retrieve user profile, documents, templates, and business orders based on granted scopes

POST
/oauth-revoke

Revoke 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_uPttI4sDmMdtmaL5s8Y

Full 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_typeRequired Parameters
authorization_codecode, redirect_uri, client_id, and either client_secret or code_verifier
refresh_tokenrefresh_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.

profile
User Profile

Read 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_type
documents:read
Documents (Read)

List 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_at
documents:write
Documents (Write)

Create, 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_reminders
templates:read
Templates

Browse 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[].language
business_services
Business Services

View 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_at

Error 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):

ErrorHTTPMeaning
invalid_request400Missing or invalid parameter (client_id, redirect_uri)
unauthorized_client401Client not found or not yet approved
invalid_scope400Requested scope not valid or not authorized for this client
login_required401User is not authenticated or token is expired
access_deniedUser denied the authorization request
unsupported_response_type400Only response_type=code is supported

Token Errors:

ErrorHTTPMeaning
invalid_grant400Code expired, already used, redirect_uri mismatch, or PKCE failed
invalid_client401Client secret is incorrect
unsupported_grant_type400Only authorization_code and refresh_token are supported
invalid_token401Access token is invalid, expired, or revoked (userinfo endpoint)
server_error500Internal error — retry with exponential backoff
rate_limit_exceeded429Too many requests — check Retry-After header (60s)

Best Practices

  • • Always validate the state parameter 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_token by 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

ScopeDefault LimitNotes
All endpoints (per client)60 req/minShared 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

1

Register & get approved

Create your app at /developers. Approval takes 24-48 hours. Once approved, you'll have a client_id.

2

Set redirect_uri to localhost

Add http://localhost:3000/callback (or your local dev port) as a redirect URI in your app settings.

3

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.

4

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.

5

Integrate into your app

Once the manual flow works, integrate it into your application code. Use our JS/Python examples as a starting point.

6

Test token refresh

Wait for the access token to expire (1 hour), or use the refresh_token grant immediately to verify refresh works.

7

Test error handling

Intentionally send invalid codes, expired tokens, and wrong redirect_uris to ensure your error handling is robust.

Testing Checklist

Authorization redirect works and consent screen shows correct scopes
Callback receives code and state parameters
State parameter matches what you sent (CSRF protection)
Token exchange returns access_token, refresh_token, and expires_in
UserInfo returns correct data for each requested scope
Token refresh returns a new token pair and revokes the old one
Expired access tokens return 401 with error: "invalid_token"
Invalid codes return 400 with error: "invalid_grant"
User deny on consent screen returns error: "access_denied" to your redirect_uri
Token revocation works for both access and refresh tokens

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.