Logo

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.

EndpointAPI KeyServer SDKClient SDK
Upsert personpublic or privateaudience.people.upsert()audience.people.upsert()
Upsert companypublic or privateaudience.companies.upsert()
Upsert listprivate onlyaudience.lists.upsert()
List all listsprivate onlyaudience.lists.list()
Get a listprivate onlyaudience.lists.get()
List people in a listprivate onlyaudience.lists.listPeople()
Add people to a listprivate onlyaudience.lists.addPeople()
Remove people from a listprivate onlyaudience.lists.removePeople()
List custom propertiesprivate onlyaudience.properties.list()
Create custom propertyprivate onlyaudience.properties.create()
Update custom propertyprivate onlyaudience.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/people

Auth: public or private key · scope: AUDIENCE_PERSON_WRITE

Creates or updates a person record based on email address.

ParamTypeRequiredDescription
emailAddressstringYesPrimary identifier
emailSubscriptionStatus"SUBSCRIBED" | "UNSUBSCRIBED"NoMarketing email opt-in state
firstNamestringNoFirst name
lastNamestringNoLast name
jobTitlestringNoJob title
leadScorenumberNoInteger lead score (default 0)
listsstring[]NoList slugs to add the person to
propertiesRecord<string, string | number | boolean | Date>NoCustom 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 emails
  • UNSUBSCRIBED - 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/companies

Auth: public or private key · scope: AUDIENCE_COMPANY_WRITE

Creates or updates a company record keyed on domain.

ParamTypeRequiredDescription
domainstringYesCompany domain (upsert key)
namestringNoCompany name
descriptionstringNoDescription
industriesstring[]NoIndustry tags
totalEmployeesnumberNoEmployee count
address1stringNoStreet address
citystringNoCity
regionCodestringNoState/region code
countrystringNoCountry code
zipstringNoPostal code
socialLinkLinkedInstringNoLinkedIn URL
propertiesRecord<string, string | number | boolean | Date>NoCustom 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/lists

Auth: 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.

ParamTypeRequiredDescription
namestringOne of name or slugList name (slug auto-derived if slug omitted)
slugstringOne of name or slugExplicit slug to target
descriptionstringNoList 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:

InputLookup keyNot foundFound
{ name }generateSlug(name)Create with name + generated slugUpdate name
{ name, description }generateSlug(name)Create with bothUpdate name + description
{ slug }slugCreate (name defaults to slug)No-op
{ slug, name }slugCreate with name + slugUpdate name
{ slug, description }slugCreate (name defaults to slug)Update description
{ slug, name, description }slugCreate with all fieldsUpdate name + description

Returns:

{
  listId: string;
  name: string;
  slug: string;
}

List All Lists

audience.lists.list(options?)

GET /api/v0/audience/lists

Auth: private key only · scope: AUDIENCE_LIST_READ

ParamTypeRequiredDescription
searchstringNoFilter by name
pagenumberNoPage number (default 1)
limitnumberNoResults 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

ParamTypeRequiredDescription
slugstringYesList 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}/people

Auth: private key only · scope: AUDIENCE_LIST_READ

ParamTypeRequiredDescription
listSlugstringYesList slug
searchstringNoFilter by email
pagenumberNoPage number (default 1)
limitnumberNoResults 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}/people

Auth: 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.

ParamTypeRequiredDescription
listSlugstringYesList slug
peopleAddPersonEntry[]YesArray of people to add (max 100)

Each entry in people is one of:

ParamTypeRequiredDescription
personIdstringYes (if no email)Existing person ID
emailAddressstringYes (if no ID)Email to upsert
firstNamestringNoFirst name (with email only)
lastNamestringNoLast name (with email only)
jobTitlestringNoJob title (with email only)
leadScorenumberNoLead score (with email only)
propertiesRecord<string, ...>NoCustom 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}/people

Auth: 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.

ParamTypeRequiredDescription
listSlugstringYesList slug
peopleRemovePersonEntry[]YesArray of people to remove (max 100)

Each entry in people is one of:

ParamTypeDescription
personIdstringExisting person ID
emailAddressstringPerson'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/properties

Auth: private key only · scope: AUDIENCE_PROPERTY_READ

ParamTypeRequiredDescription
entityType"person" | "company" | "deal"YesWhich 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/properties

Auth: private key only · scope: AUDIENCE_PROPERTY_WRITE

ParamTypeRequiredDescription
entityType"person" | "company" | "deal"YesWhich entity the property is defined for
idstringYesAPI name — alphanumeric, unique per entity type, immutable
displayNamestringYesHuman-readable name shown in the UI
datatypestringYesValue type (STRING, NUMBER, BOOLEAN, DATE, SINGLE_SELECT, ...) — immutable
allowedValuesstring[]Yes (select datatypes)Allowed options — at least 2; forbidden for other datatypes
readOnlybooleanNoRead-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/:propertyId

Auth: 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.

ParamTypeRequiredDescription
propertyIdstringYesThe property's id (API name)
entityType"person" | "company" | "deal"YesWhich entity the property is defined for
displayNamestringNoNew display name
readOnlybooleanNoNew read-only flag
addAllowedValuesstring[]NoNEW 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(),
      },
    });

    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,
      },
    })),
  });
}

Next Steps