| description | Every route of the conserver REST API, with authentication, parameters, status codes and the Redis behavior behind each. |
|---|
The conserver API is a FastAPI application. It stores and fetches vCons, feeds and drains chains, reads and writes the configuration, and manages the dead letter queues.
Every route, including the system routes, sits under API_ROOT_PATH (default /api). With the compose file, the API listens on port 8000, so the health route is http://localhost:8000/api/health.
Every response carries X-Vcon-Server-Version and X-Vcon-Server-Commit, taken from the image build arguments. Both read dev or unknown on an image built without them. CORS is open to all origins.
Two separate models apply, and a key from one is not accepted by the other.
| Routes | Credential |
|---|---|
| All routes below except those named next | The main token in the x-conserver-api-token header, or the header named by CONSERVER_HEADER_NAME. |
GET /version, GET /health, GET /stats/queue |
None. |
POST /vcon/external-ingress |
A key from ingress_auth in config.yml, scoped to one ingress list. |
Set CONSERVER_API_TOKEN, or CONSERVER_API_TOKEN_FILE with one token per line. With neither set, the main API accepts every request. A wrong or missing token returns 403 with {"detail": "Invalid API Key"}.
curl -H "x-conserver-api-token: $TOKEN" "http://localhost:8000/api/vcon"| Code | Meaning |
|---|---|
400 |
GET /vcons/search called with no search parameter. |
403 |
Bad or missing key. External ingress adds the reason to detail. |
404 |
Unknown vCon, or an unknown storage name on a storage DLQ replay. |
409 |
A storage DLQ replay is already running, or a stale replay lock needs recovery. |
422 |
The request body or a query parameter failed validation. FastAPI returns a detail list that names the failing field. |
500 |
A Redis or storage error. detail is a short fixed message. The cause is in the server log. |
{ "detail": "vCon not found" }POST /vcon and POST /vcon/external-ingress parse the body with a model that requires vcon (string), uuid and created_at, and accepts extra fields. It returns 422 when:
dialog[].urlis not empty and does not look like a URL (scheme://...).dialog[].durationis negative.dialog[].mimetypeis present and not a valid media type, ordialog[].algis not a known algorithm.parties[].telis not empty and does not look like a phone number.- a
dialog[].partiesindex is outside thepartiesarray.
The model fills redacted, group and appended with empty defaults, so the copy the API stores in Redis can carry them. The first link that stores the vCon drops an empty group and redacted and stamps vcon as 0.4.0.
The conserver does not require or check a lawful basis. A vCon you send should carry an attachment with purpose: "lawful_basis", as in Quick Start. See the Lawful Basis extension.
GET /version
{ "version": "2026.05.18", "git_commit": "5bc6b6e", "build_time": "2026-05-18T10:00:00Z" }GET /health
Returns 200 with {"status": "healthy", "version": {...}}. It does not touch Redis, so it shows the API process is up, not that Redis is reachable.
GET /stats/queue?list_name=<redis-list>
Returns {"list_name": "incoming_calls", "depth": 127} for any Redis list name. It needs no token, so use it for autoscalers and dashboards, and keep the route off the public internet.
POST /vcon?ingress_lists=<list>&ingress_lists=<list>
Stores the body in Redis with a TTL of VCON_REDIS_EXPIRY seconds, adds it to the vcons sorted set and indexes its parties. If you pass ingress_lists, it also pushes the UUID onto each list, with the caller's trace context stored first so the worker can link its spans. It returns 201 and the stored vCon. The body is not written to any storage until a chain does it.
curl -X POST "http://localhost:8000/api/vcon?ingress_lists=main_chain" \
-H "x-conserver-api-token: $TOKEN" -H "Content-Type: application/json" \
-d @vcon.jsonGET /vcon/{vcon_uuid}
Reads vcon:{uuid} from Redis. On a miss it asks each configured storage in turn, restores the first hit into Redis with VCON_REDIS_EXPIRY, and adds it to the sorted set. 404 if no storage has it. If EGRESS_FORMAT_VERSION is set, the response uses that legacy shape.
GET /vcons?vcon_uuids=<uuid>&vcon_uuids=<uuid>
Returns a JSON array in request order. A UUID that is in neither Redis nor storage comes back as null in its position.
GET /vcon?page=1&size=50&since=<datetime>&until=<datetime>
Returns UUIDs from the sorted set, newest first by created_at. since and until filter on that timestamp.
GET /vcons/search?tel=<number>&mailto=<address>&name=<name>
Returns matching UUIDs. At least one parameter is required, or the route returns 400. Matches are exact. The index is written when a vCon arrives through POST /vcon or POST /vcon/external-ingress, and its keys expire after VCON_INDEX_EXPIRY. Rebuild it with GET /index_vcons. When you pass several parameters the route intersects the sets that matched, and a parameter with no match is ignored instead of emptying the result.
DELETE /vcon/{vcon_uuid}
Deletes vcon:{uuid} from Redis, then calls delete on every storage in config.yml. It always returns 204, including when a delete failed, and logs each failure. The storages that implement delete are s3, postgres, file, elasticsearch, vcon_mcp and utopia. The others are skipped with a warning. The call leaves the sorted set entry and the search index keys in place.
POST /vcon/ingress?ingress_list=<list>
Body: a JSON array of UUIDs. Each vCon must exist in Redis or in a storage. The route restores a stored one into Redis, skips a missing one with a warning, and pushes the rest. It returns 204 with no body, whether or not it skipped any. This route takes only the main token. A partner key does not work here.
curl -X POST "http://localhost:8000/api/vcon/ingress?ingress_list=main_chain" \
-H "x-conserver-api-token: $TOKEN" -H "Content-Type: application/json" \
-d '["550e8400-e29b-41d4-a716-446655440000"]'POST /vcon/external-ingress?ingress_list=<list>
Body: one full vCon. This is the route for a partner system, and it is the only route that does not take the main token. The partner sends a key from ingress_auth in the same header:
ingress_auth:
partner_data:
- "partner-key-1"
- "partner-key-2"
customer_data: "single-key"The route stores, indexes and enqueues the vCon exactly as POST /vcon does, onto the one list named in the query, and returns 204. The key opens only that list. The conserver reads ingress_auth from the file on each call. A 403 carries one of these reasons in detail: API Key required, No ingress authentication configured, Ingress list '<name>' not configured, Invalid API Key for ingress list '<name>'.
curl -X POST "http://localhost:8000/api/vcon/external-ingress?ingress_list=partner_data" \
-H "x-conserver-api-token: partner-key-1" -H "Content-Type: application/json" \
-d @vcon.jsonGET /vcon/egress?egress_list=<list>&limit=1
Removes up to limit UUIDs from the list and returns them as a JSON array with status 200. Despite being a GET, it changes state: a UUID you read is gone from the list. It pops from the tail, which is where the conserver appends, so with a small limit you get the newest UUIDs first. Fetch each vCon with GET /vcon/{uuid}.
GET /vcon/count?egress_list=<list>
Returns the list length as a bare number.
Two kinds exist. The ingress DLQ DLQ:<ingress_list> holds a UUID whose chain raised, and replaying it runs the whole chain again. The storage DLQ DLQ:storage:<storage_name> holds a UUID whose write to one storage raised, and replaying it retries only that write. Concepts has the full rules.
GET /dlq?ingress_list=<list>
GET /dlq/storage?storage_name=<name>
Each returns every UUID in the queue, oldest first.
POST /dlq/reprocess?ingress_list=<list>&count=1000
Moves up to count UUIDs, oldest first, from DLQ:<list> back onto the ingress list. count is 1 to 100000 and defaults to 1000, so a single call stays short on a large queue. It returns the number moved. Call it again until it returns 0.
POST /dlq/storage/reprocess?storage_name=<name>&count=1000
Calls save on the named storage for up to count UUIDs from DLQ:storage:<name>, and returns how many succeeded. A UUID leaves the queue only after its write succeeds. The route stops at the first failure, so a storage that is still down does not drain the queue. A second call while one runs returns 409. An unknown storage name returns 404.
Replay is at least once: a crash between the write and the removal repeats the write, so the storage has to accept a second save of the same UUID. The replay lock, DLQ:storage:<name>:replay-lock, has no expiry. If the API crashes mid-replay, delete that key in Redis to recover.
A vCon in either queue has its Redis TTL raised to VCON_DLQ_EXPIRY so the body survives until replay.
GET /config
Returns the parsed config.yml as JSON. It includes every key and password in the file.
POST /config
Body: the whole configuration as JSON. The route writes it to the path in the CONSERVER_CONFIG_FILE environment variable with yaml.dump, which drops comments and formatting, and returns 204. It returns 500 if the variable is unset or the file is read-only. Workers pick the new file up on their next vCon. The conserver does not validate the content.
GET /index_vcons
Scans every vcon:* key, rebuilds the party index for each, and returns the count. The scan uses KEYS, which blocks Redis while it runs, so avoid it on a large instance.