Contacts API

Store subscribers, subscription state, custom properties, and manual segments for broadcast targeting.

Contact Object

json
{
  "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
}
POST/v1/contacts

Create 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

json
{
  "email": "[email protected]",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false,
  "segment_ids": ["seg_product"],
  "properties": { "company": "Acme" }
}

Response

json
{
  "object": "contact",
  "id": "clx_contact123"
}
bash
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.

GET/v1/contact-properties

List the properties this team has declared.

POST/v1/contact-properties

Declare a property. Returns 409 if the key is already declared.

Parameters

ParameterTypeRequiredDescription
keystringYes1 to 64 characters: letters, digits, underscore, dot or hyphen. Cannot be a built-in tag such as first_name.
typestringYesstring or number
fallback_valuestring | numberNoUsed when a contact has no value and the template gives no inline default. Must match the type.

Request Body

json
{
  "key": "company",
  "type": "string",
  "fallback_value": "your company"
}
GET/v1/contact-properties/:id

Retrieve one declared property.

PATCH/v1/contact-properties/:id

Change 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

ParameterTypeRequiredDescription
fallback_valuestring | numberNoSend null to remove the default.
DELETE/v1/contact-properties/:id

Remove 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.

POST/v1/contacts

Import 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

json
{
  "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

json
{
  "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 contacts array. Batch email sending is the opposite: POST /v1/emails/batch takes 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_ids only adds memberships; it never removes any.
  • "unsubscribed": false clears the contact's unsubscribed flag. It does not undo a recipient's own opt-out: an unsubscribe link also adds a suppression with reason unsubscribe, and that keeps blocking marketing email until an owner or admin removes it. Leave unsubscribed out of a routine sync unless you mean to change it.
  • A contact with "unsubscribed": true gets 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 suppression with reason: "unsubscribe" and the time in created_at. Its scope says what it blocks: marketing for new opt-outs, which still let email labelled transactional through, and all for opt-outs recorded before marketing-only unsubscribes existed, which block every email. See suppression scope.
  • The bulk form does not fire contact.created or contact.updated webhooks. See contact events for what does.
GET/v1/contacts

Returns 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

ParameterTypeRequiredDescription
qstringNoSearch by email, first name, or last name
statusstringNoall, subscribed, or unsubscribed
segment_idstringNoReturn only contacts assigned to this manual segment
disposablebooleanNotrue returns only contacts on throwaway-mailbox domains, false only the rest
limitnumberNoContacts per page, 1 to 100, default 50
pagenumberNoPage number, starting at 1. The contacts list pages by number, not by cursor; has_more says whether another page exists.

Response

json
{
  "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"
}
GET/v1/segments

List manual contact segments for the current account. The response includes the plan limit so dashboards can show quota clearly.

Response

json
{
  "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"
}
POST/v1/segments

Create 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

json
{
  "name": "Product updates",
  "description": "Customers who want release notes",
  "color": "#10b981"
}

Response

json
{
  "object": "segment",
  "id": "seg_product",
  "name": "Product updates",
  "contact_count": 0
}
GET/v1/segments/:id/contacts

List contacts assigned to a manual segment. Pagination uses the same page and limit parameters as the contacts list.

GET/v1/contacts/export?format=csv

Export up to 10,000 contacts as CSV or JSON. Pass status=subscribed, status=unsubscribed or disposable=true to export a filtered list.

GET/v1/contacts/:id

Retrieve a contact by ID or URL-encoded email address.

PATCH/v1/contacts/:id

Update name fields, subscription status, custom properties, or segment membership. Email addresses are immutable.

Request Body

json
{
  "unsubscribed": true,
  "segment_ids": ["seg_product"]
}

Response

json
{
  "object": "contact",
  "id": "clx_contact123"
}
bash
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"] }'
DELETE/v1/contacts/:id

Delete a contact by ID or URL-encoded email address.

Response

json
{
  "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.

GET/v1/contacts/{id}/segments

The segments one contact belongs to.

POST/v1/contacts/{id}/segments/{segment_id}

Add a contact to a segment. Same as Resend.

Response

json
{ "id": "seg_..." }
DELETE/v1/contacts/{id}/segments/{segment_id}

Remove a contact from a segment. The contact stays in the audience.

Response

json
{ "object": "contact_segment", "id": "seg_...", "deleted": true }
DELETE/v1/contacts

Delete up to 500 contacts by id, or the whole audience.

Parameters

ParameterTypeRequiredDescription
idsstring[]NoUp to 500 contact ids
confirmstringNoall, to delete every contact (owners and admins only)
statusstringNoWith confirm all: subscribed or unsubscribed only

Request Body

json
{ "ids": ["con_1", "con_2"] }

Response

json
{ "deleted": 2 }