---
url: 'https://docs.kitbase.dev/api-reference.md'
description: >-
  Complete REST API reference for Kitbase — OpenAPI spec, authentication and API
  keys, permissions, the error model, rate-limit headers, versioning and
  deprecation policy, and the SDK ingestion endpoints.
---

# API Reference

REST API reference for the Kitbase SDK endpoints, plus the conventions — authentication, errors, rate limits, versioning — that every Kitbase API operation follows.

## OpenAPI specification

The full public surface (the same operations the [CLI](/cli) and dashboard use) is published as an OpenAPI 3.0 document. Every operation has a unique `operationId`, a description, typed parameters, response schemas and typed error responses, so it can be loaded straight into an API client, a code generator or an LLM's function-calling tool set.

| Format | URL |
|--------|-----|
| JSON | `https://api.kitbase.dev/openapi.json` (mirrored at `https://kitbase.dev/openapi.json`) |
| YAML | `https://api.kitbase.dev/openapi.yaml` (mirrored at `https://kitbase.dev/openapi.yaml`) |

Both are served anonymously with `Cache-Control: public, max-age=3600`, an `ETag`, and permissive CORS.

```bash
curl -s https://api.kitbase.dev/openapi.json | jq '.info.version, (.paths | length)'
```

## Base URL

```
https://api.kitbase.dev
```

Self-hosted deployments serve the API under `https://<your-host>/api`.

## Authentication

| Credential | Header | Used by |
|------------|--------|---------|
| **SDK key** (`pk_kitbase_…`) | `x-sdk-key: pk_kitbase_…` | The `/sdk/v1/*` ingestion endpoints below (browser SDKs, tracking script) |
| **Private API key** (`sk_kitbase_…`) | `Authorization: Bearer sk_kitbase_…` or `X-API-Key: sk_kitbase_…` | Everything else: the CLI in CI, server-side ingestion (`/ingest/v1/*`), the MCP server, your own scripts |
| **User session** (JWT) | `Authorization: Bearer <jwt>` | The dashboard and the CLI's browser login |

SDK endpoints use the `x-sdk-key` header:

```
x-sdk-key: <YOUR_SDK_KEY>
```

### Permissions and scoping

Every operation is guarded by a named permission (for example `analytics.view`, `webhook.create`, `aivisibility.manage`). A user holds the permissions of their role in the organization — owner, admin, developer, analyst or support. A private API key is created by a user and holds **that user's live permissions, limited to the key's project**; it can never create or delete credentials (`private_api_key.*`, `sdk_key.*`). Revoking a role or a key takes effect on the next request. A permission failure is a `403` with error code `PERM_001`.

The [MCP server](/sdks/mcp) is authorized with OAuth 2.1 (PKCE). At consent time the user picks the organization, the project and the exact subset of permissions the connection may use; each tool call is then checked against that subset intersected with the user's live permissions.

***

## Events

### Track Event

Track an event in your application.

```
POST /sdk/v1/logs
```

#### Headers

| Header | Required | Description |
|--------|----------|-------------|
| `x-sdk-key` | Yes | Your Kitbase SDK key |
| `Content-Type` | Yes | `application/json` |

#### Request Body

```json
{
  "event": "New Subscription",
  "channel": "payments",
  "user_id": "user-123",
  "anonymous_id": "550e8400-e29b-41d4-a716-446655440000",
  "icon": "💰",
  "notify": true,
  "description": "User subscribed to premium plan",
  "timestamp": 1705321800000,
  "tags": {
    "plan": "premium",
    "amount": 9.99
  }
}
```

#### Parameters

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `event` | `string` | Yes | Event name |
| `channel` | `string` | Yes | Event channel/category |
| `user_id` | `string` | No | Identified user ID |
| `anonymous_id` | `string` | No | Anonymous user ID (UUID) |
| `icon` | `string` | No | Emoji icon |
| `notify` | `boolean` | No | Send real-time notification |
| `description` | `string` | No | Event description |
| `timestamp` | `number` | No | Unix timestamp in ms |
| `tags` | `object` | No | Key-value metadata |

#### Response

**Status: `202 Accepted`**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "New Subscription",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```

#### Errors

| Status | Description |
|--------|-------------|
| `400` | Invalid parameters |
| `401` | Missing or invalid SDK key |
| `503` | Queue full |

***

### Server-Side Crawler Detection

Detect crawlers and AI bots that **never run the JavaScript SDK** (GPTBot, Googlebot, ClaudeBot, scrapers, …). Install lightweight middleware on your server or edge that forwards each request's signals; Kitbase classifies the actor server-side and stores bot/crawler traffic with full attribution.

```
POST /ingest/v1/server
```

**Authentication:** this is a server-to-server endpoint, so it uses your **secret API key** (`sk_kitbase_…`), sent as `Authorization: Bearer <API_KEY>` (or `X-API-Key`) — never the browser-exposed SDK key. The key is bound to a project, which is the target for everything you send. This is the generic source in the ingestion family at `/ingest/v1/{source}`; per-vendor log-drain endpoints (`/ingest/v1/vercel`, `/ingest/v1/cloudfront`, `/ingest/v1/netlify`, `/ingest/v1/fastly`, …) are added under the same namespace over time.

> **Why this exists:** tag-based analytics only sees clients that execute JS, so non-JS crawlers are invisible. This endpoint observes them in your request path. **Human requests are ignored** here (the JS SDK already counts them, so this avoids double-counting); detected bot/crawler requests are stored whenever events are enabled for the project. Forwarded human traffic is classified in memory and discarded — not stored; for stored bot events, the raw IP is kept only when IP logging is enabled for the project.

#### Request Body

Send the **original visitor's** signals (not your server's connection):

```json
{
  "events": [
    {
      "user_agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.1; +https://openai.com/gptbot",
      "ip_address": "203.0.113.5",
      "method": "GET",
      "host": "example.com",
      "path": "/pricing",
      "referrer": "https://www.google.com/",
      "signature": "...",
      "signature_input": "...",
      "signature_agent": "https://chatgpt.com",
      "client_timestamp": 1705321800000
    }
  ]
}
```

#### Parameters (per item)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `user_agent` | `string` | Yes | Original request `User-Agent` |
| `ip_address` | `string` | Yes | Original client IP |
| `method` | `string` | No | HTTP method (used for Web Bot Auth signature base) |
| `host` | `string` | No | Original host/authority |
| `path` | `string` | No | Original request path |
| `referrer` | `string` | No | Original `Referer` |
| `signature`, `signature_input`, `signature_agent` | `string` | No | Web Bot Auth headers, if present — enables cryptographic verification |
| `client_timestamp` | `number` | No | Observation time (ms); defaults to receipt time |
| `event` | `string` | No | Event name; defaults to `server_request` |

#### Response

**Status: `202 Accepted`** — `accepted` counts how many were stored (bots); humans/disabled are silently skipped.

```json
{ "accepted": 1, "total": 1 }
```

Stored rows carry the attribution fields documented in [Server-Side Tag Enrichment](/track-events#bot-crawler-attribution) (`actor_type`, `bot_name`, `bot_vendor`, `actor_verified`, `verification_method`, etc.) and are flagged `is_bot` so they're excluded from human visitor and billing counts. (Surfacing these as dedicated dashboard filter dimensions is incremental.)

#### Platform setup guides

Copy-paste setup for each platform lives under **[Bot & Crawler Detection](/crawler-detection)** — pick yours:

> Batch requests (up to 500 per call) and send fire-and-forget so analytics never adds latency to your responses.

***

### Vercel Log Drain

Hosted on Vercel? Point a **Log Drain** at Kitbase to capture every request — including crawlers that never run JS — with zero code.

```
POST /ingest/v1/vercel
```

Vercel POSTs batches of request logs (JSON array or NDJSON); Kitbase reads each log's `proxy` object (`clientIp`, `userAgent`, `method`, `host`, `path`, `referer`), classifies the actor, and stores bot/crawler requests (humans ignored). The drain authenticates with your project's secret key (as a custom `Authorization: Bearer` header), which resolves the target project.

**`200 OK`** — `{ "accepted": <stored bots>, "received": <request logs parsed> }`. Non-request logs (build/function output) are ignored.

See the [Vercel setup guide](/crawler-detection/vercel) for step-by-step Log Drain configuration. The same per-vendor pattern extends to other hosts (CloudFront, Netlify, Fastly) under `/ingest/v1/<vendor>`.

***

### AWS CloudFront Standard Logs

Served through **AWS CloudFront** (including **AWS Amplify** sites)? Stream CloudFront **standard logs (v2)** to Kitbase through **Amazon Data Firehose** to capture every request — including crawlers that never run JS — with zero code.

```
POST /ingest/v1/cloudfront
```

Firehose POSTs batched log records (base64 envelope, JSON output format); Kitbase decodes each CloudFront log's fields (`c-ip`, `cs(User-Agent)`, `cs-method`, `cs(Host)`, `cs-uri-stem`, `cs(Referer)`), classifies the actor, and stores bot/crawler requests (humans ignored). The stream authenticates with your project's **secret** key, which Firehose sends in the `X-Amz-Firehose-Access-Key` header and which resolves the target project.

**`200 OK`** — Firehose's required acknowledgement, `{ "requestId": "…", "timestamp": <ms> }`. Rows without a usable IP + User-Agent are ignored; a non-2xx makes Firehose retry then back up to S3.

See the [AWS CloudFront setup guide](/crawler-detection/cloudfront) for the CloudFront + Firehose configuration.

***

### Identify User

Link an anonymous user to an identified user.

```
POST /sdk/v1/identify
```

#### Request Body

```json
{
  "anonymous_id": "550e8400-e29b-41d4-a716-446655440000",
  "user_id": "user-123",
  "traits": {
    "email": "user@example.com",
    "plan": "premium"
  }
}
```

#### Parameters

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `anonymous_id` | `string` | Yes | Anonymous user ID to link |
| `user_id` | `string` | Yes | Identified user ID |
| `traits` | `object` | No | User properties |

#### Response

**Status: `200 OK`**

```json
{
  "success": true
}
```

***

## Tag Enrichment

Events are automatically enriched with these tags:

| Tag | Description |
|-----|-------------|
| `__browser` | Browser name |
| `__browser_version` | Browser version |
| `__os` | Operating system |
| `__os_version` | OS version |
| `__device` | Device type |
| `__country` | Country code |
| `__region` | Region/State |
| `__city` | City |
| `__session_id` | Session ID |
| `__path` | Page path |
| `__referrer` | Referrer URL |
| `__utm_source` | UTM source |
| `__utm_medium` | UTM medium |
| `__utm_campaign` | UTM campaign |

***

## HTTP Status Codes

| Code | Description |
|------|-------------|
| `200` | Success |
| `202` | Accepted (queued) |
| `400` | Invalid parameters |
| `401` | Missing or invalid credential |
| `403` | Authenticated, but not permitted to do this |
| `404` | Not found (or not visible to this credential) |
| `409` | Conflict with existing state |
| `429` | Rate limited — see [Rate Limits](#rate-limits) |
| `5xx` | Unexpected server error |

`401` and `403` mean different things and are worth handling differently: `401` says the credential is absent, malformed or expired — presenting a valid one will work. `403` says the credential was accepted and still may not perform this action, so retrying with the same key never helps. A log drain that receives `401` should check that its key is actually being sent.

## Error Response

Every non-2xx response — from any endpoint, including authentication failures raised before a request reaches a handler — has the same JSON body, the `ErrorResponse` schema in the [OpenAPI spec](#openapi-specification):

```json
{
  "error": {
    "code": "AUTH_003",
    "message": "Authentication required",
    "details": {}
  },
  "timestamp": "2026-08-22T10:45:52.855Z"
}
```

| Field | Meaning |
|-------|---------|
| `error.code` | Stable, machine-readable identifier. Branch on this, not on `message`. Families: `AUTH_*`, `PERM_*`, `VAL_*`, `ORG_*`, `PROJECT_*`, `BILLING_*`, `WF_*`, … |
| `error.message` | Human-readable explanation; may change without notice |
| `error.details` | Typed extras when they exist — `field`, `value`, `resource`, `reason`, `retryAfterSeconds` |
| `timestamp` | When the error was produced (RFC 3339) |

## Rate Limits

Authenticated requests are limited **per credential** (user, private API key or MCP connection) to **1,000 requests per minute** in a fixed one-minute window. Every response carries the IETF rate-limit headers plus the legacy `X-RateLimit-*` trio, so a client can throttle itself before it is throttled:

```
RateLimit-Policy: "per-minute";q=1000;w=60
RateLimit: "per-minute";r=997;t=42
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 42
```

| Header | Meaning |
|--------|---------|
| `RateLimit-Policy` | The policy: quota `q` requests per window `w` seconds |
| `RateLimit` | Live state: `r` requests remaining, `t` seconds until the window resets |
| `X-RateLimit-Limit` / `-Remaining` / `-Reset` | The same three numbers, in the older de-facto format |

Over the limit, the response is `429` with the standard [error body](#error-response) (`AUTH_014`) and a `Retry-After` header in seconds. Public, unauthenticated endpoints (`/public/*`) have their own stricter per-IP limits and answer `429` + `Retry-After` the same way. Event ingestion (`/sdk/v1/*`, `/ingest/v1/*`) is not subject to the per-credential limit.

## Versioning and Deprecation

The API is **version 1** (`info.version` in the spec). Paths are stable and changes are additive: new fields, parameters and endpoints may appear at any time, and clients must ignore fields they do not recognise. Breaking changes ship under a new path prefix (`/v2/…`) rather than by changing an existing operation.

An operation scheduled for removal is marked `deprecated: true` in the OpenAPI spec at least **90 days** before its sunset, and during that period every response from it carries:

```
Deprecation: @1767225600
Sunset: Wed, 01 Apr 2026 00:00:00 GMT
Link: <https://docs.kitbase.dev/changelog/>; rel="deprecation"; type="text/html"
```

`Deprecation` ([RFC 9745](https://www.rfc-editor.org/rfc/rfc9745)) is the date the deprecation was announced, `Sunset` ([RFC 8594](https://www.rfc-editor.org/rfc/rfc8594)) the date after which the operation may stop working, and the `Link` points at the migration notes. Changes are announced in the [changelog](/changelog/).

## Next steps

* [OpenAPI spec](https://api.kitbase.dev/openapi.json) — the complete, machine-readable public surface.
* [MCP Server](/sdks/mcp) — the same data and actions for AI assistants, with a server card for discovery.
* [CLI](/cli) — every endpoint here is also a `kitbase` command.
* [SDKs & Tools](/sdks/) — typed SDKs that wrap this API.
* [Bot & Crawler Detection](/crawler-detection) — platform guides for the server-side ingestion endpoint.
