VectorDB API
This page lists every endpoint on the VectorDB service (vectordb-api), with request fields, response shapes, and the exact error messages the server returns. Use it when you are writing code against the API; for a guided introduction, start with the VectorDB overview and quickstart.
Base URL
VectorDB has its own API service, separate from the core platform API. In-cluster it is vectordb-api.cai-system.svc:8080.
The platform is in alpha and does not publish a public hostname for this API. Reach it through a port-forward (shown below), or through an endpoint your administrator provides. The platformctl CLI does this automatically when $CAI_VECTORDB_API is not set.
kubectl -n cai-system port-forward svc/vectordb-api 18080:8080 &
export CAI_VECTORDB_API=http://localhost:18080
Authentication
Every route except GET /healthz requires a bearer credential:
Authorization: Bearer <token-or-api-key>
The credential is a session token (from POST /v1/auth/login on the core API, 12-hour life), a service-account or personal API key (prefix cai_), or the platform automation token. Your project role is re-read from the database on every request, so a role change takes effect immediately. See API authentication.
Two roles matter here:
| Role | Can call |
|---|---|
| member | list, get, create index; :upsert, :query, :scroll, :delete-points |
| admin | everything a member can, plus PATCH index, DELETE index, and GET /vectordb/credentials |
Conventions
- Error envelope. Every non-2xx response is
{"error": "<message>", "request_id": "<id>"}. Unknown paths return a 404 in this envelope; a wrong HTTP method returns a 405. - The 404 rule. A request against an index (or project) you hold no grant on returns 404 — never 403 — so resource existence is not discoverable. You only see 403 when you are a member but the action needs admin.
- Pagination. List endpoints accept
page_size(1–200, default 50;limitis an alias) andpage_token(opaque). Responses carrynext_page_token, absent on the last page. Out-of-range sizes are rejected with 400page_size must be between 1 and 200, never clamped. A token from a different list or project returns 400page_token is invalid or was issued for a different list; start from the first page. - Custom methods. Data operations use GCP-style
:verbsuffixes on the index path (:upsert,:query,:scroll,:delete-points), always with POST. - Rate limiting. A per-principal token bucket wraps every route except
/healthz; shed requests get 429.
Endpoints at a glance
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/projects/{projectID}/indexes | member | List indexes |
| POST | /v1/projects/{projectID}/indexes | member | Create an index |
| GET | /v1/projects/{projectID}/indexes/{name} | member | Get one index |
| PATCH | /v1/projects/{projectID}/indexes/{name} | admin | Edit mutable fields |
| DELETE | /v1/projects/{projectID}/indexes/{name} | admin | Delete an index |
| POST | /v1/projects/{projectID}/indexes/{name}:upsert | member | Write points |
| POST | /v1/projects/{projectID}/indexes/{name}:query | member | Similarity search |
| POST | /v1/projects/{projectID}/indexes/{name}:scroll | member | Browse points in id order |
| POST | /v1/projects/{projectID}/indexes/{name}:delete-points | member | Delete points by id or filter |
| GET | /v1/projects/{projectID}/vectordb/credentials | admin | Direct Qdrant access credential |
| GET | /healthz | none | Health check |
The index object
Every read returns this shape (the IndexResponse):
{
"name": "docs",
"resource_path": "projects/<short>/indexes/docs",
"collection": "p_<short>_docs",
"dimensions": 1536,
"distance": "cosine",
"on_disk": false,
"payload_on_disk": false,
"quantization": {"kind": "none", "quantile": "", "always_ram": false},
"shards": 1,
"replicas": 1,
"state": "ready",
"ready": true,
"collection_status": "green",
"points_count": 0,
"vectors_count": 0,
"segments_count": 0,
"message": "",
"created_at": "2026-08-06T12:00:00Z"
}
| Field | Type | Meaning |
|---|---|---|
name | string | The index name you chose |
resource_path | string | Stable path: projects/<short>/indexes/<name> |
collection | string | The internal storage name (p_<short>_<name>). Reported so you can find your data; never accepted as input anywhere |
dimensions | int | Vector width. Immutable |
distance | string | cosine, dot, or euclid. Immutable |
on_disk | bool | Vectors stored on disk instead of memory. Immutable |
payload_on_disk | bool | Payloads stored on disk. Editable via PATCH |
quantization | object | kind (none/scalar/binary), quantile, always_ram. Immutable |
shards | int | Shard count. Immutable |
replicas | int | Replica count (1–8). Editable via PATCH |
state | string | pending, creating, ready, degraded, conflict, or blocked |
ready | bool | The authoritative "you can use it now" signal |
collection_status | string | The engine's own health word (green/yellow/red/grey); present only once the collection exists |
points_count, vectors_count, segments_count | int | Live counts |
message | string | Why the index is not ready (for example, a conflict explanation) |
created_at | string | RFC 3339 timestamp |
List indexes
GET /v1/projects/{projectID}/indexes
Auth: member.
Query parameters:
| Name | Type | Required | Default | Notes |
|---|---|---|---|---|
page_size | int | no | 50 | 1–200; limit is an alias |
page_token | string | no | — | Opaque cursor from a previous page |
Response 200:
{"indexes": [], "next_page_token": "..."}
next_page_token is absent on the last page.
Errors: 400 page_size must be between 1 and 200; 400 page_token is invalid or was issued for a different list; start from the first page.
Create an index
POST /v1/projects/{projectID}/indexes
Auth: member. Returns 201 with an index object in state pending — the collection is created asynchronously. Poll GET until ready: true; data-plane calls return 409 until then.
Request body (only name is required — {"name":"docs"} is a complete request):
{
"name": "docs",
"dimensions": 256,
"distance": "cosine",
"on_disk": false,
"payload_on_disk": false,
"quantization": {"kind": "scalar", "quantile": "0.99", "always_ram": true},
"shards": 1,
"replicas": 1
}
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
name | string | yes | — | Lowercase letters, digits, hyphens; must start and end alphanumeric; max 48 chars |
dimensions | int | no | 1536 | 1–65536; omitted or 0 means 1536. Immutable after create |
distance | string | no | cosine | Case-insensitive: cosine; dot (aliases dotproduct, dot_product); euclid (aliases euclidean, l2) |
on_disk | bool | no | false | Immutable |
payload_on_disk | bool | no | false | Editable later |
quantization.kind | string | no | none | none, scalar (alias int8), or binary. Immutable |
quantization.quantile | string | no | — | Scalar quantization quantile |
quantization.always_ram | bool | no | false | Keep quantized vectors in memory |
shards | int | no | 1 | Immutable |
replicas | int | no | 1 | 1–8; editable later |
Errors:
| Status | Message |
|---|---|
| 400 | dimensions must be between 1 and 65536, or omit it for the default of 1536 |
| 400 | distance must be one of "cosine", "dot" or "euclid" |
| 400 | quantization.kind must be one of "none", "scalar" or "binary" |
| 400 | index name %q must match [a-z0-9]([a-z0-9-]*[a-z0-9])? - lowercase letters, digits and hyphens, starting and ending with an alphanumeric |
| 400 | index name %q is longer than 48 characters |
| 409 | an index with that name already exists |
Get an index
GET /v1/projects/{projectID}/indexes/{name}
Auth: member. Returns 200 with the index object. A missing index, a malformed name, and another project's index all return the same 404 — deliberately indistinguishable.
Edit an index
PATCH /v1/projects/{projectID}/indexes/{name}
Auth: admin. Returns 200 with the updated index object. Only two fields are editable:
{"replicas": 2, "payload_on_disk": true}
| Field | Type | Rules |
|---|---|---|
replicas | int | 1–8. 400 replicas must be between 1 and 8 |
payload_on_disk | bool | — |
Sending an immutable field returns a 400 that names it:
| Field sent | Message |
|---|---|
dimensions | dimensions is immutable: existing points cannot be re-embedded, so a different width is a new index, not an edit |
distance | distance is immutable: the metric is baked into the graph built over the existing points |
shards | shards is immutable: resharding moves data and is not performed on a spec edit |
on_disk | on_disk is immutable: vectors are not restaged between memory and disk in place |
quantization | quantization is immutable: it is fixed when the collection is built |
| (empty patch) | no mutable fields to change; the editable fields are replicas and payload_on_disk |
Delete an index
DELETE /v1/projects/{projectID}/indexes/{name}
Auth: admin. Returns 204 with no body.
Deleting an index destroys the collection and every vector in it. There are no snapshots, no backups, and no restore.
Custom methods
The four data operations are POSTs to the index path with a :verb suffix. Two routing errors apply to all of them:
- A POST with an unknown verb returns 400
unknown method "<verb>"; expected upsert, query, scroll or delete-points. - A POST with no
:verbat all returns 400expected a custom method: POST .../indexes/{name}:upsert, :query, :scroll or :delete-points. - While the index is still pending, all four return 409
index is not ready yet; its collection has not been created.
Upsert points
POST /v1/projects/{projectID}/indexes/{name}:upsert
Auth: member.
{"points": [
{"id": 42, "vector": [0.1, 0.2], "payload": {"tag": "alpha"}},
{"vector": [0.3, 0.4]}
]}
| Field | Type | Required | Rules |
|---|---|---|---|
points | array | yes | 1–1000 points per request |
points[].id | uint or UUID string | no | Omitted means a UUID is generated |
points[].vector | array of numbers | yes | Width must equal the index's dimensions |
points[].payload | object | no | Arbitrary JSON metadata, filterable at query time |
Batches are all-or-nothing: one bad point rejects the whole request, and nothing is written.
Response 200:
{"index": "docs", "upserted_count": 2}
Errors:
| Status | Message |
|---|---|
| 400 | points must contain at least one point |
| 400 | at most 1000 points per request (got N) |
| 400 | point 3 has 1536 dimensions; index "docs" expects 256 |
| 400 | point 0: id must be an unsigned integer or a UUID string |
| 400 | a string id must be a UUID (or use an unsigned integer) |
Query (similarity search)
POST /v1/projects/{projectID}/indexes/{name}:query
Auth: member.
{
"vector": [0.1, 0.2],
"top_k": 5,
"filter": {"must": [{"key": "tag", "match": {"value": "alpha"}}]},
"with_payload": true,
"with_vector": false,
"score_threshold": 0.5,
"offset": 0
}
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
vector | array of numbers | yes | — | Width must equal the index's dimensions |
top_k | int | no | 10 | Max 1000 |
filter | object | no | — | A Qdrant payload filter; must be a JSON object; max 64 KiB |
with_payload | bool | no | true | Include each hit's payload |
with_vector | bool | no | false | Include each hit's vector |
score_threshold | number | no | — | Drop hits scoring below this |
offset | int | no | 0 | Must not be negative |
Response 200 (payload and vector appear per the flags):
{"index": "docs", "results": [{"id": 42, "score": 0.98, "payload": {"tag": "alpha"}, "vector": [0.1, 0.2]}]}
Errors:
| Status | Message |
|---|---|
| 400 | top_k must be at most 1000 |
| 400 | filter must be a JSON object |
| 400 | filter is too large |
| 400 | offset must not be negative |
| 400 | query vector has N dimensions; index "docs" expects M |
| 400 | vector database rejected the request: <qdrant message> (engine-side rejections passed through) |
Scroll (browse points)
POST /v1/projects/{projectID}/indexes/{name}:scroll
Auth: member. Lists points in id order — no vector needed. This is what the console's data browser uses.
{"offset": null, "limit": 50}
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
limit | int | no | 50 | Max 1000 |
offset | uint or UUID string | no | — | The previous page's next_offset, echoed back |
Response 200:
{"index": "docs", "points": [{"id": 42, "payload": {"tag": "alpha"}}], "next_offset": 99}
next_offset is omitted when the walk is exhausted. Scrolled points never carry vectors.
Errors: 400 limit must be at most 1000; 400 offset must be an unsigned integer or a UUID string.
Delete points
POST /v1/projects/{projectID}/indexes/{name}:delete-points
Auth: member. Provide exactly one of ids or filter:
{"ids": [42, "6f1c..."]}
{"filter": {"must": [{"key": "tag", "match": {"value": "beta"}}]}}
| Field | Type | Rules |
|---|---|---|
ids | array | Max 1000 ids; each an unsigned integer or UUID string |
filter | object | A Qdrant payload filter; an empty object is refused |
Response 200 (ids_requested is omitted for filter deletes):
{"index": "docs", "status": "ok", "ids_requested": 2}
Errors:
| Status | Message |
|---|---|
| 400 | provide exactly one of ids or filter |
| 400 | at most 1000 ids per request |
| 400 | an empty filter would delete every point; delete the index instead if that is the intent |
Direct Qdrant credentials
GET /v1/projects/{projectID}/vectordb/credentials
Auth: admin. Returns a credential for talking to the underlying Qdrant engine directly, scoped to your project's collections only.
Response 200:
{
"service_url": "http://qdrant.cai-services.svc.cluster.local:6333",
"external_service_url": "https://qdrant.<domain>",
"token": "<HS256 JWT>",
"collection_prefix": "p_<short>_",
"collections": ["p_<short>_docs"],
"truncated": false,
"note": "this credential is scoped to only this project's index collections..."
}
| Field | Meaning |
|---|---|
service_url | In-cluster Qdrant address |
external_service_url | TLS endpoint reachable from your Crusoe VPC (never the public internet). Present only alongside a token |
token | A JWT you present to Qdrant as the api-key header (or as a Bearer token). Read-write on the listed collections; cannot create collections |
collection_prefix | Your project's collection name prefix |
collections | The exact collection names the token covers, snapshotted at mint time |
truncated | true when more than 256 collections exist; only 256 are enumerated per token |
note | Plain-English explanation of exactly which of the above applies to this response |
The token is minted only when the platform enforces per-project access control on Qdrant — which is off by default in the current alpha. Without it you get the addresses and collection names but no credential. The token's scope is a snapshot of exact collection names: an index created later needs a fresh fetch. The raw Qdrant master key is never returned.
Health check
GET /healthz
No auth. Response 200:
{"status": "ok", "dependencies": {
"vector_store": {"ok": true, "latency_ms": 3},
"kubernetes": {"ok": true, "latency_ms": 12},
"database": {"ok": true, "latency_ms": 1}}}
When any dependency fails, the status becomes "degraded", the HTTP status is 503, and each failing dependency carries an error field. The internal probe budget is 5 seconds.
Server limits
These are enforced server-side. Some are operator-tunable environment variables; the values below are the defaults.
| Limit | Value |
|---|---|
| Points per upsert request | 1000 (MAX_POINTS_PER_REQUEST) |
| Any request body | 32 MiB (MAX_BODY_BYTES) |
top_k | max 1000 (MAX_TOP_K), default 10 |
| Payload filter size | 64 KiB |
| Scroll page | default 50, max 1000 |
List page_size | default 50, max 200 |
| Index name length | 48 characters |
dimensions | 1–65536, default 1536 |
replicas | 1–8 |
| Collections per direct-access token | 256 |
See platform limits for cross-service limits.
Status codes
| Code | When |
|---|---|
| 400 | Validation failure; malformed project id (project id must be a UUID); Qdrant 4xx passthrough |
| 401 | Missing or invalid credential |
| 403 | You are a member but the route needs admin |
| 404 | Not found, or not yours — deliberately identical |
| 409 | Index name taken; index not reconciled yet |
| 429 | Per-principal rate limit |
| 500 | internal error |
| 503 | Health degraded; session signing key not loaded |
Related pages
- VectorDB overview — what the service is and when to use it
- Collections and points — concepts behind this API
- Search — query patterns and filters
- CLI: data and messaging commands — the read-only
platformctl vectordbcommands - API overview — shared conventions across all platform APIs