Event Ingestion
The event ingestion endpoint is the primary integration point between your application and Synapse. Events trigger flows, update contacts, and drive your communication pipeline.
Endpoint
Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json |
X-WORKSPACE-ID | Yes | Your workspace identifier |
X-API-KEY | Yes | A data-scoped API key |
Request Body
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
event_name | string | Yes | The event type identifier. Use snake_case (e.g., order_completed, claim_submitted). |
external_id | string | Yes | Your application's unique user identifier. Used to match or create a contact. |
attributes | object | No | Arbitrary JSON object containing event-specific data. Defaults to {}. Accessible in NLT templates via {the Amount from the trigger event}. |
contact | object | No | Fields to upsert on the contact record. See Contact Fields. |
occurred_at | datetime | No | When the event occurred (ISO 8601, defaults to server time). Useful for historical imports. |
idempotency_key | string | No | A unique key to prevent duplicate processing (max 255 chars). See Idempotency. |
Backward compatibility: The fields user_id and contact_overrides are still accepted as deprecated aliases for external_id and contact respectively. New integrations should use external_id and contact.
Contact Fields
When provided, these fields are upserted on the contact matching external_id. If no contact exists, one is created.
| Field | Type | Description |
|---|---|---|
email | string | The contact's email address. Required for email delivery. |
first_name | string | First name. Used in NLT templates as {the user's First Name}. |
last_name | string | Last name. |
phone | string | Phone number. |
timezone | string | IANA timezone (e.g., Asia/Singapore). Used for send-time optimization. |
locale | string | Locale code (e.g., en-SG, vi-VN). Used for localized content. |
properties | object | Custom key-value pairs. Shallow-merged with existing properties. |
tags | string[] | Replaces the contact's entire tag list. Use $add_tags/$remove_tags for surgical updates. |
$add_tags | string[] | Appends tags to the existing list (duplicates ignored). |
$remove_tags | string[] | Removes specified tags from the existing list. |
Always include email in contact when sending events for new users. Without an email address, Synapse cannot deliver emails even if a flow matches.
Response
Success (202 Accepted)
The event has been accepted and queued for asynchronous processing. Flow matching, trip creation, and email delivery happen in the background.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | validation_error | Missing or invalid required fields |
401 | invalid_api_key | API key is missing, revoked, or invalid |
403 | insufficient_scope | API key does not have data scope |
403 | plan_limit_reached | Monthly events quota exceeded. Upgrade your plan or wait for the next billing cycle. |
409 | duplicate_event | An event with this idempotency_key was already processed |
429 | rate_limit_exceeded | Too many requests -- see Rate Limits |
Examples
curl
Python
JavaScript
Batch Ingestion
For high-volume use cases or SDK batch uploads, use the batch endpoint to send up to 50 events in a single request.
The batch endpoint is partial-accept: each event is validated and ingested independently, so one bad event no longer fails the whole batch. Valid events land even when a sibling event is malformed or over the plan limit. Only accepted events count against your monthly events limit; the whole batch counts as 1 request against the rate limit. The source field for batch events is set to sdk.
Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json |
X-WORKSPACE-ID | Yes | Your workspace identifier |
X-API-KEY | Yes | A data-scoped API key |
X-Synapse-SDK-Version | No | Send 2 to receive the structured, per-event partial-accept response body. Omit it for the legacy response shape. The Browser SDK sends this automatically from version 0.5.0. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
events | array | Yes | Array of event objects (1-50 items). Each event follows the same schema as the single event endpoint. |
A batch that is malformed as a whole — missing the events key, events is not an array, or more than 50 items — is rejected with 422 at the request boundary before any event is processed.
Response (202 Accepted) — with X-Synapse-SDK-Version: 2
Returned when at least one event was accepted. rejected is a list of the events that did not land, each tagged with the original 0-based index in your events array and a machine-readable reason.
| Field | Type | Description |
|---|---|---|
accepted | integer | Number of events accepted and queued for processing |
rejected | array | One entry per event that was not accepted (empty array when all accepted) |
rejected[].index | integer | 0-based position of the event in the submitted events array |
rejected[].reason | string | invalid_schema (validation failed — never retry), over_limit (valid but over quota — retry later), or runtime_error (transient server error — safe to retry) |
rejected[].detail | string | null | Human-readable detail, or null |
Status matrix
| Outcome | Status |
|---|---|
| At least one event accepted | 202 Accepted |
Zero accepted, all rejections are invalid_schema | 422 Unprocessable Content |
Zero accepted, any rejection is over_limit | 403 plan_limit_reached |
Legacy clients (no version header)
Clients that omit X-Synapse-SDK-Version receive the original response shape, where rejected is a count — byte-identical to the pre-partial-accept response:
Plan-limit behavior differs for legacy clients. A legacy client whose batch would partially exceed the monthly events quota receives a 403 plan_limit_reached for the whole batch — rather than a 202 that silently drops the over-limit events — so an older client that re-queues on 4xx retries the whole batch later instead of losing data. Send X-Synapse-SDK-Version: 2 to instead get a 202 with the accepted events plus over_limit entries you re-queue yourself.
Processing Pipeline
After an event is accepted, Synapse processes it through the following pipeline:
- Validate schema -- Verify required fields and attribute types
- Upsert contact -- Create or update the contact matching
external_id - Store event -- Persist the event to PostgreSQL
- Check idempotency -- Skip if this
idempotency_keywas already processed - Publish to RabbitMQ -- Route to the flow trigger queue for async processing
- Flow matching -- Match against all active flows with matching
trigger_event - Segment evaluation -- If the flow has a target segment, verify the contact qualifies
- Trip creation -- Create a flow trip and execute the flow's steps
Event processing is asynchronous. A 202 Accepted response does not mean the event has been fully processed -- it means it has been durably queued. Use webhooks to track downstream delivery status.