Request headers, response envelope, pagination, and the query conventions shared by every endpoint
4 min readEvery endpoint follows the same request and response contract. Once you know the envelope, the pagination scheme, and the auth headers, you can call any route without re-reading per-endpoint docs.
https://api.indexhog.comReplace the host with your deployment.
| Header | When to send | Value |
|---|---|---|
Content-Type | Any request with a body | application/json |
Cookie | Browser or server acting as a signed-in user | Session cookie (sent automatically) |
X-Api-Key | Personal Access Token (scripts, CI, integrations) | tkn_AbCdEf... |
Authorization | Third-party OAuth app on behalf of a user | Bearer eyJhbGciOi... |
The organization id for tenant-scoped routes goes in the URL path (/api/user/organizations/{organizationId}/...), not in a header.
Send one credential, not several. If multiple are present, the session cookie wins, then the PAT, then the OAuth Bearer token. Sending more than one does not combine permissions.
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 res = await fetch(
`https://api.indexhog.com/api/user/organizations/${organizationId}/projects`,
{ headers: { 'X-Api-Key': apiKey } },
);import os, requests
organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.get(
f'https://api.indexhog.com/api/user/organizations/{organization_id}/projects',
headers={'X-Api-Key': os.environ['API_KEY']},
)Bodies are JSON. Write endpoints reject Content-Type values other than application/json. Request bodies are capped at 10 MiB — anything larger returns 400 Bad Request.
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl -X POST "https://api.indexhog.com/api/user/organizations/${ORG_ID}/projects" \
-H "Content-Type: application/json" \
-H "X-Api-Key: tkn_AbCdEf..." \
-d '{
"name": "Acme Studio",
"slug": "acme-studio",
"website": "https://acme.example"
}'const apiKey = '<your-api-key>'; // load this from a secure env var — never hardcode
const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const res = await fetch(
`https://api.indexhog.com/api/user/organizations/${organizationId}/projects`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': apiKey,
},
body: JSON.stringify({
name: 'Acme Studio',
slug: 'acme-studio',
website: 'https://acme.example',
}),
},
);organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.post(
f'https://api.indexhog.com/api/user/organizations/{organization_id}/projects',
headers={'X-Api-Key': os.environ['API_KEY']},
json={
'name': 'Acme Studio',
'slug': 'acme-studio',
'website': 'https://acme.example',
},
)Every JSON response — success or error — uses the same envelope.
{
"success": true,
"status": 201,
"code": "OK",
"message": "Project created successfully",
"data": {
"id": "01HZ3KA7V0J9Z8Q2G5B1T7XWDN",
"name": "Acme Studio",
"slug": "acme-studio",
"website": "https://acme.example",
"status": "active",
"createdAt": "2026-04-05T10:00:00.000Z",
"updatedAt": "2026-04-05T10:00:00.000Z"
}
}{
"success": false,
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed for fields: website, slug",
"meta": {
"errors": [
{ "field": "website", "error": "Invalid url", "location": "body" },
{ "field": "slug", "error": "The slug must be unique", "location": "body" }
]
}
}Each entry in meta.errors has field (the offending field path), error (the message), and location (body, query, or params).
| Field | Type | Description |
|---|---|---|
success | boolean | true when status < 400, false otherwise |
status | number | HTTP status code, mirrored in the body so clients only parse once |
code | string | Machine-readable code — OK on success, see Error Handling |
message | string | Human-readable summary |
data | object | array | Resource, list, or operation result on success |
meta | object | Pagination, validation error details, or other structured extras |
List endpoints use offset-based pagination. The query parameters are offset and limit; the response meta returns total, offset, limit, and hasMore.
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
offset | number | 0 | — | Number of items to skip |
limit | number | 20 | 100 | Number of items to return |
Some endpoints add their own filters on top — a q search query, a status filter, etc. See the API Reference for endpoint-specific parameters.
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.indexhog.com/api/user/organizations/${ORG_ID}/projects?offset=40&limit=20" \
-H "X-Api-Key: tkn_AbCdEf..."const apiKey = '<your-api-key>'; // load this from a secure env var — never hardcode
const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const res = await fetch(
`https://api.indexhog.com/api/user/organizations/${organizationId}/projects?offset=40&limit=20`,
{ headers: { 'X-Api-Key': apiKey } },
);
const body = await res.json();
console.log(body.meta.total, body.meta.hasMore);organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.get(
f'https://api.indexhog.com/api/user/organizations/{organization_id}/projects',
params={'offset': 40, 'limit': 20},
headers={'X-Api-Key': os.environ['API_KEY']},
)
body = res.json()
print(body['meta']['total'], body['meta']['hasMore']){
"success": true,
"status": 200,
"code": "OK",
"message": "Projects fetched successfully",
"data": [
{
"id": "01HZ3KA7V0J9Z8Q2G5B1T7XWDN",
"name": "Acme Studio",
"slug": "acme-studio",
"status": "active"
}
],
"meta": {
"total": 156,
"offset": 40,
"limit": 20,
"hasMore": true
}
}Do not use page/pageSize. The API only accepts offset/limit. A request with page
will silently ignore it and return the first page.
2xx Success — 200 OK for reads and updates, 201 Created for new resources, 204 No Content for deletes that return no body.
4xx Client Errors — 400 validation/bad JSON, 401 missing or invalid credential, 403
valid credential but insufficient scope or wrong organization, 404 resource not found, 409
conflict (duplicate slug, etc.), 429 rate limited.
5xx Server Errors — 500 unexpected failure, 503 dependency unavailable. Both are logged
with a request id you can quote when filing a bug.
2026-04-05T10:00:00.000Z). Parse with new Date(...) in JS or datetime.fromisoformat(...) in Python.id descending roughly matches createdAt descending.camelCase for every field.is, has, or needs prefixes (isActive, isVerified, hasMore).At (createdAt, updatedAt, expiresAt, lastLoginAt).data and pagination in meta. Single-resource endpoints return an object in data and omit meta unless there is something to report.class ApiClient {
constructor({ apiKey, organizationId, baseUrl = 'https://api.indexhog.com' }) {
this.apiKey = apiKey;
this.organizationId = organizationId;
this.baseUrl = baseUrl;
}
async request(path, { method = 'GET', body, searchParams } = {}) {
const url = new URL(this.baseUrl + path);
if (searchParams) {
for (const [k, v] of Object.entries(searchParams)) url.searchParams.set(k, v);
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey,
},
body: body ? JSON.stringify(body) : undefined,
});
const envelope = await res.json();
if (!envelope.success) {
const err = new Error(envelope.message);
err.code = envelope.code;
err.status = envelope.status;
err.meta = envelope.meta;
throw err;
}
return envelope;
}
listProjects({ offset = 0, limit = 20 } = {}) {
return this.request(
`/api/user/organizations/${this.organizationId}/projects`,
{ searchParams: { offset, limit } },
);
}
}import os, requests
BASE_URL = 'https://api.indexhog.com'
def client():
s = requests.Session()
s.headers.update({'X-Api-Key': os.environ['API_KEY']})
return s
def call(session, method, path, **kwargs):
res = session.request(method, BASE_URL + path, **kwargs)
envelope = res.json()
if not envelope.get('success'):
raise RuntimeError(f"{envelope.get('code')}: {envelope.get('message')}")
return envelope
def list_projects(session, organization_id, offset=0, limit=20):
return call(
session,
'GET',
f'/api/user/organizations/{organization_id}/projects',
params={'offset': offset, 'limit': limit},
)envelope.success (or code === 'OK') before reading data: The HTTP status is also correct, but checking the body is cheaper than re-parsing the status.hasMore, not arithmetic: Trust the server's hasMore flag instead of comparing offset + limit against total; total can shift between paginated requests./api/user/organizations/{organizationId}/...) so callers cannot forget to scope their requests.Z is UTC — do not truncate the trailing Z.code field in logs: It is stable across versions; the human message is not.GET, PUT, DELETE; be careful with POST unless the endpoint documents idempotency.How the API is structured, how authentication works, and how multi-tenant requests are scoped
Sign in, issue a Personal Access Token, and make your first authenticated call
Point any MCP-capable AI client at the hosted MCP server and drive Indexhog over OAuth — no token to paste.
Error envelope, full code list, and the retry strategies that actually work