Audience
Manage people, companies, and lists using the Quotient SDK
Overview
The audience API lets you manage people, companies, lists, and custom properties.
Both the client and server SDKs expose the audience API under audience.*.
The client SDK includes audience.people only. The server SDK adds
audience.companies, audience.lists, and audience.properties.
| Endpoint | API Key | Server SDK | Client SDK |
|---|---|---|---|
| Upsert person | public or private | audience.people.upsert() | audience.people.upsert() |
| Identify a person | public or private | audience.people.identify() | audience.people.identify() |
| Register for an event | public or private | audience.people.registerForEvent() | audience.people.registerForEvent() |
| Subscribe to email | public or private | audience.people.subscribeToEmail() | audience.people.subscribeToEmail() |
| Upsert company | public or private | audience.companies.upsert() | — |
| Upsert list | private only | audience.lists.upsert() | — |
| List all lists | private only | audience.lists.list() | — |
| Get a list | private only | audience.lists.get() | — |
| List people in a list | private only | audience.lists.listPeople() | — |
| Add people to a list | private only | audience.lists.addPeople() | — |
| Remove people from a list | private only | audience.lists.removePeople() | — |
| List custom properties | private only | audience.properties.list() | — |
| Create custom property | private only | audience.properties.create() | — |
| Update custom property | private only | audience.properties.update() | — |
To manage list membership from the browser, use audience.people.upsert()
with the lists parameter.
People
Upsert a Person
audience.people.upsert(params)
POST /api/v0/audience/peopleAuth: public or private key · scope:
AUDIENCE_PERSON_WRITE
Creates or updates a person record based on email address.
| Param | Type | Required | Description |
|---|---|---|---|
emailAddress | string | Yes | Primary identifier |
emailSubscriptionStatus | "SUBSCRIBED" | "UNSUBSCRIBED" | No | Marketing email opt-in state |
firstName | string | No | First name |
lastName | string | No | Last name |
jobTitle | string | No | Job title |
leadScore | number | No | Integer lead score (default 0) |
lists | string[] | No | List slugs to add the person to |
properties | Record<string, string | number | boolean | Date> | No | Custom properties (must be pre-defined) |
const { personId } = await client.audience.people.upsert({
emailAddress: "user@example.com",
firstName: "Jane",
lastName: "Doe",
emailSubscriptionStatus: "SUBSCRIBED",
leadScore: 50,
lists: ["newsletter", "customers"],
properties: {
plan: "pro",
signupSource: "landing-page",
},
});
Email Subscription Statuses:
SUBSCRIBED- Opted in to marketing emailsUNSUBSCRIBED- Opted out of marketing emails- If not specified:
- Existing people keep their current state
- New people default based on double opt-in settings
Returns:
{
personId: string; // Unique identifier (CUID format)
}
Identify a Person
audience.people.identify(params)
POST /api/v0/audience/people/identifyAuth: public or private key · scope:
AUDIENCE_PERSON_WRITE
Identifies a person by email, creating them if they don't already exist —
the same job upsert() does, but the intended path forward for new
integrations, since it carries lead attribution. Creating a new person fires
a personCreated analytics event with whatever attribution context is
available.
The client SDK attaches the visitor's current browser context (device,
session, referrer, and attribution) automatically. On the server, forward it
yourself, typically from client.getBrowserContext() on your frontend.
| Param | Type | Required | Description |
|---|---|---|---|
emailAddress | string | Yes | Primary identifier |
firstName | string | No | First name |
lastName | string | No | Last name |
jobTitle | string | No | Job title |
leadScore | number | No | Integer lead score |
lists | string[] | No | List slugs to add the person to |
properties | Record<string, string | number | boolean | Date> | No | Custom properties (must be pre-defined) |
browserContext | BrowserContext | Server SDK only | Attribution to attach if this creates a new person |
// Browser
const { personId } = await client.audience.people.identify({
emailAddress: "user@example.com",
firstName: "Jane",
});
// Server
const { personId } = await client.audience.people.identify({
emailAddress: "user@example.com",
firstName: "Jane",
browserContext: clientBrowserContext, // forwarded from the browser client
});
Returns:
{
personId: string;
}
Register for an Event
audience.people.registerForEvent(options)
POST /api/v0/audience/people/register-for-eventAuth: public or private key · scope:
AUDIENCE_PERSON_WRITE
Identifies a person, then registers them for a marketing event. Registration
dispatches the marketingEventRegistration analytics event and triggers the
event's flow automatically — no separate analytics.event() call is needed.
| Param | Type | Required | Description |
|---|---|---|---|
emailAddress | string | Yes | Primary identifier |
eventSlug | string | Yes | Slug of the marketing event to register for |
firstName | string | No | First name |
lastName | string | No | Last name |
jobTitle | string | No | Job title |
leadScore | number | No | Integer lead score |
lists | string[] | No | List slugs to add the person to |
properties | Record<string, string | number | boolean | Date> | No | Custom properties (must be pre-defined) |
browserContext | BrowserContext | No | Attribution to attach if this creates a new person |
const { personId, eventId, participantStatus } =
await client.audience.people.registerForEvent({
emailAddress: "user@example.com",
eventSlug: "spring-webinar",
});
Returns:
{
personId: string;
eventId: string;
participantStatus: "INVITED" | "REGISTERED" | "ATTENDED" | "NO_SHOW" | "CANCELLED";
}
Fails with 404 when eventSlug doesn't match an event for the business, and
400 when registration isn't allowed in the event's current state (e.g. an
ended event that doesn't accept the requested status).
Subscribe to Email
audience.people.subscribeToEmail(options)
POST /api/v0/audience/people/subscribe-to-emailAuth: public or private key · scope:
AUDIENCE_PERSON_WRITE
Identifies a person, then subscribes them to email, honoring the business's
double opt-in setting. If double opt-in is enabled, a new subscriber (or one
who was never subscribed) lands on PENDING rather than SUBSCRIBED. Any
other existing status is left exactly as it is; this call never overrides an
explicit unsubscribe or a compliance-managed status.
| Param | Type | Required | Description |
|---|---|---|---|
emailAddress | string | Yes | Primary identifier |
firstName | string | No | First name |
lastName | string | No | Last name |
jobTitle | string | No | Job title |
leadScore | number | No | Integer lead score |
lists | string[] | No | List slugs to add the person to |
properties | Record<string, string | number | boolean | Date> | No | Custom properties (must be pre-defined) |
browserContext | BrowserContext | No | Attribution to attach if this creates a new person |
const { personId, emailSubscriptionStatus } =
await client.audience.people.subscribeToEmail({
emailAddress: "user@example.com",
});
Returns:
{
personId: string;
emailSubscriptionStatus:
| "SUBSCRIBED"
| "UNSUBSCRIBED"
| "PENDING"
| "NOT_SUBSCRIBED"
| "REDACTED"
| "INVALID";
}
Companies
Upsert a Company
audience.companies.upsert(params)
POST /api/v0/audience/companiesAuth: public or private key · scope:
AUDIENCE_COMPANY_WRITE
Creates or updates a company record keyed on domain.
| Param | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | Company domain (upsert key) |
name | string | No | Company name |
description | string | No | Description |
industries | string[] | No | Industry tags |
totalEmployees | number | No | Employee count |
address1 | string | No | Street address |
city | string | No | City |
regionCode | string | No | State/region code |
country | string | No | Country code |
zip | string | No | Postal code |
socialLinkLinkedIn | string | No | LinkedIn URL |
properties | Record<string, string | number | boolean | Date> | No | Custom properties (must be pre-defined) |
const { companyId } = await client.audience.companies.upsert({
domain: "acme.com",
name: "Acme Corp",
description: "Makes everything",
industries: ["manufacturing"],
totalEmployees: 500,
properties: {
arr: 1200000,
fundingStage: "Series B",
},
});
Returns:
{
companyId: string; // Unique identifier (CUID format)
}
Domain is not required to be unique. If exactly one company matches domain,
it's updated; if none match, one is created. If more than one company shares
the same domain, the upsert fails with a 409:
{
domain: string; // The domain that matched more than one company
matchCount: number; // How many companies matched
matchingCompanyIds: string[]; // Ids of the matching companies
}
Lists
The Lists API lets you create and manage audience lists programmatically. Lists are identified by a unique slug and can contain people identified by either their person ID or email address.
All list operations require a private API key. List membership
changes (addPeople, removePeople) are not safe to expose on a
public key, so they're server-side only.
import { QuotientServer } from "@quotientjs/server";
const client = new QuotientServer({
privateKey: "sk_your_private_api_key",
});
Create or Update a List
audience.lists.upsert(options)
POST /api/v0/audience/listsAuth: private key only · scope:
AUDIENCE_LIST_WRITE
The slug is always the upsert key — you either provide it directly, or it gets derived from the name.
| Param | Type | Required | Description |
|---|---|---|---|
name | string | One of name or slug | List name (slug auto-derived if slug omitted) |
slug | string | One of name or slug | Explicit slug to target |
description | string | No | List description |
// By name — slug is auto-generated ("newsletter-subscribers")
await client.audience.lists.upsert({
name: "Newsletter Subscribers",
description: "People who opted into our weekly newsletter",
});
// By slug — target an existing list directly
await client.audience.lists.upsert({
slug: "newsletter-subscribers",
description: "Updated description",
});
// By slug + name — target by slug, rename the list
await client.audience.lists.upsert({
slug: "newsletter-subscribers",
name: "Weekly Newsletter",
});
Upsert behavior:
| Input | Lookup key | Not found | Found |
|---|---|---|---|
{ name } | generateSlug(name) | Create with name + generated slug | Update name |
{ name, description } | generateSlug(name) | Create with both | Update name + description |
{ slug } | slug | Create (name defaults to slug) | No-op |
{ slug, name } | slug | Create with name + slug | Update name |
{ slug, description } | slug | Create (name defaults to slug) | Update description |
{ slug, name, description } | slug | Create with all fields | Update name + description |
Returns:
{
listId: string;
name: string;
slug: string;
}
List All Lists
audience.lists.list(options?)
GET /api/v0/audience/listsAuth: private key only · scope:
AUDIENCE_LIST_READ
| Param | Type | Required | Description |
|---|---|---|---|
search | string | No | Filter by name |
page | number | No | Page number (default 1) |
limit | number | No | Results per page (default 20) |
const { lists, pageData } = await client.audience.lists.list({
search: "newsletter",
page: 1,
limit: 20,
});
Returns:
{
lists: {
id: string;
name: string;
slug: string;
description: string | null;
peopleCount: number;
createdAt: string;
updatedAt: string;
}[];
pageData: {
page: number;
limit: number;
total: number;
isNextPageAvailable: boolean;
};
}
Get a Single List
audience.lists.get(options)
GET /api/v0/audience/lists/{slug}Auth: private key only · scope:
AUDIENCE_LIST_READ
| Param | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | List slug |
const { list } = await client.audience.lists.get({
slug: "newsletter-subscribers",
});
Returns:
{
list: {
id: string;
name: string;
slug: string;
description: string | null;
peopleCount: number;
createdAt: string;
updatedAt: string;
};
}
List People in a List
audience.lists.listPeople(options)
GET /api/v0/audience/lists/{slug}/peopleAuth: private key only · scope:
AUDIENCE_LIST_READ
| Param | Type | Required | Description |
|---|---|---|---|
listSlug | string | Yes | List slug |
search | string | No | Filter by email |
page | number | No | Page number (default 1) |
limit | number | No | Results per page (default 20) |
const { people, pageData } = await client.audience.lists.listPeople({
listSlug: "newsletter-subscribers",
search: "jane",
page: 1,
limit: 20,
});
Returns:
{
people: {
personId: string;
emailAddress: string;
firstName: string | null;
lastName: string | null;
}[];
pageData: {
page: number;
limit: number;
total: number;
isNextPageAvailable: boolean;
};
}
Add People to a List
audience.lists.addPeople(options)
POST /api/v0/audience/lists/{slug}/peopleAuth: private key only · scope:
AUDIENCE_LIST_WRITE
Add up to 100 people per request. Each entry can reference an existing person by ID, or provide an email address to upsert a person and add them in one call.
| Param | Type | Required | Description |
|---|---|---|---|
listSlug | string | Yes | List slug |
people | AddPersonEntry[] | Yes | Array of people to add (max 100) |
Each entry in people is one of:
| Param | Type | Required | Description |
|---|---|---|---|
personId | string | Yes (if no email) | Existing person ID |
emailAddress | string | Yes (if no ID) | Email to upsert |
firstName | string | No | First name (with email only) |
lastName | string | No | Last name (with email only) |
jobTitle | string | No | Job title (with email only) |
leadScore | number | No | Lead score (with email only) |
properties | Record<string, ...> | No | Custom properties (with email only) |
await client.audience.lists.addPeople({
listSlug: "newsletter-subscribers",
people: [
{ personId: "clx..." },
{
emailAddress: "jane@example.com",
firstName: "Jane",
lastName: "Doe",
properties: { plan: "pro" },
},
],
});
Returns:
{
added: number;
listSlug: string;
listId: string;
}
Remove People from a List
audience.lists.removePeople(options)
DELETE /api/v0/audience/lists/{slug}/peopleAuth: private key only · scope:
AUDIENCE_LIST_WRITE
Remove up to 100 people per request. Each entry can reference a person by ID or by email address. This operation is idempotent — removing a person who is not in the list (or referencing an email that doesn't exist) is a no-op.
| Param | Type | Required | Description |
|---|---|---|---|
listSlug | string | Yes | List slug |
people | RemovePersonEntry[] | Yes | Array of people to remove (max 100) |
Each entry in people is one of:
| Param | Type | Description |
|---|---|---|
personId | string | Existing person ID |
emailAddress | string | Person's email address |
await client.audience.lists.removePeople({
listSlug: "newsletter-subscribers",
people: [
{ personId: "clx..." },
{ emailAddress: "jane@example.com" },
],
});
Returns:
{
removed: number;
listSlug: string;
listId: string;
}
Custom Properties
Custom properties are the fields you define on people, companies, and deals
beyond the built-in ones. These endpoints manage the property definitions —
the values themselves are written through audience.people.upsert() /
audience.companies.upsert() properties payloads, which validate against
the definitions.
Every definition has an entityType ("person", "company", or "deal"),
an immutable alphanumeric id (the key used in properties payloads), an
immutable datatype, and a readOnly flag. SINGLE_SELECT and
MULTI_SELECT datatypes carry allowedValues — the closed set of allowed
options, always at least 2.
List Custom Properties
audience.properties.list(options)
GET /api/v0/audience/propertiesAuth: private key only · scope:
AUDIENCE_PROPERTY_READ
| Param | Type | Required | Description |
|---|---|---|---|
entityType | "person" | "company" | "deal" | Yes | Which entity's definitions to list |
const { properties } = await client.audience.properties.list({
entityType: "person",
});
Returns:
{
properties: Array<{
id: string;
businessId: string;
entityType: "person" | "company" | "deal";
displayName: string;
description: string | null;
datatype: string; // e.g. "STRING", "NUMBER", "SINGLE_SELECT", ...
allowedValues?: string[]; // present only on select datatypes
readOnly: boolean;
}>;
}
Create a Custom Property
audience.properties.create(options)
POST /api/v0/audience/propertiesAuth: private key only · scope:
AUDIENCE_PROPERTY_WRITE
| Param | Type | Required | Description |
|---|---|---|---|
entityType | "person" | "company" | "deal" | Yes | Which entity the property is defined for |
id | string | Yes | API name — alphanumeric, unique per entity type, immutable |
displayName | string | Yes | Human-readable name shown in the UI |
datatype | string | Yes | Value type (STRING, NUMBER, BOOLEAN, DATE, SINGLE_SELECT, ...) — immutable |
allowedValues | string[] | Yes (select datatypes) | Allowed options — at least 2; forbidden for other datatypes |
readOnly | boolean | No | Read-only properties reject value writes (default false) |
const { property } = await client.audience.properties.create({
entityType: "person",
id: "plan",
displayName: "Plan",
datatype: "SINGLE_SELECT",
allowedValues: ["free", "pro", "enterprise"],
});
Returns:
{
property: CustomPropertyDefinition; // same shape as the list items
}
Fails with 409 when the id already exists for the entity type or collides
with a built-in field, and 422 when the id isn't alphanumeric or a select
datatype has fewer than 2 options.
Update a Custom Property
audience.properties.update(options)
PATCH /api/v0/audience/properties/:propertyIdAuth: private key only · scope:
AUDIENCE_PROPERTY_WRITE
Only displayName, readOnly, and (for select datatypes) appending new
options are supported — id and datatype are immutable, and existing
options can't be renamed or removed.
| Param | Type | Required | Description |
|---|---|---|---|
propertyId | string | Yes | The property's id (API name) |
entityType | "person" | "company" | "deal" | Yes | Which entity the property is defined for |
displayName | string | No | New display name |
readOnly | boolean | No | New read-only flag |
addAllowedValues | string[] | No | NEW options to append (select datatypes only; an exact re-add is a no-op, a case-variant of an existing option is rejected) |
const { property } = await client.audience.properties.update({
propertyId: "plan",
entityType: "person",
addAllowedValues: ["enterprise-plus"],
});
Returns:
{
property: CustomPropertyDefinition; // same shape as the list items
}
Fails with 404 when the property doesn't exist for the entity type, and
422 when addAllowedValues targets a non-select datatype.
Common Patterns
User Identification on Signup
async function identifyUser(user) {
const { personId } = await client.audience.people.upsert({
emailAddress: user.email,
firstName: user.firstName,
lastName: user.lastName,
emailSubscriptionStatus: "SUBSCRIBED",
leadScore: user.leadScore ?? 0,
lists: ["customers", "newsletter"],
properties: {
plan: user.subscription,
signupDate: new Date(),
lastLogin: new Date(),
totalPurchases: user.purchaseCount,
},
});
console.log(`User identified: ${personId}`);
}
Contact Form Capture
async function handleContactForm(formData) {
try {
await client.audience.people.upsert({
emailAddress: formData.email,
firstName: formData.firstName,
lastName: formData.lastName,
lists: ["leads"],
properties: {
message: formData.message,
source: "contact-form",
submittedAt: new Date(),
},
});
void client.analytics.event({
eventType: "custom",
customEventId: "contact_form_submitted",
});
} catch (error) {
console.error("Failed to save lead:", error);
}
}
Create contact_form_submitted in Quotient before sending it. See
Custom Events for the complete browser
workflow.
Syncing a List from an External Source
const client = new QuotientServer({ privateKey: "sk_..." });
// Ensure the list exists
await client.audience.lists.upsert({
name: "Active Customers",
description: "Synced from billing system",
});
// Add people in batches of 100
const customers = getActiveCustomers(); // your data source
for (let i = 0; i < customers.length; i += 100) {
const batch = customers.slice(i, i + 100);
await client.audience.lists.addPeople({
listSlug: "active-customers",
people: batch.map((c) => ({
emailAddress: c.email,
firstName: c.firstName,
lastName: c.lastName,
properties: {
plan: c.plan,
mrr: c.mrr,
},
})),
});
}