REST API

The CCD-Suite REST API exposes every entity in the platform — contacts, deals, posts, agent runs, planned actions, KPIs — under a consistent URL shape, authentication contract, and pagination model. The same API powers the web app's own data fetches.

Base URL

EditionBase URL
Cloudhttps://api.ccdsuite.com
Self-hosthttps://your-domain.com (Kong on port 8000 if not behind a proxy)

The web app talks to the same endpoints under /api/... — server- side proxying through Next.js. External clients use the base URL directly.

Authentication

Two auth modes:

ModeWhen to use
Bearer JWTUser-context — the API call acts as a specific user. Used by the web app + any client that signs the user in.
API keyService-context — a long-lived token tied to the tenant, not a user. Used by webhooks, cron jobs, downstream services.

Bearer JWT

Standard Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

The JWT is issued by Supabase GoTrue at sign-in. Refresh via the standard Supabase auth flow. Token lifetime is 1 hour by default; the client refreshes silently before expiry.

API key

X-API-Key: ccd_live_<base32_token>

API keys are created at Settings → Tenant → API keys (admins only). Each key:

  • Is named (e.g. "Production webhook handler").
  • Has a scope (read-only, read-write, or specific endpoint set).
  • Has a created-at + last-used-at timestamp.
  • Can be rotated or revoked instantly.

Keys are shown ONCE at creation — copy immediately. The server stores only the hash.

Endpoints

A non-exhaustive map (full OpenAPI spec at /api/openapi.json):

GroupExample
AuthPOST /api/auth/login
CRMGET /api/crm/contacts · POST /api/crm/deals · PATCH /api/crm/deals/:id
ContentGET /api/content/campaigns · POST /api/content/posts
AnalyticsGET /api/analytics/kpis · POST /api/analytics/queries
AgentPOST /api/ai/chat · POST /api/ai/chat/stream (SSE) · GET /api/ai/runs/:id
TasksGET /api/tasks/pending · POST /api/tasks/:id/confirm
TodayGET /api/dashboard/cards · POST /api/dashboard/cards/generate
GraphPOST /api/graph/search — wraps graph_semantic_search RPC
WebhooksPOST /api/webhooks/:provider — see Webhooks
HealthGET /api/health · GET /api/health/detailed

Request shape

JSON in, JSON out. POST/PATCH bodies are validated by the same Zod schemas the server uses internally — invalid input returns a structured 400 with the offending field path.

Example:

curl -X POST https://your-host/api/crm/deals \
  -H 'Authorization: Bearer eyJ...' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Acme Q3 expansion",
    "account_id": "acc_01HZ...",
    "primary_contact_id": "con_01HZ...",
    "amount": 75000,
    "currency": "USD",
    "stage": "discovery"
  }'

Response shape

All responses share an envelope:

{
  "success": true,
  "data": { ... },
  "meta": { "request_id": "req_01HZ..." }
}

Or, on error:

{
  "success": false,
  "error": {
    "message": "Validation failed: amount must be positive",
    "code": "VALIDATION_ERROR",
    "details": { "field": "amount", "value": -100 }
  },
  "meta": { "request_id": "req_01HZ..." }
}

request_id is in every response — include it when filing support tickets. Server logs are indexed by it.

Pagination

Cursor-based, not offset-based. List endpoints accept:

Query paramPurpose
limitPage size (default 50, max 200)
cursorOpaque cursor from previous response's meta.next_cursor
ordercreated_at_desc (default), created_at_asc, updated_at_desc

Response includes meta.next_cursor (null if no more pages) and meta.has_more (boolean). Iterate until has_more is false.

curl '...?limit=100&cursor=eyJpZCI...'

Filtering

List endpoints accept filter query params using a typed mini- query language:

GET /api/crm/deals?filter.stage=in:(discovery,proposal)&filter.amount=gte:50000

Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, is_null, is_not_null. Same operators across every list endpoint.

For complex queries beyond what the URL grammar supports, use POST /api/graph/search with a query body.

Rate limits

Per-tenant + per-endpoint group:

PlanReads/minWrites/minAI calls/min
Cloud Starter3006010
Cloud Scale60001500300
Cloud EnterpriseCustomCustomCustom
Self-hostOperator-tunableOperator-tunableOperator-tunable

429 includes Retry-After (seconds) + X-RateLimit-Reset (unix epoch). Burst capacity is ~10x sustained — short spikes are absorbed without 429s.

Errors

StatusMeaning
400Validation / shape error. error.details has specifics.
401Missing or invalid auth.
403Auth OK but you can't do that (RLS / role).
404Entity doesn't exist OR exists but you can't see it (we don't distinguish — leaking that would be an RLS bypass).
409Conflict — e.g. duplicate idempotency key with different payload.
422Semantic error — request well-formed but rejected by business rules.
429Rate limit. Retry-After header.
5xxServer error. request_id for support.

Server errors are auto-reported (cloud: Sentry; self-host: your observability stack if enabled). You don't need to file a ticket for every 500; if it persists, do.

Idempotency

Mutating endpoints (POST / PATCH / DELETE) accept an Idempotency-Key header. Repeated requests with the same key return the original response without re-executing. Useful for retries when the network is unreliable.

Idempotency-Key: <ulid>

Without the header, retries can double-execute. With it, only the first request takes effect — the platform stores the response keyed by (tenant_id, endpoint, idempotency_key) for 24 hours.

OpenAPI

The machine-readable spec is at /api/openapi.json. Generates from the same Zod schemas the server uses, so it's always current. Use it with OpenAPI clients (openapi-typescript, swagger-codegen, etc.) to generate typed SDKs in your language of choice.

Read next

  • Webhooks — events the platform pushes to you.
  • Security model — RLS contract that every API call is subject to.