Quick Start
Sign in, issue a Personal Access Token, and make your first authenticated call
3 min readThis guide takes you from "no credentials" to a working authenticated request in four steps. You will create an account, sign in to establish a session, issue a Personal Access Token (PAT) for programmatic use, and call a tenant-scoped endpoint with the PAT.
Prerequisites: curl, Node.js 20+, or Python 3.10+. Replace https://api.example.com with
your deployment host in every example.
Step 1 — Create an account
Sign up with email and password. This creates the user, provisions a default organization for them, and sends a verification email.
curl -X POST https://api.example.com/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "a-long-secure-passphrase",
"name": "Your Name"
}'const res = await fetch('https://api.example.com/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
email: '[email protected]',
password: 'a-long-secure-passphrase',
name: 'Your Name',
}),
});
const data = await res.json();import requests
res = requests.post(
'https://api.example.com/api/auth/sign-up/email',
json={
'email': '[email protected]',
'password': 'a-long-secure-passphrase',
'name': 'Your Name',
},
)
data = res.json()Already have an account? Skip to Step 2 — existing users sign in directly.
Step 2 — Sign in and capture the session cookie
Sign in via /api/auth/sign-in/email. The API responds with a Set-Cookie containing the session
cookie. Browser clients persist it automatically; CLI clients keep a cookie jar and echo it back on
each subsequent call.
# -c saves cookies, -b sends them back on later calls
curl -X POST https://api.example.com/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "[email protected]",
"password": "a-long-secure-passphrase"
}'// Browser: credentials: 'include' persists the cookie automatically.
// Node: use a cookie-aware client such as `tough-cookie` to keep the cookie across requests.
const res = await fetch('https://api.example.com/api/auth/sign-in/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
email: '[email protected]',
password: 'a-long-secure-passphrase',
}),
});import requests
session = requests.Session() # holds cookies across requests
session.post(
'https://api.example.com/api/auth/sign-in/email',
json={
'email': '[email protected]',
'password': 'a-long-secure-passphrase',
},
)The session is now live. You can already call any /api/user/* account route by sending the
cookie back. For headless work, continue to Step 3.
Step 3 — Issue a Personal Access Token
Call the API key endpoint while signed in (cookie attached). Choose a short list of scopes — do not ask for every permission. The full token value is returned exactly once; store it immediately.
curl -X POST https://api.example.com/api/auth/api-key/create \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"name": "CI deploy bot",
"expiresIn": 2592000,
"scopes": ["user:read", "projects:read", "projects:write"]
}'const res = await fetch('https://api.example.com/api/auth/api-key/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
name: 'CI deploy bot',
expiresIn: 2592000, // 30 days in seconds, omit for no expiry
scopes: ['user:read', 'projects:read', 'projects:write'],
}),
});
const { data } = await res.json();
const apiKey = data.key; // shown once — store it nowres = session.post(
'https://api.example.com/api/auth/api-key/create',
json={
'name': 'CI deploy bot',
'expiresIn': 2592000,
'scopes': ['user:read', 'projects:read', 'projects:write'],
},
)
api_key = res.json()['data']['key'] # shown onceResponse:
{
"success": true,
"status": 200,
"code": "OK",
"message": "API key created",
"data": {
"id": "ak_abc123",
"name": "CI deploy bot",
"key": "ba_AbCdEf0123456789XyZ...",
"scopes": ["user:read", "projects:read", "projects:write"],
"expiresAt": "2026-05-05T10:00:00.000Z"
}
}The key field is only returned at creation. After this response, the value is no longer
retrievable. If you lose it, revoke the token and issue a new one.
Step 4 — Call an authenticated endpoint
Send the PAT via the X-Api-Key header. For tenant-scoped routes (anything touching projects,
payments, or members), the URL path carries the organization id. Get yours from
GET /api/user/organizations.
# Account route — no organization id required
curl https://api.example.com/api/user/me \
-H "X-Api-Key: ba_AbCdEf0123456789XyZ..."
# Tenant route — substitute the organization id you fetched earlier
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.example.com/api/user/organizations/${ORG_ID}/projects" \
-H "X-Api-Key: ba_AbCdEf0123456789XyZ..."const apiKey = process.env.API_KEY;
const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const headers = { 'X-Api-Key': apiKey };
// Account route
const me = await fetch('https://api.example.com/api/user/me', { headers });
// Tenant route
const projects = await fetch(
`https://api.example.com/api/user/organizations/${organizationId}/projects`,
{ headers },
);import os, requests
api_key = os.environ['API_KEY']
organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
headers = {'X-Api-Key': api_key}
me = requests.get('https://api.example.com/api/user/me', headers=headers)
projects = requests.get(
f'https://api.example.com/api/user/organizations/{organization_id}/projects',
headers=headers,
)A successful response looks like this:
{
"success": true,
"status": 200,
"code": "OK",
"message": "User fetched successfully",
"data": {
"id": "01HZ3K5R4X9Y2V6QF8TJ7W0CDN",
"email": "[email protected]",
"fullName": "Your Name",
"handle": "yourname",
"isVerified": true,
"verifiedAt": "2026-04-05T10:05:00.000Z",
"isActive": true,
"lastLoginAt": "2026-04-05T10:00:00.000Z",
"role": "user",
"organizationCount": 1,
"createdAt": "2026-04-05T10:00:00.000Z",
"updatedAt": "2026-04-05T10:00:00.000Z"
}
}When to use each credential
| Caller | Credential | Header sent |
|---|---|---|
| Web / admin UI (browser) | Session cookie | Cookie: ... (sent automatically) |
| Server acting as a signed-in user | Session cookie (echoed) | Cookie: ... |
| Scripts, CI, bots, integrations | Personal Access Token | X-Api-Key: ba_... |
| Third-party app on behalf of a user | OAuth 2.1 access token | Authorization: Bearer ... |
| Enterprise SSO sign-in | Session via SSO flow | Cookie: ... |
Best Practices
- Never ship session cookies to scripts: Sessions are tied to a specific browser. Use a PAT for anything non-interactive.
- Scope tokens down: Omit
projects:write,subscription:write, andapi-keys:writefrom read-only automations. A smaller scope list is a smaller blast radius. - Set
expiresInon CI tokens: Rotating on a schedule limits damage if one leaks. Long-lived tokens belong only to infrastructure you trust. - Pin tokens to an organization when possible: Pass
metadata: { organizationId: "..." }at creation time to lock the token to one tenant. The API rejects any request whose URL:organizationIddoes not match the pin. - Fail loudly on
401/403: A401means the credential is bad or expired. A403means the credential is valid but lacks a scope or does not belong to the requested organization. Retrying will not fix either.
Next Steps
- Understand the envelope: Read API Structure for the pagination and response contract.
- Handle errors: Review Error Handling.
- Respect limits: See Rate Limits.
- Choose scopes intentionally: Check Scopes & Permissions before you ship production tokens.
- Browse endpoints: Start at the API Keys reference or the Projects reference.
Related Pages
Introduction
How the API is structured, how authentication works, and how multi-tenant requests are scoped
API Structure
Request headers, response envelope, pagination, and the query conventions shared by every endpoint
Error Handling
Error envelope, full code list, and the retry strategies that actually work
Rate Limits
Per-IP and per-endpoint request limits, the headers that expose them, and how to retry safely