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.
{
"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"
}| Field | Type | Description |
|---|---|---|
| id | string | Unique contact identifier (CUID) |
| string | Email address — unique within a workspace | |
| firstName | string | First name (optional) |
| lastName | string | Last name (optional) |
| status | enum | active, unsubscribed, bounced, complained, or suppressed |
| metadata | object | Free-form key-value pairs for storing custom attributes |
| createdAt | string | ISO 8601 creation timestamp |
| updatedAt | string | ISO 8601 last-update timestamp |
Endpoints
All contacts endpoints require the contacts:read or contacts:write scope depending on the operation.
List Contacts
/api/v1/contactsscope: contacts:readReturns a paginated list of contacts in the workspace. Supports filtering by status, search query, and list membership.
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
| page | integer | query | optional | Page number (default: 1) |
| limit | integer | query | optional | Results per page, max 100 (default: 50) |
| status | string | query | optional | Filter by status: active, unsubscribed, bounced, complained, suppressed |
| search | string | query | optional | Search by email, first name, or last name (partial match) |
| list_id | string | query | optional | Filter to contacts belonging to a specific list |
Response
{
"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.
/api/v1/contactsscope: contacts:writeCreate or update a contact by email (upsert). Returns the contact object and a created flag indicating whether this was a new contact.
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
| string | body | required | Contact email address — used as the unique key for upsert | |
| firstName | string | body | optional | Contact first name |
| lastName | string | body | optional | Contact last name |
| metadata | object | body | optional | Free-form key-value pairs. Merged with existing metadata on update. |
| listId | string | body | optional | Add the contact to this list after upsert |
Request Body
{
"email": "alex@acmecorp.com",
"firstName": "Alex",
"lastName": "Rivera",
"metadata": {
"plan": "pro",
"signupSource": "checkout",
"customerId": "cus_stripe_xyz789"
},
"listId": "clx_list_newsletter_id"
}Response
{
"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
/api/v1/contacts/bulkscope: contacts:writeUpsert up to 1,000 contacts in a single request. Returns a summary of created and updated counts.
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
| contacts | array | body | required | Array of contact objects. Each must have at least an email field. Max 1,000 items. |
| listId | string | body | optional | Add all successfully upserted contacts to this list |
Request Body
{
"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
{
"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.
/api/v1/contacts/{id}/eventsscope: contacts:writeFire a named custom event for a contact. Triggers matching CUSTOM_EVENT automations and returns the count of automations that were triggered.
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
| id | string | path | required | Contact ID |
| name | string | body | required | Event name (case-insensitive match against automation trigger config) |
| data | object | body | optional | Optional free-form data payload available to the automation runner |
Request Body
{
"name": "purchase_completed",
"data": {
"orderId": "ord_abc123",
"amount": 99.00,
"currency": "USD",
"items": ["Pro Plan"]
}
}Response
{
"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.
/api/v1/custom-fieldsscope: custom_fields:readList all custom fields defined on the workspace, ordered by their display order. Each field includes its merge-tag form for use in templates.
Response
{
"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}}"
}
]
}/api/v1/custom-fieldsscope: custom_fields:writeCreate a new custom field on the workspace. Returns 201 with the created field. Returns 400 if the key already exists.
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
| key | string | body | required | Programmatic key. Lowercase letters, digits, and underscores only. Must start with a letter. Max 50 chars. |
| label | string | body | required | Human-readable label shown in the UI. Max 120 chars. |
| type | string | body | optional | One of: text, number, date, url, boolean, single_select. Defaults to text. |
| options | string[] | body | optional | Required when type is single_select. List of allowed values. |
| required | boolean | body | optional | Whether this field is required when adding a contact via forms. |
| placeholder | string | body | optional | Placeholder text shown in form inputs. Max 200 chars. |
| helpText | string | body | optional | Help text shown below the field in forms. Max 500 chars. |
| order | integer | body | optional | Display order in the UI. Lower values appear first. |
Request Body
{
"key": "tier",
"label": "Customer Tier",
"type": "single_select",
"options": ["free", "starter", "pro", "enterprise"],
"required": false
}Response
{
"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.