Contacts API
Store subscribers, subscription state, custom properties, and manual segments for broadcast targeting.
Contact Object
{
"object": "contact",
"id": "clx_contact123",
"email": "[email protected]",
"first_name": "Steve",
"last_name": "Wozniak",
"created_at": "2026-04-30T12:00:00.000Z",
"unsubscribed": false,
"disposable": false,
"suppressed": false,
"properties": {
"company": "Acme",
"plan": "pro"
},
"segments": [
{ "id": "seg_product", "name": "Product updates", "color": "#10b981" }
],
"suppression": null
}/v1/contactsCreate one contact. Returns 409 if the email already exists: change an existing contact with PATCH /v1/contacts/:id, or send the bulk form below, which upserts. Name fields can be sent as first_name or firstName (Resend style); responses always use snake_case.
Request Body
{
"email": "[email protected]",
"first_name": "Steve",
"last_name": "Wozniak",
"unsubscribed": false,
"segment_ids": ["seg_product"],
"properties": { "company": "Acme" }
}Response
{
"object": "contact",
"id": "clx_contact123"
}curl -X POST https://smtpfa.st/api/v1/contacts \
-H "Authorization: Bearer sf_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Steve",
"last_name": "Wozniak",
"unsubscribed": false,
"segment_ids": ["seg_product"],
"properties": { "company": "Acme" }
}'Contact properties
A contact can carry any custom fields you like in properties, and you can use them in a broadcast as {{company}}. Declaring a property is optional and does two things: it gives the field a default used whenever a contact has no value, so you set it once instead of writing {{company|Acme}} in every template, and it lets the broadcast editor tell you when a merge tag matches nothing, which is usually a typo that would otherwise render as empty text.
Undeclared properties keep working exactly as before. Declaring a property as a number means later writes to that key must be numbers; other keys are unaffected.
A value is resolved in this order: the contact's own value, then an inline default written in the template, then the declared default, then empty. So {{company|}} is a deliberate way to show nothing.
/v1/contact-propertiesList the properties this team has declared.
/v1/contact-propertiesDeclare a property. Returns 409 if the key is already declared.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| key | string | Yes | 1 to 64 characters: letters, digits, underscore, dot or hyphen. Cannot be a built-in tag such as first_name. |
| type | string | Yes | string or number |
| fallback_value | string | number | No | Used when a contact has no value and the template gives no inline default. Must match the type. |
Request Body
{
"key": "company",
"type": "string",
"fallback_value": "your company"
}/v1/contact-properties/:idRetrieve one declared property.
/v1/contact-properties/:idChange the default. The key and type cannot be changed: renaming would orphan the values already stored under the old key. Delete and re-declare instead.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| fallback_value | string | number | No | Send null to remove the default. |
/v1/contact-properties/:idRemove the declaration. Contact values are left alone, so re-declaring the key finds them again.
Segments
A segment is a named list of contacts you can target a broadcast at. Membership is manual: add and remove contacts through the endpoints below, or by passing segment_ids when you create or update a contact.
/v1/contactsImport or upsert up to 500 contacts at once by sending a contacts array. Existing contacts are matched by email and updated in place.
Request Body
{
"contacts": [
{
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"properties": { "plan": "growth" }
},
{
"email": "[email protected]",
"first_name": "Grace",
"last_name": "Hopper",
"unsubscribed": true
}
],
"segment_ids": ["seg_product"]
}Response
{
"object": "list",
"has_more": false,
"data": [
{
"id": "clx_contact123",
"email": "[email protected]",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2026-04-30T12:00:00.000Z",
"unsubscribed": false,
"disposable": false,
"suppressed": false
}
]
}How the upsert treats existing contacts
- The body is an object with a
contactsarray. Batch email sending is the opposite:POST /v1/emails/batchtakes a bare JSON array. - A contact that exists is matched by email. Fields you send replace the stored ones; fields you leave out stay as they are.
segment_idsonly adds memberships; it never removes any. "unsubscribed": falseclears the contact's unsubscribed flag. It does not undo a recipient's own opt-out: an unsubscribe link also adds a suppression with reasonunsubscribe, and that keeps blocking marketing email until an owner or admin removes it. Leaveunsubscribedout of a routine sync unless you mean to change it.- A contact with
"unsubscribed": truegets no marketing email, from a broadcast or sent through the API or SMTP. Transactional email (category: "transactional") still reaches it. - To tell a recipient's own opt-out from a change made through the API, read the contact: a self-unsubscribe shows as
suppressionwithreason: "unsubscribe"and the time increated_at. Itsscopesays what it blocks:marketingfor new opt-outs, which still let email labelled transactional through, andallfor opt-outs recorded before marketing-only unsubscribes existed, which block every email. See suppression scope. - The bulk form does not fire
contact.createdorcontact.updatedwebhooks. See contact events for what does.
/v1/contactsReturns contacts in newest-first order. Use optional q, status, segment_id, disposable, and limit query parameters for dashboard-style filtering. Every contact carries a disposable flag, true when the address is on a throwaway-mailbox domain.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | No | Search by email, first name, or last name |
| status | string | No | all, subscribed, or unsubscribed |
| segment_id | string | No | Return only contacts assigned to this manual segment |
| disposable | boolean | No | true returns only contacts on throwaway-mailbox domains, false only the rest |
| limit | number | No | Contacts per page, 1 to 100, default 50 |
| page | number | No | Page number, starting at 1. The contacts list pages by number, not by cursor; has_more says whether another page exists. |
Response
{
"object": "list",
"has_more": true,
"data": [
{
"id": "clx_contact123",
"email": "[email protected]",
"first_name": "Steve",
"last_name": "Wozniak",
"created_at": "2026-04-30T12:00:00.000Z",
"unsubscribed": false,
"disposable": false,
"suppressed": false,
"properties": { "company": "Acme" },
"segments": [],
"suppression": null
}
],
"page": 1,
"page_size": 50,
"filtered_total": 1240,
"total": 1240,
"subscribed": 1198,
"unsubscribed": 42,
"disposable": 3,
"new_7d": 18,
"new_30d": 77,
"contact_limit": 25000,
"contact_limit_base": 25000,
"tier": "pro"
}/v1/segmentsList manual contact segments for the current account. The response includes the plan limit so dashboards can show quota clearly.
Response
{
"object": "list",
"data": [
{
"id": "seg_product",
"name": "Product updates",
"description": "Customers who want release notes",
"color": "#10b981",
"contact_count": 128,
"created_at": "2026-05-05T12:00:00.000Z"
}
],
"has_more": false,
"total": 1,
"segment_limit": 10,
"tier": "starter"
}/v1/segmentsCreate a manual segment for organizing contacts and targeting broadcasts. Free accounts include 2 segments, Starter includes 10, Growth includes 50, and Scale includes 200.
Request Body
{
"name": "Product updates",
"description": "Customers who want release notes",
"color": "#10b981"
}Response
{
"object": "segment",
"id": "seg_product",
"name": "Product updates",
"contact_count": 0
}/v1/segments/:id/contactsList contacts assigned to a manual segment. Pagination uses the same page and limit parameters as the contacts list.
/v1/contacts/export?format=csvExport up to 10,000 contacts as CSV or JSON. Pass status=subscribed, status=unsubscribed or disposable=true to export a filtered list.
/v1/contacts/:idRetrieve a contact by ID or URL-encoded email address.
/v1/contacts/:idUpdate name fields, subscription status, custom properties, or segment membership. Email addresses are immutable.
Request Body
{
"unsubscribed": true,
"segment_ids": ["seg_product"]
}Response
{
"object": "contact",
"id": "clx_contact123"
}curl -X PATCH https://smtpfa.st/api/v1/contacts/steve%40example.com \
-H "Authorization: Bearer sf_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "unsubscribed": true, "segment_ids": ["seg_product"] }'/v1/contacts/:idDelete a contact by ID or URL-encoded email address.
Response
{
"object": "contact",
"contact": "clx_contact123",
"deleted": true
}Segments of a contact and bulk delete
Name a contact by id or by email address. Adding a contact to a segment it is already in is not an error.
/v1/contacts/{id}/segmentsThe segments one contact belongs to.
/v1/contacts/{id}/segments/{segment_id}Add a contact to a segment. Same as Resend.
Response
{ "id": "seg_..." }/v1/contacts/{id}/segments/{segment_id}Remove a contact from a segment. The contact stays in the audience.
Response
{ "object": "contact_segment", "id": "seg_...", "deleted": true }/v1/contactsDelete up to 500 contacts by id, or the whole audience.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string[] | No | Up to 500 contact ids |
| confirm | string | No | all, to delete every contact (owners and admins only) |
| status | string | No | With confirm all: subscribed or unsubscribed only |
Request Body
{ "ids": ["con_1", "con_2"] }Response
{ "deleted": 2 }