Authentication

Authenticating with the leep API

The leep API supports two OAuth 2.0 flows. Which one you need depends on whether you want to act on behalf of your organisation or on behalf of a specific user.

Client Credentials

Server-to-server. Your backend exchanges a client secret for an org-scoped token. No user interaction required.

  • Read public leeps & insights
  • Fully manage teams & members
  • Create or manage leeps
  • Access private leeps
Authorization Code

User-delegated. The user signs in and approves your app; the token carries both their org and user identity.

  • Everything Client Credentials can do
  • Create and manage leeps
  • Access the user's private leeps
  • Act as a specific user

Client Credentials flow

Use this for automated backend jobs, dashboards, and any integration where there is no signed-in user. The token is tied to your organisation, not a person.

!

Client Credentials tokens are read-only for leeps. Creating, editing, or deleting leeps requires user identity — use the Authorization Code flow instead.

Flow

Your server
POST /oauth/token { client_id, client_secret }
leep API
{ access_token, expires_in: 3600 }
Your server
GET /api/leeps Authorization: Bearer <token>
leep API
{ leeps: [...] }
Your server

1. Generate credentials

Organisation admins generate API credentials from the developer dashboard. Each application gets a unique client ID and client secret. The secret is only shown once — store it in an environment variable.

2. Exchange for an access token

Request
curl -X POST https://api.leep.works/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "lp_live_abc123...",
    "client_secret": "sk_abc123..."
  }'
Response
{
  "access_token": "a1b2c3...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scope": "leeps:read teams:read teams:write"
}

Tokens expire after 1 hour. Request a fresh one when yours expires.

3. Call the API

curl
curl https://api.leep.works/api/leeps \
  -H "Authorization: Bearer a1b2c3..."

Code examples

Node.js / TypeScript
const res = await fetch('https://api.leep.works/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'client_credentials',
    client_id: process.env.LEEP_CLIENT_ID,
    client_secret: process.env.LEEP_CLIENT_SECRET,
  }),
});
const { access_token } = await res.json();

const leeps = await fetch('https://api.leep.works/api/leeps', {
  headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json());
Python
import requests, os

token_res = requests.post(
    "https://api.leep.works/oauth/token",
    json={
        "grant_type": "client_credentials",
        "client_id": os.environ["LEEP_CLIENT_ID"],
        "client_secret": os.environ["LEEP_CLIENT_SECRET"],
    },
)
access_token = token_res.json()["access_token"]

leeps = requests.get(
    "https://api.leep.works/api/leeps",
    headers={"Authorization": f"Bearer {access_token}"},
).json()

Authorization Code flow

Use this when your integration needs to act on behalf of a specific leep user — for example, creating leeps or accessing their private data. The user signs in on the leep consent page and grants your app permission.

i

You must set a redirect URI when creating your application in the developer dashboard. The token exchange will reject any redirect URI that doesn't exactly match.

Flow

Your app
redirect user to /oauth/authorize?client_id=&redirect_uri=&scope=
User's browser
consent page shown — user clicks Allow
leep consent
POST /api/oauth/code (Clerk JWT)
leep API
{ code } — one-time, 10 min TTL
leep consent
redirect to redirect_uri?code=xyz
Your app
POST /oauth/token { code, client_id, client_secret }
leep API
{ access_token, expires_in: 3600 }
Your app

1. Redirect the user to the consent page

Build the authorisation URL and redirect the user's browser to it. Include a random state parameter and verify it on the callback to prevent CSRF attacks.

Node.js
const params = new URLSearchParams({
  client_id: process.env.LEEP_CLIENT_ID,
  redirect_uri: 'https://yourapp.com/oauth/callback',
  scope: 'leeps:read insights:read',
  state: crypto.randomUUID(), // store this to verify on callback
});

res.redirect(
  `https://developer.leep.works/oauth/authorize?${params}`
);

Supported query parameters: client_id redirect_uri scope state

2. User approves on the consent page

leep shows the user which permissions your app is requesting. If they click Allow, they are redirected to your redirect_uri with a short-lived code appended. If they click Deny, you receive ?error=access_denied. The code is valid for 10 minutes and can only be used once.

3. Exchange the code for an access token

On your server, exchange the code for an access token. Never do this in the browser.

Node.js — callback handler
// GET /oauth/callback?code=xyz&state=abc
const { code, state } = req.query;

// verify state matches what you stored before redirecting
if (state !== session.oauthState) throw new Error('state mismatch');

const res = await fetch('https://api.leep.works/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: process.env.LEEP_CLIENT_ID,
    client_secret: process.env.LEEP_CLIENT_SECRET,
    redirect_uri: 'https://yourapp.com/oauth/callback',
  }),
});
const { access_token } = await res.json();
// store access_token against this user
Python
import requests, os

# After receiving ?code=... from the redirect
token_res = requests.post(
    "https://api.leep.works/oauth/token",
    json={
        "grant_type": "authorization_code",
        "code": code,
        "client_id": os.environ["LEEP_CLIENT_ID"],
        "client_secret": os.environ["LEEP_CLIENT_SECRET"],
        "redirect_uri": "https://yourapp.com/oauth/callback",
    },
)
access_token = token_res.json()["access_token"]

Token errors

ErrorFlowMeaning
invalid_clientBothclient_id not found, wrong secret, or application revoked
invalid_grantAuth CodeCode is invalid, expired, or already used
redirect_uri_mismatchAuth Coderedirect_uri does not match the registered value
invalid_scopeAuth CodeRequested scopes are not in the application's registered scopes
invalid_requestBothMissing or malformed request body
access_deniedAuth CodeUser clicked Deny on the consent page
401 UnauthorizedBothAccess token is invalid, expired, or from a revoked application
403 ForbiddenClient CredentialsOperation requires user identity — use Authorization Code flow