Ingest Events (Server-Side)
POST /cdp/events — authenticated, server-to-server event ingestion. Send batches of
events with your vyg_ API key so profiles are enriched from systems other than the
storefront pixel (back-office apps, internal services, custom integrations).
Ingestion is asynchronous by default: the API validates every event at the edge,
queues the accepted ones, and returns 202 immediately. A worker forwards queued events
to the CDP with bounded concurrency — expect an accepted event to be visible on the
profile within about a minute under normal load.
For debugging and low-volume request/response integrations there is an optional
synchronous mode (?sync=true) that forwards the batch
inline and returns the CDP’s processing result per event.
Request
Section titled “Request”POST /cdp/eventsContent-Type: application/jsonAuthorization: Bearer vyg_your_api_key- Batch cap: at most 50 events per request (10 with
?sync=true). A larger batch is rejected400and nothing is enqueued. - Body cap: 256 KB. A larger body is rejected
413before it is parsed. - Scope is server-side. Every event is stamped with the shop scope resolved from your
API credential. Do not send
scope,shopDomain, or asourceobject anywhere in the batch — any such field is rejected400with the offending JSON path, even when the value matches your own shop.
Event fields
Section titled “Event fields”| Field | Type | Required | Notes |
|---|---|---|---|
eventType | string | yes | A letter followed by letters/digits/underscores (max 64 chars). |
profileId | string | yes | Max 60 chars, no : characters. |
sessionId | string | no | Max 60 chars, no : characters. Defaults to a server-minted id. |
properties | object | no | Event properties. Must not contain scope/shopDomain. |
flattenedProperties | object | no | Flattened properties. Must not contain scope/shopDomain. |
timeStamp | string | no | ISO-8601. Defaults to forward time. |
target | object | no | { itemId, itemType, properties? } — no scope. |
Unknown fields on an event are rejected (per-event) — the envelope schema is strict.
Body (example)
Section titled “Body (example)”{ "events": [ { "eventType": "orderShipped", "profileId": "shopify_your-shop_1234567890", "properties": { "orderId": "5678", "carrier": "ups" } }, { "eventType": "supportTicketClosed", "profileId": "shopify_your-shop_1234567890", "sessionId": "support-20260611-0001", "properties": { "ticketId": "T-991" } } ]}Response
Section titled “Response”202 Accepted with per-event accounting — invalid events are rejected loudly, never
silently dropped, and valid events in the same batch are still accepted:
{ "accepted": 1, "rejected": [ { "index": 1, "errors": [ { "path": "$.events[1].profileId", "message": "profileId must not contain \":\" (Unomi eventcollector rejects colons in identifiers)" } ] } ], "scope": "your-shop.myshopify.com"}accepted— number of events queued for delivery.rejected[]— one entry per invalid event: itsindexin youreventsarray and the validationerrorswith JSON paths into your request body.
Synchronous mode (?sync=true)
Section titled “Synchronous mode (?sync=true)”POST /cdp/events?sync=true forwards the validated batch inline to the CDP and
returns 200 with the CDP’s processing result per event. It is a debugging /
low-volume tool — your request latency is coupled to CDP health by design. Use the
default asynchronous mode for anything sustained or bulk.
Exactly the same validation, server-side scope forcing, and metering apply as in async mode (one shared validator) — plus three sync-specific guards:
- Tighter batch cap: at most 10 events per sync request. A larger batch is
rejected
400(sync_batch_too_large) — switch to async (omit?sync) for up to 50. - Hard 5-second forward budget: if the CDP times out, is unreachable, or answers
5xx, the API returns503(upstream_unavailable). Nothing is queued — the sync path never falls back to the async queue, so a retry cannot double-ingest from our side. Retry the batch yourself if needed (at-least-once duplicates are tolerated). - Its own throttle: sync requests are rate-limited (
429,sync_throttled) on top of the API-wide limits. Back off and retry, or use the async path.
Any other ?sync value (?sync=1, ?sync=yes, …) is rejected 400 — only true
enables synchronous mode (and false is the explicit async default), so you can never
silently fall through to async while believing you got a synchronous result.
Response (200 OK)
Section titled “Response (200 OK)”{ "accepted": 1, "rejected": [], "results": [ { "index": 0, "ok": true, "status": 200, "result": { "updated": 1, "processedItems": 1 } } ], "scope": "your-shop.myshopify.com"}results[]— one entry per forwarded event (by itsindexin youreventsarray): the CDP’s HTTPstatusand, on success, its parsed processingresult.- An event the CDP rejects permanently (
4xx— e.g. aneventTypewith no registered schema) appears asok: falsewith itsstatusand a boundederrordiagnostic. The request itself still returns200; permanent rejections are per-event and are not retried. rejected[]— same per-event edge-validation accounting as async mode.
Example
Section titled “Example”curl -s -X POST "https://cdp.vyg.app/cdp/events?sync=true" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer vyg_your_api_key" \ -d '{ "events": [ { "eventType": "orderShipped", "profileId": "shopify_your-shop_123", "properties": { "orderId": "5678" } } ] }'Example
Section titled “Example”curl -s -X POST "https://cdp.vyg.app/cdp/events" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer vyg_your_api_key" \ -d '{ "events": [ { "eventType": "orderShipped", "profileId": "shopify_your-shop_123", "properties": { "orderId": "5678" } } ] }'Errors
Section titled “Errors”| Status | error code | When |
|---|---|---|
400 | bad_request | Body is not valid JSON, has no events array, the array is empty, or ?sync is invalid. |
400 | batch_too_large | More than 50 events in one batch. |
400 | sync_batch_too_large | More than 10 events in one ?sync=true batch — use async mode for larger batches. |
400 | scope_binding | Any event carries scope, shopDomain, or source — per-event JSON paths included. |
401 | unauthorized | Missing or invalid credential. |
403 | forbidden | Authenticated, but no connected integration resolves a CDP scope for this brand. |
405 | method_not_allowed | Method other than POST / OPTIONS. |
413 | payload_too_large | Body exceeds the 256 KB cap. |
429 | sync_throttled | Sync mode’s dedicated rate limit — back off and retry, or use the async path. |
503 | service_unavailable | The ingest queue was unavailable — no events were accepted; retry the batch (async mode). |
503 | upstream_unavailable | Sync mode: the CDP timed out / was unreachable / answered 5xx. Nothing queued — retry. |
See Errors for the full envelope.
Identifier limits
Section titled “Identifier limits”profileId and sessionId are CDP keyword identifiers: at most 60 characters and no
: characters. Compose composite keys with underscores, e.g.
shopify_your-shop_1234567890.
Metering
Section titled “Metering”Each request records one usage row against your brand, the same as every other authenticated CDP API request.