Skip to content

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

POST https://synapse-api.pyrx.tech/v1/events

Headers

HeaderRequiredDescription
Content-TypeYesMust be application/json
X-WORKSPACE-IDYesYour workspace identifier
X-API-KEYYesA data-scoped API key

Request Body

json
{
"event_name": "order_completed",
"external_id": "user_12345",
"attributes": {
"order_id": "ORD-2026-0042",
"amount": 149.99,
"currency": "USD",
"items": [
{"name": "Widget Pro", "quantity": 2, "price": 49.99},
{"name": "Gadget X", "quantity": 1, "price": 50.01}
]
},
"contact": {
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"timezone": "Asia/Singapore",
"tags": ["active-buyer"],
"properties": {
"plan": "growth",
"company": "Acme Corp"
}
},
"idempotency_key": "order_completed_ORD-2026-0042"
}

Parameters

FieldTypeRequiredDescription
event_namestringYesThe event type identifier. Use snake_case (e.g., order_completed, claim_submitted).
external_idstringYesYour application's unique user identifier. Used to match or create a contact.
attributesobjectNoArbitrary JSON object containing event-specific data. Defaults to {}. Accessible in NLT templates via {the Amount from the trigger event}.
contactobjectNoFields to upsert on the contact record. See Contact Fields.
occurred_atdatetimeNoWhen the event occurred (ISO 8601, defaults to server time). Useful for historical imports.
idempotency_keystringNoA unique key to prevent duplicate processing (max 255 chars). See Idempotency.
Note

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.

FieldTypeDescription
emailstringThe contact's email address. Required for email delivery.
first_namestringFirst name. Used in NLT templates as {the user's First Name}.
last_namestringLast name.
phonestringPhone number.
timezonestringIANA timezone (e.g., Asia/Singapore). Used for send-time optimization.
localestringLocale code (e.g., en-SG, vi-VN). Used for localized content.
propertiesobjectCustom key-value pairs. Shallow-merged with existing properties.
tagsstring[]Replaces the contact's entire tag list. Use $add_tags/$remove_tags for surgical updates.
$add_tagsstring[]Appends tags to the existing list (duplicates ignored).
$remove_tagsstring[]Removes specified tags from the existing list.
Tip

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)

json
{
"event_id": "evt_8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"status": "accepted"
}

The event has been accepted and queued for asynchronous processing. Flow matching, trip creation, and email delivery happen in the background.

Error Responses

json
{
"detail": "Missing required field: event_name",
"code": "validation_error"
}
StatusCodeDescription
400validation_errorMissing or invalid required fields
401invalid_api_keyAPI key is missing, revoked, or invalid
403insufficient_scopeAPI key does not have data scope
403plan_limit_reachedMonthly events quota exceeded. Upgrade your plan or wait for the next billing cycle.
409duplicate_eventAn event with this idempotency_key was already processed
429rate_limit_exceededToo many requests -- see Rate Limits

Examples

curl

bash
curl -X POST https://synapse-api.pyrx.tech/v1/events \
-H "Content-Type: application/json" \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_a1b2c3d4e5f67890abcdef1234567890" \
-d '{
"event_name": "claim_submitted",
"external_id": "worker_67890",
"attributes": {
"reference_number": "WIC-2026-0001",
"worker_name": "John Doe",
"incident_date": "2026-03-15T14:30:00+08:00",
"claim_type": "work_injury"
},
"contact": {
"email": "[email protected]",
"first_name": "John",
"timezone": "Asia/Ho_Chi_Minh",
"$add_tags": ["claimant"]
},
"idempotency_key": "claim_submitted_WIC-2026-0001"
}'

Python

python
import requests
 
response = requests.post(
"https://synapse-api.pyrx.tech/v1/events",
headers={
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
json={
"event_name": "claim_submitted",
"external_id": "worker_67890",
"attributes": {
"reference_number": "WIC-2026-0001",
"worker_name": "John Doe",
"incident_date": "2026-03-15T14:30:00+08:00",
"claim_type": "work_injury",
},
"contact": {
"email": "[email protected]",
"first_name": "John",
"timezone": "Asia/Ho_Chi_Minh",
"$add_tags": ["claimant"],
},
"idempotency_key": "claim_submitted_WIC-2026-0001",
},
)
 
assert response.status_code == 202
event = response.json()
print(event["event_id"]) # evt_8f14e45f-...

JavaScript

javascript
const response = await fetch("https://synapse-api.pyrx.tech/v1/events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
body: JSON.stringify({
event_name: "claim_submitted",
external_id: "worker_67890",
attributes: {
reference_number: "WIC-2026-0001",
worker_name: "John Doe",
incident_date: "2026-03-15T14:30:00+08:00",
claim_type: "work_injury",
},
contact: {
email: "[email protected]",
first_name: "John",
timezone: "Asia/Ho_Chi_Minh",
$add_tags: ["claimant"],
},
idempotency_key: "claim_submitted_WIC-2026-0001",
}),
});
 
const event = await response.json();
console.log(event.event_id); // evt_8f14e45f-...

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.

POST https://synapse-api.pyrx.tech/v1/events/batch

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

HeaderRequiredDescription
Content-TypeYesMust be application/json
X-WORKSPACE-IDYesYour workspace identifier
X-API-KEYYesA data-scoped API key
X-Synapse-SDK-VersionNoSend 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

json
{
"events": [
{
"event_name": "page_viewed",
"external_id": "user_12345",
"attributes": {"page": "/pricing", "referrer": "google"}
},
{
"event_name": "button_clicked",
"external_id": "user_12345",
"attributes": {"button_id": "cta_signup", "page": "/pricing"}
}
]
}
FieldTypeRequiredDescription
eventsarrayYesArray 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.

json
{
"accepted": 2,
"rejected": [
{
"index": 3,
"reason": "invalid_schema",
"detail": "1 validation error for EventIngest\n Value error, Either 'external_id' or 'user_id' must be provided"
},
{ "index": 4, "reason": "over_limit", "detail": null }
]
}
FieldTypeDescription
acceptedintegerNumber of events accepted and queued for processing
rejectedarrayOne entry per event that was not accepted (empty array when all accepted)
rejected[].indexinteger0-based position of the event in the submitted events array
rejected[].reasonstringinvalid_schema (validation failed — never retry), over_limit (valid but over quota — retry later), or runtime_error (transient server error — safe to retry)
rejected[].detailstring | nullHuman-readable detail, or null

Status matrix

OutcomeStatus
At least one event accepted202 Accepted
Zero accepted, all rejections are invalid_schema422 Unprocessable Content
Zero accepted, any rejection is over_limit403 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:

json
{
"accepted": 2,
"rejected": 1
}
Warning

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:

  1. Validate schema -- Verify required fields and attribute types
  2. Upsert contact -- Create or update the contact matching external_id
  3. Store event -- Persist the event to PostgreSQL
  4. Check idempotency -- Skip if this idempotency_key was already processed
  5. Publish to RabbitMQ -- Route to the flow trigger queue for async processing
  6. Flow matching -- Match against all active flows with matching trigger_event
  7. Segment evaluation -- If the flow has a target segment, verify the contact qualifies
  8. Trip creation -- Create a flow trip and execute the flow's steps
Note

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.