Customers API
Create and update canonical customer profiles per project, register push devices, and ingest customer-scoped events.
Endpoints
GET /api/organizations/{organizationId}/projects/{projectId}/customers
POST /api/organizations/{organizationId}/projects/{projectId}/customers
POST /api/organizations/{organizationId}/projects/{projectId}/customers/import/csv
POST /api/organizations/{organizationId}/projects/{projectId}/customers/events
GET /api/organizations/{organizationId}/projects/{projectId}/customers/segments
POST /api/organizations/{organizationId}/projects/{projectId}/customers/segments
POST /api/organizations/{organizationId}/projects/{projectId}/customers/segments/preview
POST /api/organizations/{organizationId}/projects/{projectId}/customers/eligibility
GET /api/organizations/{organizationId}/projects/{projectId}/customers/{customerId}/devices
POST /api/organizations/{organizationId}/projects/{projectId}/customers/{customerId}/devices
Authentication and Scopes
These endpoints support dashboard session auth or project API keys.
| Endpoint | Scope |
|---|---|
GET /customers | customers:read |
POST /customers | customers:write |
POST /customers/import/csv | customers:write |
POST /customers/events | customer_events:write |
GET /customers/segments | customers:read |
POST /customers/segments | customers:write |
POST /customers/segments/preview | customers:read |
POST /customers/eligibility | customers:read |
GET /customers/{customerId}/devices | customers:read |
POST /customers/{customerId}/devices | customer_devices:write |
project:admin can access all customer APIs.
Upsert Customer
POST /customers
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | conditional | Stable app/customer ID |
email | string | conditional | Customer email |
name | string | no | Display name |
phone | string | no | Phone number |
status | string | no | active, suppressed, deleted |
metadata | object | no | Arbitrary customer attributes |
identities | array | no | Alternate IDs (email, user_id, etc.) |
preferences | object | no | Channel opt-in settings |
At least one of externalId, email, or identities is required.
Response
{
"customer": {
"id": "uuid",
"externalId": "user-123",
"email": "user@example.com"
},
"created": true
}
Example: SDK API Client
import { createSyntropyApiClient } from "@eclosion-tech/syntropy-node";
const api = createSyntropyApiClient({
baseUrl: "https://syntropy.chat/api",
apiKey: process.env.SYNTROPY_API_KEY,
});
const scope = { organizationId: "org_uuid", projectId: "project_uuid" };
await api.customers.upsert(scope, {
externalId: "user_123",
email: "user@example.com",
metadata: { plan: "pro" },
});
await api.customers.trackEvent(scope, {
name: "purchase_completed",
externalId: "user_123",
payload: { amount: 99.99 },
});
Backfill Existing Subscribers
To link existing email_subscribers rows into canonical customer profiles:
pnpm --filter @syntropy/web customers:backfill-subscriber-links \
--organizationId <org-uuid> \
--accountId <account-uuid>
If --accountId is omitted, the script only auto-links organizations with a single active account.
Identity Merge Policy
POST /customers evaluates all provided identifiers (externalId, email, identities) together.
- If all matches resolve to the same existing customer, that customer is updated.
- If there are no matches, a new customer is created.
- If matches resolve to different customers, the request is rejected with
409 CUSTOMER_IDENTITY_CONFLICTand no automatic cross-customer merge is performed.
Import Customers from CSV
POST /customers/import/csv
Designed for Customer.io-style exports (header row with email, optional id / external_id, name, phone, and additional attribute columns).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
csv | string | yes | CSV file contents |
hasHeader | boolean | no | Defaults to true |
subscribeToListId | uuid | no | If provided, upserts subscriber rows into this list |
source | string | no | Import source label (default: customer_io_csv_import) |
dryRun | boolean | no | Validate/parse only, do not write |
maxRows | number | no | Max rows to process (default: 2000, max 10000) |
Response
{
"success": true,
"summary": {
"totalRows": 200,
"parsedRows": 198,
"skippedRows": 2,
"customerCreated": 120,
"customerUpdated": 78,
"conflicts": [],
"subscribersCreated": 40,
"subscribersUpdated": 30,
"subscribersLinked": 70
},
"skipped": [
{ "lineNumber": 18, "reason": "missing_email" }
]
}
Track Customer Event
POST /customers/events
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Event name |
timestamp | ISO datetime | no | Event time |
customerId | uuid | conditional | Customer id |
externalId | string | conditional | External customer id |
email | string | conditional | Customer email |
payload | object | no | Event payload |
At least one of customerId, externalId, or email is required.
If more than one identifier is provided, they must resolve to the same customer. Otherwise the API returns 409 CUSTOMER_IDENTITY_CONFLICT.
Response
{
"success": true,
"eventId": "uuid",
"eventName": "customer.purchase_completed",
"customerId": "uuid"
}
Manage Segments
Create or Update Segment
POST /customers/segments
Creates a segment definition and re-materializes segment membership against current customer profiles.
| Field | Type | Required | Description |
|---|---|---|---|
segmentId | uuid | no | Update existing segment when provided |
name | string | yes | Segment display name |
slug | string | no | Optional slug override |
description | string | no | Segment description |
status | string | no | draft, active, paused, archived |
definition.logic | string | no | and (default) or or |
definition.conditions | array | yes | Segment condition rules |
Condition operators: eq, neq, contains, in, not_in, gt, gte, lt, lte, exists.
List Segments
GET /customers/segments
Returns segments with materialized member counts and pagination.
Preview Segment
POST /customers/segments/preview
Dry-runs a segment definition against current customers and returns matched rows.
{
"definition": {
"logic": "and",
"conditions": [
{ "field": "status", "operator": "eq", "value": "active" },
{ "field": "metadata.plan", "operator": "eq", "value": "pro" }
]
},
"limit": 100,
"offset": 0
}
Customer Eligibility
POST /customers/eligibility
Evaluates whether a customer is currently eligible for a channel/topic send, optionally constrained by segment membership.
| Field | Type | Required | Description |
|---|---|---|---|
customerId | uuid | conditional | Customer id |
externalId | string | conditional | External customer id |
email | string | conditional | Customer email |
channel | string | yes | email, push, sms |
topic | string | no | Optional topic key |
segmentIds | uuid[] | no | Required segment ids |
segmentMatch | string | no | any (default) or all |
Response includes eligible, reasons, and check breakdown (channelOptIn, topicAllowed, suppressionActive, segmentMatch).
Use this as a pre-send gate before calling the transactional email send API — if eligible is false, skip the send rather than letting it reach the deliverability guardrails. This gives you explicit control over suppression logic and lets you surface a reason to the user if needed.
const { eligible, reasons } = await api.customers.checkEligibility(scope, {
email: "user@example.com",
channel: "email",
});
if (!eligible) {
console.log("Skipping send:", reasons);
return;
}
// safe to send
await sendTransactionalEmail({ to: "user@example.com", ... });
Pass segmentIds to additionally gate sends on segment membership — useful for feature-flagged campaigns or plan-tier targeting.
Register Device
POST /customers/{customerId}/devices
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | yes | ios, android, web |
pushToken | string | yes | Push token |
deviceId | string | no | Client device identifier |
appVersion | string | no | App version |
osVersion | string | no | OS version |
locale | string | no | Locale |
timezone | string | no | Timezone |
metadata | object | no | Device attributes |
active | boolean | no | Device active flag |
Response
{
"device": {
"id": "uuid",
"platform": "ios",
"active": true
}
}
Push Notification Delivery
Once devices are registered, Syntropy can deliver push notifications natively via:
- Apple APNs -- for iOS devices with native APNs tokens
- Firebase FCM -- for Android devices with FCM registration tokens
- Expo Push -- for apps using Expo push tokens (
ExponentPushToken[...])
Configure push credentials per project in the dashboard under Settings > Push. See the React Native SDK docs for setup instructions.
Syntropy automatically invalidates device tokens that are rejected by the push provider, keeping your device inventory clean.
Related
- Chat Triggers — capture leads from AI chat conversations and bridge them into customer records via webhook
- Transactional Email — send email to customers; use the eligibility API as a pre-send gate
- Chat Schemas — define structured fields extracted from chat leads
- Project API Keys —
customers:read,customers:write,customer_events:write,customer_devices:writescopes