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
| Edition | Base URL |
|---|---|
| Cloud | https://api.ccdsuite.com |
| Self-host | https://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:
| Mode | When to use |
|---|---|
| Bearer JWT | User-context — the API call acts as a specific user. Used by the web app + any client that signs the user in. |
| API key | Service-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):
| Group | Example |
|---|---|
| Auth | POST /api/auth/login |
| CRM | GET /api/crm/contacts · POST /api/crm/deals · PATCH /api/crm/deals/:id |
| Content | GET /api/content/campaigns · POST /api/content/posts |
| Analytics | GET /api/analytics/kpis · POST /api/analytics/queries |
| Agent | POST /api/ai/chat · POST /api/ai/chat/stream (SSE) · GET /api/ai/runs/:id |
| Tasks | GET /api/tasks/pending · POST /api/tasks/:id/confirm |
| Today | GET /api/dashboard/cards · POST /api/dashboard/cards/generate |
| Graph | POST /api/graph/search — wraps graph_semantic_search RPC |
| Webhooks | POST /api/webhooks/:provider — see Webhooks |
| Health | GET /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 param | Purpose |
|---|---|
limit | Page size (default 50, max 200) |
cursor | Opaque cursor from previous response's meta.next_cursor |
order | created_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:
| Plan | Reads/min | Writes/min | AI calls/min |
|---|---|---|---|
| Cloud Starter | 300 | 60 | 10 |
| Cloud Scale | 6000 | 1500 | 300 |
| Cloud Enterprise | Custom | Custom | Custom |
| Self-host | Operator-tunable | Operator-tunable | Operator-tunable |
429 includes Retry-After (seconds) + X-RateLimit-Reset
(unix epoch). Burst capacity is ~10x sustained — short spikes
are absorbed without 429s.
Errors
| Status | Meaning |
|---|---|
| 400 | Validation / shape error. error.details has specifics. |
| 401 | Missing or invalid auth. |
| 403 | Auth OK but you can't do that (RLS / role). |
| 404 | Entity doesn't exist OR exists but you can't see it (we don't distinguish — leaking that would be an RLS bypass). |
| 409 | Conflict — e.g. duplicate idempotency key with different payload. |
| 422 | Semantic error — request well-formed but rejected by business rules. |
| 429 | Rate limit. Retry-After header. |
| 5xx | Server 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.