Skip to content

Contacts API

Contacts represent the people who receive emails from your Synapse workspace. Each contact is identified by an external_id (your application's user ID) and has an email address, name, tags, and custom properties.

Note

For a user guide on managing contacts in the dashboard, see Contacts.


Endpoints

MethodPathDescription
GET/v1/contactsList and search contacts
POST/v1/contactsUpsert a contact (create or merge)
GET/v1/contacts/{id}Get a contact by ID
PATCH/v1/contacts/{external_id}Partial update with operators
DELETE/v1/contacts/{external_id}Soft delete a contact
POST/v1/contacts/bulkBulk import up to 1,000 contacts
POST/v1/contacts/bulk-actionBulk action (delete, update status, export)
POST/v1/contacts/exportExport contacts as CSV or JSON
GET/v1/contacts/viewsList saved views
POST/v1/contacts/viewsCreate a saved view
PUT/v1/contacts/views/{view_id}Update a saved view
DELETE/v1/contacts/views/{view_id}Delete a saved view
GET/v1/contacts/event-namesList distinct event names
GET/v1/contacts/property-keysList distinct property keys
GET/v1/contacts/property-valuesList distinct values for a property key
GET/v1/contacts/countCount contacts (with optional audience filter)
GET/v1/contacts/{id}/eventsList events for a contact (cursor-paginated)

List Contacts

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl "https://synapse-api.pyrx.tech/v1/contacts?page=1&per_page=50" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number (1-indexed)
per_pageinteger50Items per page (max 200)
searchstringSearch by email, name, or external_id (substring match)
filtersstringURL-encoded JSON filter DSL (same format as segments)
segment_idUUIDFilter contacts by segment membership
sort_bystringcreated_atSort column: email, first_name, last_name, created_at, updated_at, subscription_status, product_type, or properties.{key}
sort_orderstringdescasc or desc

Response

json
{
"data": [
{
"id": "8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"external_id": "user_12345",
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+6591234567",
"product_type": "Standard",
"subscription_status": "subscribed",
"properties": {
"plan": "growth",
"company": "Acme Corp",
"country": "SG"
},
"created_at": "2026-01-15T08:00:00Z",
"updated_at": "2026-04-01T14:30:00Z"
}
],
"meta": {
"total": 4821,
"page": 1,
"per_page": 50,
"total_pages": 97
}
}

Upsert a Contact

Auth: API key. Requires scope: data, management, or full.

Creates a new contact or merges with an existing one if the external_id already exists. This is an idempotent operation -- safe to call repeatedly.

curl

bash
curl -X POST https://synapse-api.pyrx.tech/v1/contacts \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_67890",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"phone": "+6598765432",
"product_type": "Standard",
"tags": ["beta-user", "sg-region"],
"timezone": "Asia/Singapore",
"locale": "en-SG",
"properties": {
"plan": "starter",
"company": "Beta Inc",
"country": "SG",
"signup_source": "referral"
}
}'

Python

python
import requests
 
response = requests.post(
"https://synapse-api.pyrx.tech/v1/contacts",
headers={
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
json={
"external_id": "user_67890",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"tags": ["beta-user"],
"timezone": "Asia/Singapore",
"properties": {"plan": "starter", "company": "Beta Inc"},
},
)
 
data = response.json()
print(data["created"]) # True (new contact) or False (merged)
print(data["updated_fields"]) # [] or ["email", "first_name", ...]

JavaScript

javascript
const response = await fetch("https://synapse-api.pyrx.tech/v1/contacts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
body: JSON.stringify({
external_id: "user_67890",
email: "[email protected]",
first_name: "John",
last_name: "Doe",
tags: ["beta-user"],
timezone: "Asia/Singapore",
properties: { plan: "starter", company: "Beta Inc" },
}),
});
 
const data = await response.json();
console.log(data.created); // true or false
console.log(data.updated_fields); // [] or ["email", ...]

Request Body

FieldTypeRequiredDescription
external_idstringYesYour application's unique user identifier (unique per workspace)
emailstringNoContact's email address. Required for email delivery but not for contact creation.
first_namestringNoFirst name
last_namestringNoLast name
phonestringNoPhone number
product_typestringNoProduct category (used in segment exclusion rules)
subscription_statusstringNoContact status: subscribed, unsubscribed, bounced, complained
tagsstring[]NoList of tags for categorization and segmentation
timezonestringNoIANA timezone (e.g., Asia/Singapore, America/New_York)
localestringNoLocale code (e.g., en-SG, vi-VN) for localized content
propertiesobjectNoCustom key-value pairs (any valid JSON)

Response (200 OK)

json
{
"id": "con_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"external_id": "user_67890",
"email": "[email protected]",
"created": true,
"updated_fields": [],
"created_at": "2026-04-07T10:30:00Z"
}

When the contact already exists (matched by external_id), the provided fields are merged:

json
{
"id": "con_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"external_id": "user_67890",
"email": "[email protected]",
"created": false,
"updated_fields": ["first_name", "tags", "properties"],
"created_at": "2026-04-07T10:30:00Z"
}
Tip

Unlike a traditional POST that returns 409 Conflict on duplicates, this endpoint is designed as an upsert. It is safe to call repeatedly with the same external_id -- existing contacts are merged, not rejected. Use PATCH for partial updates with tag operators.


Partial Update

Auth: API key. Requires scope: data, management, or full.

Update specific fields on an existing contact. Supports tag operators ($add_tags, $remove_tags) and property shallow-merge.

curl

bash
curl -X PATCH https://synapse-api.pyrx.tech/v1/contacts/user_67890 \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_..." \
-H "Content-Type: application/json" \
-d '{
"first_name": "Jonathan",
"$add_tags": ["premium", "vip"],
"$remove_tags": ["beta-user"],
"properties": {
"plan": "growth",
"upgraded_at": "2026-04-07"
}
}'

Python

python
response = requests.patch(
"https://synapse-api.pyrx.tech/v1/contacts/user_67890",
headers={
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
json={
"first_name": "Jonathan",
"$add_tags": ["premium", "vip"],
"$remove_tags": ["beta-user"],
"properties": {
"plan": "growth",
"upgraded_at": "2026-04-07",
},
},
)

JavaScript

javascript
await fetch("https://synapse-api.pyrx.tech/v1/contacts/user_67890", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
body: JSON.stringify({
first_name: "Jonathan",
$add_tags: ["premium", "vip"],
$remove_tags: ["beta-user"],
properties: {
plan: "growth",
upgraded_at: "2026-04-07",
},
}),
});

Merge Behavior

  • Top-level fields (email, first_name, last_name, phone, timezone, locale): Overwritten if provided
  • tags: Replaced entirely if provided. Use $add_tags/$remove_tags for surgical updates.
  • $add_tags: Appends tags to the existing list (duplicates are ignored)
  • $remove_tags: Removes specified tags from the existing list
  • properties: Shallow-merged with existing properties. Set a key to null to remove it.
json
// Existing: properties = { "plan": "free", "company": "Acme", "old_field": "x" }
// PATCH: properties = { "plan": "growth", "region": "APAC", "old_field": null }
// Result: properties = { "plan": "growth", "company": "Acme", "region": "APAC" }
Note

The external_id in the URL path identifies the contact. You cannot change a contact's external_id via PATCH.


Delete a Contact

Auth: API key. Requires scope: management or full.

Soft-deletes a contact by setting subscription_status to "deleted". The contact record is retained for audit purposes, and email logs are preserved.

bash
curl -X DELETE https://synapse-api.pyrx.tech/v1/contacts/user_67890 \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_..."

Response (200 OK)

json
{
"deleted": true,
"external_id": "user_67890"
}
Note

This is a soft delete. The contact's subscription_status is set to "deleted", which prevents future email delivery. Active flow trips for this contact are dropped. The contact record and email logs are retained for audit purposes -- no data is permanently removed.


Bulk Import

Auth: API key. Requires scope: data, management, or full.

Import up to 1,000 contacts in a single request. Supports three conflict resolution strategies.

curl

bash
curl -X POST https://synapse-api.pyrx.tech/v1/contacts/bulk \
-H "X-WORKSPACE-ID: ws_k7x9m2p4" \
-H "X-API-KEY: psk_live_..." \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"external_id": "user_001",
"email": "[email protected]",
"first_name": "Alice",
"tags": ["imported"],
"properties": {"plan": "free"}
},
{
"external_id": "user_002",
"email": "[email protected]",
"first_name": "Bob",
"tags": ["imported"],
"properties": {"plan": "starter"}
}
],
"on_conflict": "merge"
}'

Python

python
result = requests.post(
"https://synapse-api.pyrx.tech/v1/contacts/bulk",
headers={
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
json={
"contacts": [
{"external_id": "user_001", "email": "[email protected]", "first_name": "Alice"},
{"external_id": "user_002", "email": "[email protected]", "first_name": "Bob"},
],
"on_conflict": "merge",
},
).json()
 
print(result["total"]) # 2
print(result["created"]) # 1
print(result["updated"]) # 1

JavaScript

javascript
const result = await fetch("https://synapse-api.pyrx.tech/v1/contacts/bulk", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WORKSPACE-ID": "ws_k7x9m2p4",
"X-API-KEY": "psk_live_a1b2c3d4e5f67890abcdef1234567890",
},
body: JSON.stringify({
contacts: [
{ external_id: "user_001", email: "[email protected]", first_name: "Alice" },
{ external_id: "user_002", email: "[email protected]", first_name: "Bob" },
],
on_conflict: "merge",
}),
}).then(r => r.json());
 
console.log(result.total, result.created, result.updated);

Parameters

FieldTypeRequiredDescription
contactsarrayYesArray of contact objects (max 1,000)
on_conflictstringNoConflict resolution: "merge" (default), "skip", or "replace"

Conflict Resolution

StrategyBehavior
skipIf external_id exists, skip the contact entirely. No fields are updated.
mergeIf external_id exists, merge provided fields into the existing contact. Properties are shallow-merged, tags are appended.
replaceIf external_id exists, overwrite all fields with the provided values. Omitted fields are cleared.

Response

json
{
"total": 2,
"created": 1,
"updated": 1,
"skipped": 0,
"errors": []
}

Partial success is supported. If some contacts fail validation, they appear in errors while valid contacts are processed:

json
{
"total": 3,
"created": 1,
"updated": 1,
"skipped": 0,
"errors": [
{
"index": 2,
"external_id": "user_003",
"error": "Invalid email format"
}
]
}

Bulk Action

Perform bulk operations on multiple contacts at once. Supports individual selection (up to 200 IDs) or select-all with filters.

Auth: JWT (dashboard). Requires contacts:write permission.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/contacts/bulk-action \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"action": "update_status",
"contact_ids": ["8f14e45f-ceea-467f-a83c-01a01ba3c5db"],
"params": {
"subscription_status": "unsubscribed"
}
}'

Request Body

FieldTypeRequiredDescription
actionstringYes"delete", "update_status", or "export"
contact_idsUUID[]ConditionalContact IDs to target (required unless select_all is true, max 200)
select_allbooleanNoTarget all contacts matching filters instead of contact_ids
filtersobjectNoFilter DSL (only used when select_all is true)
paramsobjectNoAction-specific parameters (e.g., {"subscription_status": "unsubscribed"} for update_status)

Action: update_status

Valid statuses: subscribed, unsubscribed, bounced, complained.

Response (200 OK)

json
{
"affected": 15,
"action": "update_status",
"errors": []
}

Export Contacts

Export contacts as CSV or JSON. Small exports (up to 5,000 contacts) download immediately. Larger exports create a background job.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl -X POST https://synapse-api.pyrx.tech/v1/contacts/export \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"format": "csv",
"scope": "all",
"include_properties": true
}'

Request Body

FieldTypeDefaultDescription
formatstring"csv""csv" or "json"
scopestring"all""all", "filtered", or "selected"
contact_idsUUID[][]Required when scope is "selected"
filtersobject{}Filter DSL, used when scope is "filtered"
columnsstring[][]Column keys to include (empty = all columns)
include_propertiesbooleantrueFlatten custom properties into individual columns

Response: Sync (up to 5,000 contacts)

Returns a streaming file download with Content-Disposition: attachment header. The X-Export-Count response header contains the total number of exported rows.

Response: Async (more than 5,000 contacts)

json
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"estimated_rows": 12450,
"message": "Export started. Poll GET /v1/jobs/{job_id} for progress."
}

Poll GET /v1/jobs/{job_id} for status. When status is "completed", download the file with GET /v1/jobs/{job_id}/download. Export files expire after 24 hours.


Saved Views

Saved views store filter, column, and sort configurations. Each workspace can have up to 20 custom views.

Auth: JWT (dashboard). Read requires contacts:read, write requires contacts:write.

List Views

bash
curl https://synapse-api.pyrx.tech/v1/contacts/views \
-H "Authorization: Bearer <jwt>"
json
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Active Subscribers",
"filters": {"subscription_status": "subscribed"},
"columns": [{"key": "email"}, {"key": "first_name"}, {"key": "created_at"}],
"sort_by": "created_at",
"sort_order": "desc",
"is_system": false,
"is_default": true,
"pin_order": 1,
"created_by": "member_abc",
"created_at": "2026-04-01T10:00:00Z",
"updated_at": "2026-04-01T10:00:00Z"
}
]

Create View

bash
curl -X POST https://synapse-api.pyrx.tech/v1/contacts/views \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"name": "SG Enterprise Contacts",
"filters": {"properties.country": "SG"},
"columns": [{"key": "email"}, {"key": "first_name"}, {"key": "properties.company"}],
"sort_by": "updated_at",
"sort_order": "desc",
"is_default": false,
"pin_order": 0
}'

Response: 201 Created with the full ContactView object.

FieldTypeDefaultDescription
namestringRequiredView name (max 255 chars)
filtersobject{}Filter configuration
columnsarray[]Column configuration objects
sort_bystring"created_at"Sort column
sort_orderstring"desc""asc" or "desc"
is_defaultbooleanfalseLoad this view by default (unsets any other default)
pin_orderinteger0Pin position (higher = listed first)

Update View

bash
curl -X PUT https://synapse-api.pyrx.tech/v1/contacts/views/{view_id} \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"name": "Updated View Name", "is_default": true}'

All fields are optional -- only provided fields are updated. System views cannot be modified (returns 403).

Delete View

bash
curl -X DELETE https://synapse-api.pyrx.tech/v1/contacts/views/{view_id} \
-H "Authorization: Bearer <jwt>"

Returns 204 No Content. System views cannot be deleted (returns 403).


Event Names

List distinct event names received for this workspace. Useful for populating event dropdowns in the UI.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl "https://synapse-api.pyrx.tech/v1/contacts/event-names?source=tenant_api" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeDescription
sourcestringFilter by event source: tenant_api, email, system, flow, sdk

Response

json
[
{"name": "user_signup", "count": 1542},
{"name": "order_placed", "count": 891},
{"name": "claim_submitted", "count": 234}
]

Property Keys

List distinct custom property keys found across contacts. Samples up to 500 recent contacts and returns keys with occurrence counts and auto-detected data types.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl https://synapse-api.pyrx.tech/v1/contacts/property-keys \
-H "Authorization: Bearer <jwt>"

Response

json
[
{"key": "country", "count": 412, "type": "string"},
{"key": "plan", "count": 389, "type": "string"},
{"key": "age", "count": 201, "type": "number"},
{"key": "is_verified", "count": 156, "type": "boolean"},
{"key": "registered_at", "count": 98, "type": "date"}
]

Property Values

List distinct values for a specific property key. Samples up to 500 contacts and returns the top 100 values by frequency.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl "https://synapse-api.pyrx.tech/v1/contacts/property-values?property_key=country" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeRequiredDescription
property_keystringYesThe property key to get values for

Response

json
[
{"value": "SG", "count": 342},
{"value": "US", "count": 215},
{"value": "VN", "count": 89}
]

Count Contacts

Count contacts matching audience criteria. Used internally by the flow wizard to show audience size.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl "https://synapse-api.pyrx.tech/v1/contacts/count?mode=filter&filter_event=user_signup&filter_operator=has_executed" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeDefaultDescription
modestring"all""all" (total count) or "filter" (filter by event behavior)
filter_eventstringEvent name to filter by (used when mode is "filter")
filter_operatorstring"has_executed" or "has_not_executed"

Response

json
{
"user_count": 1542,
"reachable_users": 1480,
"reachable_email": 1480,
"total_contacts": 4821
}
  • user_count: Contacts matching the filter criteria
  • reachable_users / reachable_email: Contacts with a non-empty email address (reachable by email)
  • total_contacts: Total contacts in the workspace (unfiltered)

Contact Events

List events for a specific contact with cursor-based pagination and filtering.

Auth: JWT (dashboard). Requires contacts:read permission.

bash
curl "https://synapse-api.pyrx.tech/v1/contacts/8f14e45f-ceea-467f-a83c-01a01ba3c5db/events?limit=50&order=desc" \
-H "Authorization: Bearer <jwt>"

Query Parameters

ParameterTypeDefaultDescription
sourcestring--Filter by source: tenant_api, email, system, flow, sdk
event_namestring--Filter by event name (substring match)
cursorstring--ISO timestamp cursor for pagination. Returns events older (or newer) than this timestamp.
limitinteger100Items per page (max 500)
orderstringdescSort order: desc (latest first) or asc (oldest first)

Response

json
{
"events": [
{
"id": "evt_8f14e45f-ceea-467f-a83c-01a01ba3c5db",
"event_name": "order_completed",
"source": "tenant_api",
"timestamp": "2026-04-07T10:30:00Z",
"received_at": "2026-04-07T10:30:01Z",
"properties": {
"order_id": "ORD-2026-0042",
"amount": 149.99
}
}
],
"next_cursor": "2026-04-06T15:22:00Z",
"has_more": true
}

Use next_cursor as the cursor parameter in the next request to fetch the next page.


Plan Limits

PlanMax Contacts
Free1,000
Starter10,000
Growth100,000
EnterpriseUnlimited
json
{
"detail": "Contact limit reached for your plan. Upgrade to add more contacts.",
"code": "plan_limit_reached"
}