# API Structure

Request headers, response envelope, pagination, and the query conventions shared by every endpoint

Every 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.

## Base URL

```text
https://api.indexhog.com
```

Replace the host with your deployment.

## Request Headers

| 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.

> **Info:** **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.

### Sending a typical authenticated request

**curl**

```bash
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.indexhog.com/api/user/organizations/${ORG_ID}/projects" \
-H "X-Api-Key: tkn_AbCdEf0123456789XyZ..."
```

**JavaScript**

```javascript
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 } },
);
```

**Python**

```python
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']},
)
```

## Request Bodies

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`.

**curl**

```bash
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"
}'
```

**JavaScript**

```javascript
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',
  }),
},
);
```

**Python**

```python
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',
  },
)
```

## Response Envelope

Every JSON response — success or error — uses the same envelope.

### Success

```json
{
"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"
}
}
```

### Error

```json
{
"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`).

### Fields

| 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](/docs/get-started/errors) |
| `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                        |

## Pagination

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](/docs/api-reference/api-keys) for endpoint-specific parameters.

**curl**

```bash
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.indexhog.com/api/user/organizations/${ORG_ID}/projects?offset=40&limit=20" \
-H "X-Api-Key: tkn_AbCdEf..."
```

**JavaScript**

```javascript
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);
```

**Python**

```python
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'])
```

### Paginated response

```json
{
"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
}
}
```

> **Warning:** **Do not use `page`/`pageSize`.** The API only accepts `offset`/`limit`. A request with `page`
> will silently ignore it and return the first page.

## HTTP Status Codes

> **Success:** **2xx Success** — `200 OK` for reads and updates, `201 Created` for new resources, `204 No
>   Content` for deletes that return no body.

> **Error:** **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.

> **Warning:** **5xx Server Errors** — `500` unexpected failure, `503` dependency unavailable. Both are logged
> with a request id you can quote when filing a bug.

## Dates and IDs

- **Dates** — ISO 8601 with UTC (`2026-04-05T10:00:00.000Z`). Parse with `new Date(...)` in JS or `datetime.fromisoformat(...)` in Python.
- **IDs** — UUID v7 strings. They are time-sortable, so ordering by `id` descending roughly matches `createdAt` descending.

## Field Naming

- `camelCase` for every field.
- Booleans use `is`, `has`, or `needs` prefixes (`isActive`, `isVerified`, `hasMore`).
- Date fields end in `At` (`createdAt`, `updatedAt`, `expiresAt`, `lastLoginAt`).
- List endpoints return arrays in `data` and pagination in `meta`. Single-resource endpoints return an object in `data` and omit `meta` unless there is something to report.

## A minimal API client

**JavaScript**

```javascript
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 } },
);
}
}
```

**Python**

```python
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},
)
```

## Best Practices

- **Check `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.
- **Use `hasMore`, not arithmetic:** Trust the server's `hasMore` flag instead of comparing `offset + limit` against `total`; `total` can shift between paginated requests.
- **Build URLs from the active organization id:** Keep the organization id in one place (e.g., a client wrapper that prepends `/api/user/organizations/{organizationId}/...`) so callers cannot forget to scope their requests.
- **Parse dates as timezone-aware:** ISO 8601 with `Z` is UTC — do not truncate the trailing `Z`.
- **Surface the `code` field in logs:** It is stable across versions; the human message is not.
- **Retry only idempotent methods:** Safe to retry on `GET`, `PUT`, `DELETE`; be careful with `POST` unless the endpoint documents idempotency.

## Next Steps

1. **Handle failures:** Read [Error Handling](/docs/get-started/errors) for the full code list.
2. **Plan for limits:** See [Rate Limits](/docs/get-started/rate-limits) before batching calls.
3. **Lock down scopes:** Pick the right scopes in [Scopes & Permissions](/docs/get-started/scopes).
4. **Configure SSO (optional):** Set up [Enterprise SSO](/docs/get-started/enterprise-sso) for domain-based authentication.
5. **Browse endpoints:** Jump to the [API Reference](/docs/api-reference/api-keys) and start integrating.