Skip to content

Latest commit

 

History

History
703 lines (547 loc) · 14.3 KB

File metadata and controls

703 lines (547 loc) · 14.3 KB

HTTP API Reference

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)

Authentication

Development: No authentication required when connecting directly to the Rust server

Production: The Go gateway provides Google OAuth authentication:

  • Unauthenticated requests to /v1/* return 302 Found redirect to /login
  • After OAuth, requests include session cookie
  • Session expires after 24 hours of inactivity

Contexts

List Contexts

GET /v1/contexts

Query 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 Context Details

GET /v1/contexts/:context_id

Query 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

List Child Contexts

GET /v1/contexts/:context_id/children

Query 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

Create Context

POST /v1/contexts/create

Alias:

POST /v1/contexts

Request 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
}

Fork Context

POST /v1/contexts/fork

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

Turns

Get Turns from Context

GET /v1/contexts/:context_id/turns

Query 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=100

Use next_before_turn_id from the previous response to continue paging.

Append Turn

POST /v1/contexts/:context_id/append

Alias:

POST /v1/contexts/:context_id/turns

Request 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 exist
  • 409 Conflict - Invalid parent_turn_id
  • 422 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.

Registry

Publish Type Bundle

PUT /v1/registry/bundles/:bundle_id

Request 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 stored
  • 204 No Content - Identical bundle already exists
  • 409 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 Type Bundle

GET /v1/registry/bundles/:bundle_id

Response:

{
  "registry_version": 1,
  "bundle_id": "...",
  "types": { ... }
}

Headers:

  • ETag: "abc123" - For caching
  • Cache-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 Type Version Descriptor

GET /v1/registry/types/:type_id/versions/:type_version

Example:

GET /v1/registry/types/com.example.Message/versions/1

Response:

{
  "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

List Latest Type Versions

GET /v1/registry/types

Response:

{
  "types": [
    {
      "type_id": "com.example.Message",
      "latest_version": 2,
      "bundle_id": "2025-01-30T10:00:00Z#abc123"
    }
  ]
}

Blobs

Get Blob by Hash

GET /v1/blobs/:content_hash

Example:

GET /v1/blobs/a3f5b8c2d1e4f6a9b2c5d8e1f4a7b0c3d6e9f2a5b8c1d4e7f0a3b6c9d2e5f8a1

Response:

  • Content-Type: application/octet-stream
  • Body: Raw uncompressed bytes

Error Responses:

  • 404 Not Found - Blob doesn't exist

Health and Status

Health Check

GET /health

Response:

{
  "status": "ok",
  "version": "1.0.0",
  "uptime_seconds": 3600
}

Storage Stats

GET /v1/stats

Response:

{
  "contexts": 100,
  "turns": 10000,
  "blobs": 5000,
  "storage_bytes": 52428800,
  "dedup_hit_rate": 0.35
}

Error Responses

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

Rate Limiting

Development: No rate limits

Production (with gateway):

  • 1000 requests/minute per user
  • 429 Too Many Requests when exceeded
  • Retry-After: 60 header indicates retry time

CORS

Development: All origins allowed (Access-Control-Allow-Origin: *)

Production (with gateway): Configured via ALLOWED_ORIGINS environment variable

Examples

Complete Flow: Create Context and Add Turns

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

Branching Example

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

Publish Type Registry

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

Client Libraries

JavaScript/TypeScript

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 });

Python

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)

See Also