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+.
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.indexhog.com/api/auth/sign-up/email \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "your-password",
"name": "Your Name"
}'const res = await fetch('https://api.indexhog.com/api/auth/sign-up/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
email: '[email protected]',
password: 'your-password',
name: 'Your Name',
}),
});
const data = await res.json();import requests
res = requests.post(
'https://api.indexhog.com/api/auth/sign-up/email',
json={
'email': '[email protected]',
'password': 'your-password',
'name': 'Your Name',
},
)
data = res.json()Already have an account? Skip to Step 2 — existing users sign in directly.
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.indexhog.com/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "[email protected]",
"password": "your-password"
}'// 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.indexhog.com/api/auth/sign-in/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
email: '[email protected]',
password: 'your-password',
}),
});import requests
session = requests.Session() # holds cookies across requests
session.post(
'https://api.indexhog.com/api/auth/sign-in/email',
json={
'email': '[email protected]',
'password': 'your-password',
},
)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.
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.indexhog.com/api/auth/api-key/create \
-H "Content-Type: application/json" \
-b cookies.txt \
-d '{
"name": "CI deploy bot",
"expiresIn": 2592000,
"metadata": {
"scopes": ["user:read", "projects:read", "projects:write"]
}
}'const res = await fetch('https://api.indexhog.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
metadata: { 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.indexhog.com/api/auth/api-key/create',
json={
'name': 'CI deploy bot',
'expiresIn': 2592000,
'metadata': {'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": "tkn_AbCdEf0123456789XyZ...",
"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.
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.indexhog.com/api/user/me \
-H "X-Api-Key: tkn_AbCdEf0123456789XyZ..."
# Tenant route — substitute the organization id you fetched earlier
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.indexhog.com/api/user/organizations/${ORG_ID}/projects" \
-H "X-Api-Key: tkn_AbCdEf0123456789XyZ..."const apiKey = '<your-api-key>'; // load this from a secure env var — never hardcode
const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const headers = { 'X-Api-Key': apiKey };
// Account route
const me = await fetch('https://api.indexhog.com/api/user/me', { headers });
// Tenant route
const projects = await fetch(
`https://api.indexhog.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.indexhog.com/api/user/me', headers=headers)
projects = requests.get(
f'https://api.indexhog.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"
}
}| Caller | Credential | Header sent |
|---|---|---|
| Browser-based UI | 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: tkn_... |
| Third-party app on behalf of a user | OAuth 2.1 access token | Authorization: Bearer ... |
| Enterprise SSO sign-in | Session via SSO flow | Cookie: ... |
projects:write and api-keys:write from read-only automations. A smaller scope list is a smaller blast radius.expiresIn on CI tokens: Rotating on a schedule limits damage if one leaks. Long-lived tokens belong only to infrastructure you trust.metadata: { organizationId: "..." } at creation time to lock the token to one tenant. The API rejects any request whose URL :organizationId does not match the pin.401 / 403: A 401 means the credential is bad or expired. A 403 means the credential is valid but lacks a scope or does not belong to the requested organization. Retrying will not fix either.How the API is structured, how authentication works, and how multi-tenant requests are scoped
Point any MCP-capable AI client at the hosted MCP server and drive Indexhog over OAuth — no token to paste.
Request headers, response envelope, pagination, and the query conventions shared by every endpoint
Error envelope, full code list, and the retry strategies that actually work