Contacts API

Create, update, and query contacts in your workspace. The contacts API supports upsert-by-email semantics, bulk imports, and custom event firing to trigger automations.

The Contact Object

A contact represents a single subscriber in your workspace. Contacts are uniquely identified by email address within a workspace.

json
{
  "id": "clx1a2b3c4d5e6f7g8h9i0j",
  "email": "john@example.com",
  "firstName": "John",
  "lastName": "Smith",
  "status": "active",
  "metadata": {
    "plan": "pro",
    "signupSource": "website",
    "customerId": "cus_stripe_abc123"
  },
  "createdAt": "2026-01-15T10:00:00Z",
  "updatedAt": "2026-01-20T14:30:00Z"
}
FieldTypeDescription
idstringUnique contact identifier (CUID)
emailstringEmail address — unique within a workspace
firstNamestringFirst name (optional)
lastNamestringLast name (optional)
statusenumactive, unsubscribed, bounced, complained, or suppressed
metadataobjectFree-form key-value pairs for storing custom attributes
createdAtstringISO 8601 creation timestamp
updatedAtstringISO 8601 last-update timestamp

Endpoints

All contacts endpoints require the contacts:read or contacts:write scope depending on the operation.

List Contacts

GET/api/v1/contactsscope: contacts:read

Returns a paginated list of contacts in the workspace. Supports filtering by status, search query, and list membership.

Parameters

NameTypeInRequiredDescription
pageintegerqueryoptionalPage number (default: 1)
limitintegerqueryoptionalResults per page, max 100 (default: 50)
statusstringqueryoptionalFilter by status: active, unsubscribed, bounced, complained, suppressed
searchstringqueryoptionalSearch by email, first name, or last name (partial match)
list_idstringqueryoptionalFilter to contacts belonging to a specific list

Response

json
{
  "data": [
    {
      "id": "clx1a2b3c4d5e6f7g8h9i0j",
      "email": "john@example.com",
      "firstName": "John",
      "lastName": "Smith",
      "status": "active",
      "metadata": { "plan": "pro" },
      "createdAt": "2026-01-15T10:00:00Z",
      "updatedAt": "2026-01-20T14:30:00Z"
    },
    {
      "id": "clx9z8y7x6w5v4u3t2s1r0q",
      "email": "jane@example.com",
      "firstName": "Jane",
      "lastName": "Doe",
      "status": "active",
      "metadata": { "plan": "starter" },
      "createdAt": "2026-01-18T09:15:00Z",
      "updatedAt": "2026-01-18T09:15:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 4820,
    "totalPages": 97
  }
}

Upsert Contact

Create or update a contact by email address. If a contact with the given email already exists in the workspace, it is updated. If not, a new contact is created. This is an upsert — it is safe to call repeatedly without creating duplicates.

POST/api/v1/contactsscope: contacts:write

Create or update a contact by email (upsert). Returns the contact object and a created flag indicating whether this was a new contact.

Parameters

NameTypeInRequiredDescription
emailstringbodyrequiredContact email address — used as the unique key for upsert
firstNamestringbodyoptionalContact first name
lastNamestringbodyoptionalContact last name
metadataobjectbodyoptionalFree-form key-value pairs. Merged with existing metadata on update.
listIdstringbodyoptionalAdd the contact to this list after upsert

Request Body

json
{
  "email": "alex@acmecorp.com",
  "firstName": "Alex",
  "lastName": "Rivera",
  "metadata": {
    "plan": "pro",
    "signupSource": "checkout",
    "customerId": "cus_stripe_xyz789"
  },
  "listId": "clx_list_newsletter_id"
}

Response

json
{
  "data": {
    "id": "clxnew1a2b3c4d5e6f7g8h",
    "email": "alex@acmecorp.com",
    "firstName": "Alex",
    "lastName": "Rivera",
    "status": "active",
    "metadata": {
      "plan": "pro",
      "signupSource": "checkout",
      "customerId": "cus_stripe_xyz789"
    },
    "createdAt": "2026-04-22T08:30:00Z",
    "updatedAt": "2026-04-22T08:30:00Z"
  },
  "created": true
}

Bulk Import Contacts

Upsert up to 1,000 contacts in a single API call. All contacts in the array are upserted by email using the same semantics as the single upsert endpoint. Returns a summary with counts of created and updated contacts.

Use bulk for large imports

Bulk import is significantly more efficient than calling the single upsert endpoint in a loop. Each bulk request counts as one API call against your rate limit.
POST/api/v1/contacts/bulkscope: contacts:write

Upsert up to 1,000 contacts in a single request. Returns a summary of created and updated counts.

Parameters

NameTypeInRequiredDescription
contactsarraybodyrequiredArray of contact objects. Each must have at least an email field. Max 1,000 items.
listIdstringbodyoptionalAdd all successfully upserted contacts to this list

Request Body

json
{
  "contacts": [
    {
      "email": "alice@example.com",
      "firstName": "Alice",
      "metadata": { "tier": "enterprise" }
    },
    {
      "email": "bob@example.com",
      "firstName": "Bob",
      "lastName": "Chen",
      "metadata": { "tier": "pro" }
    },
    {
      "email": "carol@example.com",
      "firstName": "Carol"
    }
  ],
  "listId": "clx_list_onboarding_id"
}

Response

json
{
  "data": {
    "processed": 3,
    "created": 2,
    "updated": 1,
    "failed": 0,
    "errors": []
  }
}

Fire Custom Event

Fire a named event against a contact. This triggers any active automations in the workspace that have a Custom Event trigger configured with a matching event name. See the Events API page for full details.

POST/api/v1/contacts/{id}/eventsscope: contacts:write

Fire a named custom event for a contact. Triggers matching CUSTOM_EVENT automations and returns the count of automations that were triggered.

Parameters

NameTypeInRequiredDescription
idstringpathrequiredContact ID
namestringbodyrequiredEvent name (case-insensitive match against automation trigger config)
dataobjectbodyoptionalOptional free-form data payload available to the automation runner

Request Body

json
{
  "name": "purchase_completed",
  "data": {
    "orderId": "ord_abc123",
    "amount": 99.00,
    "currency": "USD",
    "items": ["Pro Plan"]
  }
}

Response

json
{
  "data": {
    "contactId": "clx1a2b3c4d5e6f7g8h9i0j",
    "eventName": "purchase_completed",
    "matchingAutomations": 2,
    "firedAt": "2026-04-22T09:00:00Z"
  }
}

Custom Fields

Custom fields are typed columns you define on your workspace's contact schema (e.g., company, tier, signup_source). Once defined, they can be set on any contact and referenced in templates as {{contact.custom.<key>}} merge tags.

GET/api/v1/custom-fieldsscope: custom_fields:read

List all custom fields defined on the workspace, ordered by their display order. Each field includes its merge-tag form for use in templates.

Response

json
{
  "data": [
    {
      "id": "cf_01hxyz...",
      "key": "company",
      "label": "Company",
      "type": "text",
      "options": null,
      "required": false,
      "placeholder": "Acme Inc.",
      "helpText": null,
      "order": 0,
      "createdAt": "2026-01-15T10:00:00Z",
      "updatedAt": "2026-01-15T10:00:00Z",
      "mergeTag": "{{contact.custom.company}}"
    }
  ]
}
POST/api/v1/custom-fieldsscope: custom_fields:write

Create a new custom field on the workspace. Returns 201 with the created field. Returns 400 if the key already exists.

Parameters

NameTypeInRequiredDescription
keystringbodyrequiredProgrammatic key. Lowercase letters, digits, and underscores only. Must start with a letter. Max 50 chars.
labelstringbodyrequiredHuman-readable label shown in the UI. Max 120 chars.
typestringbodyoptionalOne of: text, number, date, url, boolean, single_select. Defaults to text.
optionsstring[]bodyoptionalRequired when type is single_select. List of allowed values.
requiredbooleanbodyoptionalWhether this field is required when adding a contact via forms.
placeholderstringbodyoptionalPlaceholder text shown in form inputs. Max 200 chars.
helpTextstringbodyoptionalHelp text shown below the field in forms. Max 500 chars.
orderintegerbodyoptionalDisplay order in the UI. Lower values appear first.

Request Body

json
{
  "key": "tier",
  "label": "Customer Tier",
  "type": "single_select",
  "options": ["free", "starter", "pro", "enterprise"],
  "required": false
}

Response

json
{
  "data": {
    "id": "cf_02jyza...",
    "key": "tier",
    "label": "Customer Tier",
    "type": "single_select",
    "options": ["free", "starter", "pro", "enterprise"],
    "required": false,
    "placeholder": null,
    "helpText": null,
    "order": 0,
    "createdAt": "2026-04-27T10:00:00Z",
    "updatedAt": "2026-04-27T10:00:00Z",
    "mergeTag": "{{contact.custom.tier}}"
  }
}

Managing contacts at scale?

Use bulk import for large datasets and fire custom events to drive personalized automation workflows.