QANATIX
API Reference

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

  1. UploadPOST /portal/verticals/{name}/replace creates a staging generation and ingests your file into it. A separate Qdrant collection is created for the staging data.
  2. MonitorGET /portal/verticals/{name}/replace-status tracks embedding progress. All entities must reach indexed status before swapping.
  3. SwapPOST /portal/verticals/{name}/swap atomically promotes the staging generation. Both Postgres metadata and Qdrant aliases switch in one operation.
  4. Cancel (optional) — POST /portal/verticals/{name}/cancel-replace archives 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

ParameterTypeRequiredDescription
entity_typestringyesEntity type for the ingested records
source_namestringnoSource label attached to each entity
record_tagstringnoXML 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_limit

If 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

StatusMeaning
402New dataset would exceed your plan's entity limit
409A replace operation is already in progress for this vertical
413Uploaded 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

FieldTypeDescription
has_pending_replacebooleanWhether a staging generation exists
staging_generationintegerGeneration number of the staging data
staging_statusstringAlways "staging" while in progress
totalintegerTotal entities in the staging generation
indexedintegerEntities with completed embeddings
pendingintegerEntities waiting for embedding
processingintegerEntities currently being embedded
failedintegerEntities that failed embedding
ready_to_swapbooleantrue 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:

  1. Postgres swap — staging entities become the active generation; old entities are archived.
  2. 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

FieldTypeDescription
verticalstringVertical name
old_generationintegerPrevious production generation (now archived)
new_generationintegerNew active generation
old_entity_countintegerNumber of entities in the old generation
new_entity_countintegerNumber of entities now in production

Errors

StatusMeaning
409No 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

FieldTypeDescription
verticalstringVertical name
cancelled_generationintegerGeneration number that was cancelled
entities_archivedintegerNumber of staging entities archived

Errors

StatusMeaning
409No 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'"
}
StatusMeaning
401Missing or invalid JWT token
402Entity quota exceeded
404Vertical not found
409Conflict (replace in progress, not ready, or no staging)
413File too large
422Validation error (missing entity_type, invalid file format)

On this page