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.
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
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
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
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..."
}'{
"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 https://api.leep.works/api/leeps \
-H "Authorization: Bearer a1b2c3..."Code examples
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());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.
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
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.
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.
// 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 userimport 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
| Error | Flow | Meaning |
|---|---|---|
invalid_client | Both | client_id not found, wrong secret, or application revoked |
invalid_grant | Auth Code | Code is invalid, expired, or already used |
redirect_uri_mismatch | Auth Code | redirect_uri does not match the registered value |
invalid_scope | Auth Code | Requested scopes are not in the application's registered scopes |
invalid_request | Both | Missing or malformed request body |
access_denied | Auth Code | User clicked Deny on the consent page |
401 Unauthorized | Both | Access token is invalid, expired, or from a revoked application |
403 Forbidden | Client Credentials | Operation requires user identity — use Authorization Code flow |