Skip to main content

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, so it has its own base URL.

Where it lives

On the public API, https://api.codyhill.dev, which serves the VectorDB routes alongside every other service. platformctl goes there by default; curl needs the variable set.

export CAI_VECTORDB_API=https://api.codyhill.dev

Every path on this page is relative to that base URL.

Authentication

Every route except GET /healthz requires a bearer credential:

Authorization: Bearer <token-or-api-key>

The credential is either a session token (from POST /v1/auth/login on the core API, 12-hour life) or a service-account or personal API key (prefix cai_). 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:

RoleCan call
memberlist, get, create index; :upsert, :query, :scroll, :delete-points
admineverything 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; limit is an alias) and page_token (opaque). Responses carry next_page_token, absent on the last page. Out-of-range sizes are rejected with 400 page_size must be between 1 and 200, never clamped. A token from a different list or project returns 400 page_token is invalid or was issued for a different list; start from the first page.
  • Custom methods. Data operations use GCP-style :verb suffixes 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

MethodPathAuthPurpose
GET/v1/projects/{projectID}/indexesmemberList indexes
POST/v1/projects/{projectID}/indexesmemberCreate an index
GET/v1/projects/{projectID}/indexes/{name}memberGet one index
PATCH/v1/projects/{projectID}/indexes/{name}adminEdit mutable fields
DELETE/v1/projects/{projectID}/indexes/{name}adminDelete an index
POST/v1/projects/{projectID}/indexes/{name}:upsertmemberWrite points
POST/v1/projects/{projectID}/indexes/{name}:querymemberSimilarity search
POST/v1/projects/{projectID}/indexes/{name}:scrollmemberBrowse points in id order
POST/v1/projects/{projectID}/indexes/{name}:delete-pointsmemberDelete points by id or filter
GET/v1/projects/{projectID}/vectordb/credentialsadminDirect-access credential
GET/healthznoneHealth 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"
}
FieldTypeMeaning
namestringThe index name you chose
resource_pathstringStable path: projects/<short>/indexes/<name>
collectionstringThe internal storage name (p_<short>_<name>). Reported so you can find your data; never accepted as input anywhere
dimensionsintVector width. Immutable
distancestringcosine, dot, or euclid. Immutable
on_diskboolVectors stored on disk instead of memory. Immutable
payload_on_diskboolPayloads stored on disk. Editable via PATCH
quantizationobjectkind (none/scalar/binary), quantile, always_ram. Immutable
shardsintShard count. Immutable
replicasintReplica count (1–8). Editable via PATCH
statestringpending, creating, ready, degraded, conflict, or blocked
readyboolThe authoritative "you can use it now" signal
collection_statusstringThe storage layer's own health word (green/yellow/red/grey); present only once the collection exists
points_count, vectors_count, segments_countintCounts from the vector store, refreshed about every 5 minutes — not live, see below
points_count lags your writes by up to 5 minutes

Upsert 1,000 points and read the index straight back, and points_count will very likely still say 0. Nothing is wrong: these counts are collected from the vector store on a periodic sweep (about every 5 minutes), not measured when you ask.

Your points are queryable immediately — the upsert is synchronous, and a :query right after it returns them. Only the count is behind.

So: to check an upsert worked, query for the points, do not watch the counter. Use the counter for "roughly how big is this index", which is the question it can actually answer.

# Right: proves the write landed, immediately
curl -sS -X POST "$CAI_VECTORDB_API/v1/projects/$PROJECT/indexes/docs:query" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'Content-Type: application/json' \
-d '{"vector": [0.1, 0.2, 0.3, 0.4], "top_k": 5}'

# Misleading: may read 0 for several minutes after a successful upsert
curl -sS "$CAI_VECTORDB_API/v1/projects/$PROJECT/indexes/docs" \
-H "Authorization: Bearer $CAI_TOKEN" | jq .index.points_count

| 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:

NameTypeRequiredDefaultNotes
page_sizeintno501–200; limit is an alias
page_tokenstringnoOpaque 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
}
FieldTypeRequiredDefaultRules
namestringyesLowercase letters, digits, hyphens; must start and end alphanumeric; max 48 chars
dimensionsintnothe platform embedding model's width1–65536; omitted or 0 sizes the index for the platform's own model. Immutable after create - see the note below
distancestringnocosineCase-insensitive: cosine; dot (aliases dotproduct, dot_product); euclid (aliases euclidean, l2)
on_diskboolnofalseImmutable
payload_on_diskboolnofalseEditable later
quantization.kindstringnononenone, scalar (alias int8), or binary. Immutable
quantization.quantilestringnoScalar quantization quantile
quantization.always_ramboolnofalseKeep quantized vectors in memory
shardsintno1Immutable
replicasintno11–8; editable later
Width is permanent

dimensions is fixed when the index is created. Existing vectors cannot be re-embedded into a different width, so a mismatch is repaired only by deleting the index and building it again.

Omitting dimensions sizes the index for the platform's own hosted embedding model, qwen-embedding - the one served at POST /v1/embeddings and injected as EMBED_MODEL. Embed with that and write the result and the two match with nothing set. Name a width explicitly only when your vectors come from a different model.

{"name": "docs-rag-index", "dimensions": 4096}

Better than trusting either number: read the width off the model you will actually embed with. A workload is injected with EMBED_BASE_URL and EMBED_MODEL, and one embedding call reports its own length.

Errors:

StatusMessage
400dimensions must be between 1 and 65536, or omit it for the platform default

That default is the width of the platform's own embedding model, so a create that omits it fits vectors from POST /v1/embeddings - see the note above.

| 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}
FieldTypeRules
replicasint1–8. 400 replicas must be between 1 and 8
payload_on_diskbool

Sending an immutable field returns a 400 that names it:

Field sentMessage
dimensionsdimensions is immutable: existing points cannot be re-embedded, so a different width is a new index, not an edit
distancedistance is immutable: the metric is baked into the graph built over the existing points
shardsshards is immutable: resharding moves data and is not performed on a spec edit
on_diskon_disk is immutable: vectors are not restaged between memory and disk in place
quantizationquantization 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.

No undo

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 :verb at all returns 400 expected 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]}
]}
FieldTypeRequiredRules
pointsarrayyes1–1000 points per request
points[].iduint or UUID stringnoOmitted means a UUID is generated
points[].vectorarray of numbersyesWidth must equal the index's dimensions
points[].payloadobjectnoArbitrary 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:

StatusMessage
400points must contain at least one point
400at most 1000 points per request (got N)
400point 3 has 1536 dimensions; index "docs" expects 256
400point 0: id must be an unsigned integer or a UUID string
400a string id must be a UUID (or use an unsigned integer)
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
}
FieldTypeRequiredDefaultRules
vectorarray of numbersyesWidth must equal the index's dimensions
top_kintno10Max 1000
filterobjectnoA payload filter (syntax); must be a JSON object; max 64 KiB
with_payloadboolnotrueInclude each hit's payload
with_vectorboolnofalseInclude each hit's vector
score_thresholdnumbernoDrop hits scoring below this
offsetintno0Must 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:

StatusMessage
400top_k must be at most 1000
400filter must be a JSON object
400filter is too large
400offset must not be negative
400query vector has N dimensions; index "docs" expects M
400vector database rejected the request: <message> (storage-side rejections passed through verbatim)

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}
FieldTypeRequiredDefaultRules
limitintno50Max 1000
offsetuint or UUID stringnoThe 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"}}]}}
FieldTypeRules
idsarrayMax 1000 ids; each an unsigned integer or UUID string
filterobjectA payload filter (syntax); an empty object is refused

Response 200 (ids_requested is omitted for filter deletes):

{"index": "docs", "status": "ok", "ids_requested": 2}

Errors:

StatusMessage
400provide exactly one of ids or filter
400at most 1000 ids per request
400an empty filter would delete every point; delete the index instead if that is the intent

Direct-access credentials

GET /v1/projects/{projectID}/vectordb/credentials

Auth: admin. Returns a credential for talking to the underlying vector store directly — an escape hatch for clients that need its native API rather than the REST surface above. The credential is scoped to your project's collections only.

Response 200:

{
"service_url": "http://<private-hostname>:6333",
"external_service_url": "https://<endpoint>-<short>.apps.<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..."
}
FieldMeaning
service_urlThe store's platform-internal address. Reachable only from workloads running on the platform, never from your laptop
external_service_urlThe address of your own published endpoint for the vector database, over TLS. Present only when you have published one and a token was issued — nothing is reachable from the internet until you publish it. When present it is reachable from the internet, with the project token as the only boundary; add an address allow list to the endpoint to narrow that
tokenA JWT you present as the api-key header (or as a Bearer token). Read-write on the listed collections; cannot create collections
collection_prefixYour project's collection name prefix
collectionsThe exact collection names the token covers, snapshotted at mint time
truncatedtrue when more than 256 collections exist; only 256 are enumerated per token
notePlain-English explanation of exactly which of the above applies to this response
Token availability and scope

The token is minted when the platform enforces per-project access control on the store. Without it you get the addresses and collection names. The token's scope is a snapshot of exact collection names: an index created later needs a fresh fetch. The store's own master key is never returned.

Health check

GET /healthz

No auth. Response 200:

{"status": "ok", "dependencies": {
"vector_store": {"ok": true, "latency_ms": 3},
"orchestration": {"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 by the server. The values below are the defaults; your administrator can tune a few of them, so treat these as the starting point rather than a promise.

LimitValue
Points per upsert request1000
Any request body32 MiB
top_kmax 1000, default 10
Payload filter size64 KiB
Scroll pagedefault 50, max 1000
List page_sizedefault 50, max 200
Index name length48 characters
dimensions1–65536, defaults to the platform embedding model's width
replicas1–8
Collections per direct-access token256

See platform limits for cross-service limits.

Status codes

CodeWhen
400Validation failure; malformed project id (project id must be a UUID); storage-side 4xx passthrough
401Missing or invalid credential
403You are a member but the route needs admin
404Not found, or not yours — deliberately identical
409Index name taken; index not reconciled yet
429Per-principal rate limit
500internal error
503Health degraded; session signing key not loaded