The CXDB HTTP gateway provides a JSON API for reading turns, managing contexts, and publishing type registry bundles. It's designed for UI clients and tooling that need typed projections.
Base URL: http://localhost:9010 (development) or https://your-domain.com (production with gateway)
Development: No authentication required when connecting directly to the Rust server
Production: The Go gateway provides Google OAuth authentication:
- Unauthenticated requests to
/v1/*return302 Foundredirect to/login - After OAuth, requests include session cookie
- Session expires after 24 hours of inactivity
GET /v1/contextsQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 100 | Max contexts to return |
tag |
string | - | Filter by exact client tag |
include_provenance |
bool | false | Include provenance in each context |
include_lineage |
bool | false | Include parent/root/children lineage summary |
Response:
{
"contexts": [
{
"context_id": "1",
"head_turn_id": "42",
"head_depth": 42,
"created_at": "2025-01-30T10:00:00Z"
}
],
"total": 1
}GET /v1/contexts/:context_idQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
include_provenance |
bool | true | Include provenance block |
include_lineage |
bool | true | Include lineage block with parent/root/children |
Response:
{
"context_id": "1",
"head_turn_id": "42",
"head_depth": 42,
"created_at": "2025-01-30T10:00:00Z"
}Error Responses:
404 Not Found- Context doesn't exist
GET /v1/contexts/:context_id/childrenQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
recursive |
bool | false | Include all descendants, not just direct children |
limit |
int | 256 | Max child contexts to return |
include_provenance |
bool | true | Include provenance in each child |
include_lineage |
bool | true | Include lineage in each child |
POST /v1/contexts/createAlias:
POST /v1/contextsRequest Body:
{
"base_turn_id": "0"
}base_turn_id:"0"for empty context, or turn ID to start from
Response:
{
"context_id": "1",
"head_turn_id": "0",
"head_depth": 0
}POST /v1/contexts/forkRequest Body:
{
"base_turn_id": "42"
}Response:
{
"context_id": "2",
"head_turn_id": "42",
"head_depth": 42
}Creates a new context whose head is the specified turn. The new context shares history up to that turn but can diverge with new appends.
GET /v1/contexts/:context_id/turnsQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 64 | Max turns to return |
before_turn_id |
string | - | For paging: return turns older than this |
view |
string | typed |
Response format: typed, raw, both |
type_hint_mode |
string | inherit |
Type resolution: inherit, latest, explicit |
as_type_id |
string | - | Override type (requires explicit mode) |
as_type_version |
int | - | Override version (requires explicit mode) |
include_unknown |
bool | false | Include unknown fields in response |
bytes_render |
string | base64 |
Binary encoding: base64, hex, len_only |
u64_format |
string | string |
Large int format: string, number |
enum_render |
string | label |
Enum display: label, number, both |
time_render |
string | iso |
Timestamp format: iso, unix_ms |
Response (view=typed):
{
"meta": {
"context_id": "1",
"head_turn_id": "3",
"head_depth": 3,
"registry_bundle_id": "2025-01-30T10:00:00Z#abc123"
},
"turns": [
{
"turn_id": "1",
"parent_turn_id": "0",
"depth": 1,
"declared_type": {
"type_id": "com.example.Message",
"type_version": 1
},
"decoded_as": {
"type_id": "com.example.Message",
"type_version": 1
},
"data": {
"role": "user",
"text": "What is 2+2?"
}
},
{
"turn_id": "2",
"parent_turn_id": "1",
"depth": 2,
"declared_type": {
"type_id": "com.example.Message",
"type_version": 1
},
"decoded_as": {
"type_id": "com.example.Message",
"type_version": 1
},
"data": {
"role": "assistant",
"text": "2+2 equals 4."
}
}
],
"next_before_turn_id": "1"
}Response (view=raw):
{
"meta": { ... },
"turns": [
{
"turn_id": "1",
"parent_turn_id": "0",
"depth": 1,
"declared_type": {
"type_id": "com.example.Message",
"type_version": 1
},
"content_hash_b3": "a3f5b8c2...",
"encoding": 1,
"compression": 0,
"uncompressed_len": 42,
"bytes_b64": "gaJyb2xlo3VzZXK..."
}
]
}Response (view=both):
Combines both data and raw fields in each turn.
Paging:
To fetch older turns:
GET /v1/contexts/1/turns?limit=10&before_turn_id=100Use next_before_turn_id from the previous response to continue paging.
POST /v1/contexts/:context_id/appendAlias:
POST /v1/contexts/:context_id/turnsRequest Body:
{
"type_id": "com.example.Message",
"type_version": 1,
"data": {
"role": "user",
"text": "Hello!"
},
"parent_turn_id": "0",
"idempotency_key": "client-123-1706615000-001"
}| Field | Type | Required | Description |
|---|---|---|---|
type_id |
string | Yes | Type identifier |
type_version |
int | Yes | Type version |
data |
object | Yes* | Turn payload (will be encoded as msgpack) |
payload |
object | Yes* | Alias for data (for compatibility) |
parent_turn_id |
string | No | Parent turn (default: current head) |
idempotency_key |
string | No | For safe retries |
*At least one of data or payload is required.
Response:
{
"context_id": "1",
"turn_id": "1",
"depth": 1,
"content_hash": "a3f5b8c2..."
}Error Responses:
404 Not Found- Context doesn't exist409 Conflict- Invalid parent_turn_id422 Unprocessable Entity- Invalid data or missing type
Note: The HTTP API accepts JSON payloads and converts them to msgpack internally. If a type descriptor exists, numeric tags are derived from the registry. If no descriptor exists, the JSON structure is still persisted as msgpack (string/numeric keys preserved). For maximum control over encoding, use the binary protocol.
PUT /v1/registry/bundles/:bundle_idRequest Body: (JSON)
{
"registry_version": 1,
"bundle_id": "2025-01-30T10:00:00Z#abc123",
"types": {
"com.example.Message": {
"versions": {
"1": {
"fields": {
"1": { "name": "role", "type": "string" },
"2": { "name": "text", "type": "string", "optional": true },
"3": { "name": "timestamp", "type": "u64", "semantic": "unix_ms" }
}
},
"2": {
"fields": {
"1": { "name": "role", "type": "string" },
"2": { "name": "text", "type": "string", "optional": true },
"3": { "name": "timestamp", "type": "u64", "semantic": "unix_ms" },
"4": { "name": "attachments", "type": "array", "items": "bytes" }
}
}
}
}
},
"enums": {
"com.example.Role": {
"1": "system",
"2": "user",
"3": "assistant",
"4": "tool"
}
}
}Response:
201 Created- New bundle stored204 No Content- Identical bundle already exists409 Conflict- Invalid evolution (tag reuse, version regression)422 Unprocessable Entity- Malformed bundle
Bundle ID Format:
Use timestamp + hash: 2025-01-30T10:00:00Z#abc123
GET /v1/registry/bundles/:bundle_idResponse:
{
"registry_version": 1,
"bundle_id": "...",
"types": { ... }
}Headers:
ETag: "abc123"- For cachingCache-Control: public, max-age=31536000- Bundles are immutable
Conditional Requests:
GET /v1/registry/bundles/:bundle_id
If-None-Match: "abc123"Returns 304 Not Modified if ETag matches.
GET /v1/registry/types/:type_id/versions/:type_versionExample:
GET /v1/registry/types/com.example.Message/versions/1Response:
{
"type_id": "com.example.Message",
"type_version": 1,
"fields": {
"1": { "name": "role", "type": "string" },
"2": { "name": "text", "type": "string", "optional": true }
}
}Error Responses:
404 Not Found- Type or version doesn't exist
GET /v1/registry/typesResponse:
{
"types": [
{
"type_id": "com.example.Message",
"latest_version": 2,
"bundle_id": "2025-01-30T10:00:00Z#abc123"
}
]
}GET /v1/blobs/:content_hashExample:
GET /v1/blobs/a3f5b8c2d1e4f6a9b2c5d8e1f4a7b0c3d6e9f2a5b8c1d4e7f0a3b6c9d2e5f8a1Response:
- Content-Type:
application/octet-stream - Body: Raw uncompressed bytes
Error Responses:
404 Not Found- Blob doesn't exist
GET /healthResponse:
{
"status": "ok",
"version": "1.0.0",
"uptime_seconds": 3600
}GET /v1/statsResponse:
{
"contexts": 100,
"turns": 10000,
"blobs": 5000,
"storage_bytes": 52428800,
"dedup_hit_rate": 0.35
}All errors return JSON with this format:
{
"error": {
"code": "NOT_FOUND",
"message": "Context 999 not found",
"details": {
"context_id": "999"
}
}
}Common Error Codes:
| HTTP Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST |
Malformed request |
| 401 | UNAUTHORIZED |
Missing/invalid auth (gateway only) |
| 404 | NOT_FOUND |
Resource doesn't exist |
| 409 | CONFLICT |
Invalid operation (e.g., bad parent) |
| 412 | PRECONDITION_FAILED |
Missing type registry |
| 422 | UNPROCESSABLE_ENTITY |
Invalid data |
| 424 | FAILED_DEPENDENCY |
Missing type descriptor |
| 500 | INTERNAL_ERROR |
Server error |
Development: No rate limits
Production (with gateway):
- 1000 requests/minute per user
429 Too Many Requestswhen exceededRetry-After: 60header indicates retry time
Development: All origins allowed (Access-Control-Allow-Origin: *)
Production (with gateway): Configured via ALLOWED_ORIGINS environment variable
# Create context
curl -X POST http://localhost:9010/v1/contexts/create \
-H "Content-Type: application/json" \
-d '{"base_turn_id": "0"}' \
| jq .
# Output: {"context_id": "1", "head_turn_id": "0", "head_depth": 0}
# Append user message
curl -X POST http://localhost:9010/v1/contexts/1/append \
-H "Content-Type: application/json" \
-d '{
"type_id": "com.example.Message",
"type_version": 1,
"data": {
"role": "user",
"text": "What is the weather?"
}
}' | jq .
# Output: {"context_id": "1", "turn_id": "1", "depth": 1, ...}
# Append assistant response
curl -X POST http://localhost:9010/v1/contexts/1/append \
-H "Content-Type: application/json" \
-d '{
"type_id": "com.example.Message",
"type_version": 1,
"data": {
"role": "assistant",
"text": "I need your location to check the weather."
}
}' | jq .
# Get conversation
curl http://localhost:9010/v1/contexts/1/turns?limit=10 | jq .# Fork from turn 1
curl -X POST http://localhost:9010/v1/contexts/fork \
-H "Content-Type: application/json" \
-d '{"base_turn_id": "1"}' \
| jq .
# Output: {"context_id": "2", "head_turn_id": "1", "head_depth": 1}
# Append alternate response to new context
curl -X POST http://localhost:9010/v1/contexts/2/append \
-H "Content-Type: application/json" \
-d '{
"type_id": "com.example.Message",
"type_version": 1,
"data": {
"role": "assistant",
"text": "The weather is sunny and 72°F."
}
}' | jq .curl -X PUT http://localhost:9010/v1/registry/bundles/2025-01-30T10:00:00Z \
-H "Content-Type: application/json" \
-d '{
"registry_version": 1,
"bundle_id": "2025-01-30T10:00:00Z",
"types": {
"com.example.Message": {
"versions": {
"1": {
"fields": {
"1": {"name": "role", "type": "string"},
"2": {"name": "text", "type": "string"}
}
}
}
}
}
}'import { CxdbClient } from '@strongdm/cxdb';
const client = new CxdbClient('http://localhost:9010');
// Create context
const ctx = await client.createContext();
// Append turn
const turn = await client.appendTurn(ctx.context_id, {
type_id: 'com.example.Message',
type_version: 1,
data: {
role: 'user',
text: 'Hello!'
}
});
// Get turns
const turns = await client.getTurns(ctx.context_id, { limit: 10 });from cxdb import Client
client = Client("http://localhost:9010")
# Create context
ctx = client.create_context()
# Append turn
turn = client.append_turn(ctx.context_id, {
"type_id": "com.example.Message",
"type_version": 1,
"data": {
"role": "user",
"text": "Hello!"
}
})
# Get turns
turns = client.get_turns(ctx.context_id, limit=10)- Binary Protocol - For high-throughput writers
- Type Registry - Defining custom types
- Renderers - Custom UI visualizations
- Troubleshooting - Debugging API issues