Blue-Green Replace
Zero-downtime data replacement using staged generations and atomic swaps.
Blue-Green Replace
Replace all data in a vertical with zero downtime. Upload a new dataset into a staging generation, verify it, then atomically swap it into production. The old data stays searchable until the swap completes.
How it works
- Upload —
POST /portal/verticals/{name}/replacecreates a staging generation and ingests your file into it. A separate Qdrant collection is created for the staging data. - Monitor —
GET /portal/verticals/{name}/replace-statustracks embedding progress. All entities must reachindexedstatus before swapping. - Swap —
POST /portal/verticals/{name}/swapatomically promotes the staging generation. Both Postgres metadata and Qdrant aliases switch in one operation. - Cancel (optional) —
POST /portal/verticals/{name}/cancel-replacearchives the staging generation and drops its Qdrant collection.
Only one replace operation can be in progress per vertical at a time.
POST /portal/verticals/{name}/replace
Start a blue-green replace by uploading a file. Accepts the same file formats as regular file upload: CSV, JSON, NDJSON, XML, and PDF.
curl -X POST "https://api.qanatix.com/api/v1/portal/verticals/manufacturing/replace?entity_type=product" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-F "file=@new-catalog.csv"Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
entity_type | string | yes | Entity type for the ingested records |
source_name | string | no | Source label attached to each entity |
record_tag | string | no | XML record tag name (required for XML files) |
Quota check
Before ingestion begins, the endpoint checks that the replacement will not exceed your plan's entity limit. The check uses the post-swap count: entities in other verticals plus the new records being uploaded.
other_entities + new_records <= entity_limitIf the check fails, the upload is rejected with a 402 error before any data is written.
Response (200)
{
"ingestion_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing",
"summary": {
"submitted": 2500,
"accepted": 2500,
"rejected": 0,
"dedup_skipped": 0
},
"errors": [],
"metadata": {
"processing_time_ms": 4500,
"file_hash": "sha256:abc...",
"reconciliation_passed": true,
"pipeline_backpressure": false
},
"staging": {
"generation": 2,
"status": "staging",
"vertical": "manufacturing"
}
}The response combines the standard ingestion result fields with a staging object containing the new generation number and status.
Errors
| Status | Meaning |
|---|---|
402 | New dataset would exceed your plan's entity limit |
409 | A replace operation is already in progress for this vertical |
413 | Uploaded file exceeds the maximum size (50 MB) |
GET /portal/verticals/{name}/replace-status
Check the progress of a pending replace operation.
curl https://api.qanatix.com/api/v1/portal/verticals/manufacturing/replace-status \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"has_pending_replace": true,
"staging_generation": 2,
"staging_status": "staging",
"total": 2500,
"indexed": 2100,
"pending": 350,
"processing": 50,
"failed": 0,
"ready_to_swap": false
}Response fields
| Field | Type | Description |
|---|---|---|
has_pending_replace | boolean | Whether a staging generation exists |
staging_generation | integer | Generation number of the staging data |
staging_status | string | Always "staging" while in progress |
total | integer | Total entities in the staging generation |
indexed | integer | Entities with completed embeddings |
pending | integer | Entities waiting for embedding |
processing | integer | Entities currently being embedded |
failed | integer | Entities that failed embedding |
ready_to_swap | boolean | true when all entities are indexed (no pending, processing, or failed) |
When has_pending_replace is false, all other fields are null.
POST /portal/verticals/{name}/swap
Promote the staging generation to production. This performs two atomic operations:
- Postgres swap — staging entities become the active generation; old entities are archived.
- Qdrant alias swap — the vertical's search alias points to the new collection.
Both operations succeed or fail together. Search traffic sees no interruption.
curl -X POST https://api.qanatix.com/api/v1/portal/verticals/manufacturing/swap \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Preconditions
All staging entities must have embedding_status = 'indexed'. Check ready_to_swap in the replace-status response before calling this endpoint.
Response (200)
{
"vertical": "manufacturing",
"old_generation": 1,
"new_generation": 2,
"old_entity_count": 2000,
"new_entity_count": 2500
}Response fields
| Field | Type | Description |
|---|---|---|
vertical | string | Vertical name |
old_generation | integer | Previous production generation (now archived) |
new_generation | integer | New active generation |
old_entity_count | integer | Number of entities in the old generation |
new_entity_count | integer | Number of entities now in production |
Errors
| Status | Meaning |
|---|---|
409 | No staging generation exists, or staging entities are not fully indexed |
POST /portal/verticals/{name}/cancel-replace
Cancel a pending replace operation. Archives all staging entities and drops the staging Qdrant collection. The current production data is unaffected.
curl -X POST https://api.qanatix.com/api/v1/portal/verticals/manufacturing/cancel-replace \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response (200)
{
"vertical": "manufacturing",
"cancelled_generation": 2,
"entities_archived": 2500
}Response fields
| Field | Type | Description |
|---|---|---|
vertical | string | Vertical name |
cancelled_generation | integer | Generation number that was cancelled |
entities_archived | integer | Number of staging entities archived |
Errors
| Status | Meaning |
|---|---|
409 | No staging generation exists to cancel |
Complete example
A typical replace workflow for refreshing a product catalog:
# 1. Upload new dataset
curl -X POST "https://api.qanatix.com/api/v1/portal/verticals/manufacturing/replace?entity_type=product&source_name=catalog-q2" \
-H "Authorization: Bearer $JWT" \
-F "file=@catalog-q2-2026.csv"
# 2. Poll until ready
while true; do
STATUS=$(curl -s https://api.qanatix.com/api/v1/portal/verticals/manufacturing/replace-status \
-H "Authorization: Bearer $JWT")
READY=$(echo $STATUS | jq -r '.ready_to_swap')
if [ "$READY" = "true" ]; then break; fi
echo "Waiting... $(echo $STATUS | jq -r '.indexed')/$(echo $STATUS | jq -r '.total') indexed"
sleep 5
done
# 3. Swap into production
curl -X POST https://api.qanatix.com/api/v1/portal/verticals/manufacturing/swap \
-H "Authorization: Bearer $JWT"Error responses
All blue-green replace endpoints follow the standard error format:
{
"detail": "Replace already in progress for vertical 'manufacturing'"
}| Status | Meaning |
|---|---|
401 | Missing or invalid JWT token |
402 | Entity quota exceeded |
404 | Vertical not found |
409 | Conflict (replace in progress, not ready, or no staging) |
413 | File too large |
422 | Validation error (missing entity_type, invalid file format) |