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.
| Endpoint | Method | Auth | Idempotency |
|---|---|---|---|
/pair | POST | None (TLS only) | Pair code is single-use |
/init | POST | HMAC | — |
/upload/{sync_id} | PUT | Single-use URL | Overwrites by key |
/finalize | POST | HMAC | Idempotency-Key (required for full snapshots) |
/events | POST | HMAC | batch_id + optional Idempotency-Key |
/status | GET | HMAC | Always safe |
/unpair | POST | HMAC | Idempotent |
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.
{ "sync_type": "full", "expected_counts": { "product": 245, "category": 18, "page": 32 } }
{
"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.
{ "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; body cap 5 MB. batch_id is
your dedup id; replaying a seen batch_id returns idempotent_replay: true with processed: 0.
{
"batch_id": "8e3a1b9c-...",
"site_clock": "2026-05-16T10:15:33Z",
"events": [
{ "type": "upsert", "entity_kind": "product", "external_id": "wc:product:iphone-15-pro",
"payload": { "name": "iPhone 15 Pro", "price_amount": 52999, "stock_status": "instock" } },
{ "type": "delete", "entity_kind": "product", "external_id": "wc:product:obsolete",
"deleted_at": "2026-05-16T10:15:30Z" }
]
}
{
"processed": 1,
"errors": [ { "index": 1, "entity_kind": "product", "external_id": "wc:product:obsolete", "error": "..." } ],
"idempotent_replay": false
}
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
| Scope | Limit |
|---|---|
/pair | 10 / min per IP |
init + upload + finalize (combined) | 6 / min per tenant |
/events | 60 / min per tenant |
| Full snapshots | 4 / 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
/eventspayload.