Contacts
Manage your subscriber contacts via the API.
Contacts represent individual email subscribers. They can belong to one or more audiences and can be individually suppressed or unsubscribed.
Create contact
POST /v1/contacts
Requires sending_access scope.
Request body
{
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Smith",
"metadata": {"plan": "pro", "source": "signup"}
}
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Contact's email address. |
first_name | string | No | First name. |
last_name | string | No | Last name. |
metadata | Record<string, any> | No | Arbitrary key/value data. |
Response
{
"id": "con_abc123",
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Smith",
"metadata": {"plan": "pro"},
"subscribed": true,
"created_at": "2025-03-12T09:00:00Z"
}
Get contact
GET /v1/contacts/:id
Requires read_only scope.
Get contact by email
GET /v1/contacts/by-email
Requires read_only scope. Looks up a contact by email address, case-insensitively.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
email | string | Contact email address. Required. | |
audience_id | string | Optional audience membership filter. |
Response
Returns the same shape as Get contact.
{
"id": "con_abc123",
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Smith",
"metadata": {"plan": "pro"},
"unsubscribed": false,
"properties": {"plan": "pro"},
"created_at": "2025-03-12T09:00:00Z",
"updated_at": "2025-03-12T09:00:00Z"
}
Returns 404 not_found if no contact with that email exists.
Update contact by email
PATCH /v1/contacts/by-email
Requires sending_access scope. Partially updates a contact identified by email, case-insensitively.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
email | string | Contact email address. Required. |
Request body
All fields are optional; only include fields you want to change.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | No | Updated email address. |
first_name | string | null | No | Updated first name. |
last_name | string | null | No | Updated last name. |
metadata | object | null | No | Updated metadata key/value pairs. |
unsubscribed | boolean | No | Updated unsubscribe status. |
{
"first_name": "Alicia",
"metadata": {"plan": "business"}
}
Response
Returns the same shape as Get contact. The body options match Update contact.
Get contact activity timeline
GET /v1/contacts/:id/activity
Requires read_only scope. Returns the contact's email events (sends, opens, clicks, bounces, and so on), newest first.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | Pagination cursor from the previous response's next_cursor. |
Response
{
"data": [
{
"id": "evt_abc123",
"type": "opened",
"email_id": "email_abc123",
"subject": "Welcome to Acme!",
"from": "Acme <hello@acme.com>",
"recipient": "alice@example.com",
"metadata": {
"user_agent": "Mozilla/5.0...",
"ip": "1.2.3.4"
},
"created_at": "2025-03-12T09:15:30Z"
}
],
"has_more": false,
"next_cursor": null
}
Pagination is cursor-based. Pass next_cursor as the cursor parameter to get the next page.
Get contact property values
GET /v1/contacts/:id/properties
Requires read_only scope. Returns every property definition for the organization alongside this contact's current value for it.
Response
{
"data": [
{
"property_id": "cprop_abc123",
"name": "plan",
"label": "Plan",
"type": "string",
"value": "pro"
}
]
}
value is null when the contact has no value set for that property. type is one of string, number, boolean, or date.
Set contact property values
PUT /v1/contacts/:id/properties
Requires sending_access scope. Upserts the supplied property values for the contact. Property names must already exist as definitions.
Request body
A flat map of property name to value. Values may be a string, number, boolean, or null.
{
"plan": "business",
"seats": 25,
"trial": false
}
Response
{
"updated": 3
}
List contacts
GET /v1/contacts
Requires read_only scope.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Results per page (1–100). |
cursor | string | Pagination cursor. |
Update contact
PUT /v1/contacts/:id
Requires sending_access scope.
Delete contact
DELETE /v1/contacts/:id
Requires full_access scope. Permanently removes the contact.
Bulk delete contacts
DELETE /v1/contacts/bulk
Requires full_access scope. Permanently removes up to 500 contacts in a single request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
ids | string[] | Yes | Contact IDs to delete. Between 1 and 500. |
{
"ids": ["con_abc123", "con_abc124"]
}
Response
{
"deleted": 2
}
Bulk import contacts
POST /v1/contacts/import
Requires full_access scope. Import up to 1,000 contacts in a single request. The import checks your plan's contact limit before inserting — if the batch would exceed your limit, the entire import is rejected.
Request body
{
"contacts": [
{"email": "alice@example.com", "first_name": "Alice"},
{"email": "bob@example.com", "first_name": "Bob"}
]
}
Response
{
"imported": 2,
"skipped": 0,
"errors": []
}
Import contacts from file
POST /v1/contacts/import/file
Requires full_access scope. Uploads a CSV or Excel file and imports up to 1,000 contacts. Existing contacts (matched by email) are updated, new ones are created. Send this request as multipart/form-data.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | .csv, .xlsx, or .xls file. Max 10 MB. |
mapping | string | Yes | JSON object mapping contact field names to file column headers. Must include an email key. |
audience_id | string | No | Audience ID to add all imported contacts to. |
mapping supports email, first_name, last_name, unsubscribed, and arbitrary metadata.<key> entries. For example, {"email": "Email Address", "first_name": "First", "metadata.plan": "Plan"}.
Response
{
"created": 8,
"updated": 2,
"total": 10
}
List property definitions
GET /v1/contacts/properties
Requires read_only scope. Returns every custom contact property defined for the organization.
Response
{
"data": [
{
"id": "cprop_abc123",
"name": "plan",
"label": "Plan",
"type": "string",
"default_value": "free",
"created_at": "2025-03-12T09:00:00Z"
}
]
}
Create property definition
POST /v1/contacts/properties
Requires full_access scope. Defines a new custom contact property. An organization can define up to 50 properties.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Machine-readable key. 1–50 characters, lowercase letters, digits, and underscores only. |
label | string | Yes | Human-readable label. 1–200 characters. |
type | string | Yes | Value type: string, number, boolean, or date. |
default_value | string | No | Default value, serialized as a string. |
Example request
{
"name": "plan",
"label": "Plan",
"type": "string",
"default_value": "free"
}
Response
{
"id": "cprop_abc123",
"name": "plan",
"label": "Plan",
"type": "string",
"default_value": "free",
"created_at": "2025-03-12T09:00:00Z"
}
Returns 409 property_exists if a property with the same name already exists, and 403 property_limit_reached once the organization has 50 properties.
Delete property definition
DELETE /v1/contacts/properties/:propId
Requires full_access scope. Deletes the definition and all associated contact values.
Response
{
"deleted": true
}
Plan limits
| Plan | Contacts |
|---|---|
| Free | 500 |
| Pro | 10,000 |
| Business | 100,000 |
| Enterprise | Unlimited |
Suppression and unsubscribes
Contacts who unsubscribe or hard-bounce are automatically added to the suppression list. Attempting to send to a suppressed address returns an error.
See the suppression endpoints for manual management:
GET /v1/suppression— list suppressed addressesPOST /v1/suppression— add a suppressionDELETE /v1/suppression/:email— remove a suppression
Unsubscribe management:
GET /v1/unsubscribes— list all unsubscribesPOST /v1/unsubscribes— add an unsubscribeDELETE /v1/unsubscribes/:id— remove an unsubscribe