Audience
Manage people, companies, and lists using the Quotient SDK
Overview
The audience API lets you manage people, companies, and lists.
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 and audience.lists.
| Endpoint | API Key | Server SDK | Client SDK |
|---|---|---|---|
| Upsert person | public or private | audience.people.upsert() | audience.people.upsert() |
| 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() | — |
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_WRITE
Creates or updates a person record based on email address.
| Param | Type | Required | Description |
|---|---|---|---|
emailAddress | string | Yes | Primary identifier |
emailMarketingState | "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",
emailMarketingState: "SUBSCRIBED",
leadScore: 50,
lists: ["newsletter", "customers"],
properties: {
plan: "pro",
signupSource: "landing-page",
},
});
Email Marketing States:
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)
}
Companies
Upsert a Company
audience.companies.upsert(params)
POST /api/v0/audience/companiesAuth: public or private key · scope:
AUDIENCE_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)
}
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.
Most list operations require a private API key. The exceptions are
addPeople and removePeople, which also accept public keys so you
can manage list membership from your frontend.
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_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_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_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_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_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_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;
}
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,
emailMarketingState: "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(),
},
});
await client.analytics.event({
eventType: "formSubmit",
formName: "contact",
});
} catch (error) {
console.error("Failed to save lead:", error);
}
}
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,
},
})),
});
}