Skip to main content
Version: v1

Endpoints

Seven endpoints under /api/v1/ingest. One is unsigned (pairing), one is signed by the single-use upload URL rather than HMAC (upload), and the rest are HMAC-signed per Authentication & HMAC.

EndpointMethodAuthIdempotency
/pairPOSTNone (TLS only)Pair code is single-use
/initPOSTHMAC—
/upload/{sync_id}PUTSingle-use URLOverwrites by key
/finalizePOSTHMACIdempotency-Key (required for full snapshots)
/eventsPOSTHMACbatch_id + optional Idempotency-Key
/statusGETHMACAlways safe
/unpairPOSTHMACIdempotent

Full sync — three steps​

A snapshot is init → upload → finalize.

POST /init​

Opens a sync session and returns an upload target. sync_type is "full" (the snapshot path) or "partial"; expected_counts is a hint for dashboard progress, not validation.

Request
{ "sync_type": "full", "expected_counts": { "product": 245, "category": 18, "page": 32 } }
Response
{
"sync_id": "a7b1c4e2-19f0-4d6f-87aa-c00ffee12345",
"upload_url": "https://api.monoverse.tech/api/v1/ingest/upload/a7b1c4e2-...",
"object_key": "ingest/03d7d7e6-.../a7b1c4e2-....ndjson.gz",
"format": "ndjson_gzip",
"expires_at": "2026-05-16T11:00:00Z",
"max_size_bytes": 209715200
}

If a snapshot is already uploading or dispatching, init returns 409 conflict_full_sync_in_progress.

PUT /upload/{sync_id}​

Stream the NDJSON body (one entity per line), optionally Content-Encoding: gzip, to the returned upload_url. No HMAC here — the URL is single-use and ingest-scoped (in production it's an S3 signed PUT). Max body is max_size_bytes (200 MB); over that is 413. Success is 204 No Content.

POST /finalize​

Closes the session and schedules processing. Idempotency-Key is required for full-snapshot finalize — a retry with the same key returns the cached response and does no extra work.

Request → Response
{ "sync_id": "a7b1c4e2-..." }
// →
{ "sync_id": "a7b1c4e2-...", "status": "dispatching", "dispatch_eta_seconds": 60 }

After finalize the backend parses each NDJSON line, validates it, and writes the entities and their projections. Per-line failures are dead-lettered, not fatal.

Incremental — POST /events​

Small signed batches of upserts and deletes, 1–500 operations per batch. The server sets no body-size cap; the official SDKs split batches at 5 MB. batch_id is your dedup id — see Idempotency for exactly-once for what a resend returns.

Request
{
"batch_id": "8e3a1b9c-...",
"events": [
{
"type": "upsert",
"entity_kind": "product",
"external_id": "wc:product:iphone-15-pro-256gb",
"lang": "uk_UA",
"payload": {
"sku": "APL-IP15P-256",
"name": "Apple iPhone 15 Pro 256GB",
"slug": "iphone-15-pro-256gb",
"product_type": "variable",
"price_amount": 52999,
"regular_price_amount": 59999,
"currency": "UAH",
"description": "Флагман Apple з чіпом A18 Pro та титановим корпусом.",
"stock_status": "instock",
"stock_quantity": 14,
"permalink": "https://shop.example.com/product/iphone-15-pro-256gb/",
"categories": ["smartphones", "apple"],
"tags": ["new", "premium", "5g"],
"attributes": [
{ "name": "Колір", "slug": "pa_color", "options": ["Blue Titanium", "White Titanium"], "variation": true }
],
"variations": [
{ "external_id": "wc:variation:12345", "sku": "APL-IP15P-256-BLU",
"price_amount": 52999, "stock_status": "instock", "attributes": { "pa_color": "Blue Titanium" } }
],
"images": [
"https://cdn.example.com/iphone-15-pro-blue-1.jpg",
"https://cdn.example.com/iphone-15-pro-blue-2.jpg"
]
}
},
{ "type": "delete", "entity_kind": "product", "external_id": "wc:product:obsolete" }
]
}
Response
{
"processed": 1,
"errors": [],
"idempotent_replay": false
}

The upsert lowers the price of the product shown in Entities and sends that product whole. How the server applies a batch:

  • An upsert replaces the whole entity. A field you leave out does not keep its old value, so a change to one field still sends every field, lang and translation_of included — see An upsert replaces the whole entity.
  • Operations apply in the order sent. An upsert then a delete of the same external_id leaves it deleted; a delete then an upsert leaves it live.
  • processed counts the upserts applied. Deletes are not counted.
  • errors[].index is the operation's position in events[], counting from 0 and counting deletes.
  • A delete needs external_id. A delete without external_id, or with an empty entity_kind, is not applied; it comes back in errors like this, for an operation at position 2: { "index": 2, "entity_kind": "product", "external_id": null, "error": "delete requires entity_kind and external_id" }.
  • To hide a product that exists but is not for sale — a draft, for example — send a delete for it. A later upsert of the same external_id brings it back.
  • site_clock and deleted_at are ignored. The server accepts both fields and reads neither.

Each operation's payload follows the same per-kind shape as a snapshot line — see Entities.

GET /status​

Read-only heartbeat. Returns the connection status, the last sync session summary, and per-kind entity counts. The producer may also send X-VoiceBot-Site-Url, X-VoiceBot-WP-Version, X-VoiceBot-WC-Version, X-VoiceBot-PHP-Version so the dashboard reflects the live site. Throttle to at most once every ~5 minutes.

POST /unpair​

Disconnects the provider connection and invalidates the secret. Idempotent — a second call returns affected: 0. Run it before re-pairing a different tenant.

{ "ok": true, "affected": 1 }

Rate limits​

ScopeLimit
/pair10 / min per IP
init + upload + finalize (combined)6 / min per tenant
/events60 / min per tenant
Full snapshots4 / hour per tenant

A 429 returns Retry-After. Note that signed 429s are not safe to blind-retry — see Errors.

Next​

  • Entities — what goes in each NDJSON line and /events payload.