Contacts, lists and segments

Marketing contacts with typed properties, private and public lists, live filter segments, companies, and CSV import and export, compatible with Resend contacts.

A contact is one email address in a project. Lists are groups contacts join or leave; segments are saved filters that always reflect the current data. Campaigns go to lists, a segment, or both.

Contacts

POST /contacts creates a contact. The address is the contact's identity: it is stored lowercased, and every contact path accepts the ct_… id or the address.

curl https://api.stamp0.com/contacts \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "maya@northwind.dev",
    "first_name": "Maya",
    "properties": { "plan": "pro", "seats": 12 },
    "lists": [{ "id": "lst_…" }]
  }'
  • Creating an address that exists answers 409. POST /contacts/batch creates or updates up to 1,000 contacts by address, each succeeding or failing on its own.
  • PATCH /contacts/:id merges properties into the stored ones; a null value removes that property.
  • unsubscribed: true opts the contact out of all marketing email. List opt-outs are per list (below).
  • A lists entry without a status (and Resend segments and audience_id, and adding a contact to a segment or audience) subscribes the contact unless they opted out of that list or are waiting to confirm a double opt-in: a sync never resubscribes someone who unsubscribed. Re-subscribing someone who opted out ("status": "subscribed" over an unsubscribed membership, a topic back to opt_in, or "unsubscribed": false on an unsubscribed contact) is refused with a 403 for API keys and MCP clients: only the contact (preference center, a double opt-in confirmation) or an owner or admin in the dashboard can do it. To ask the contact to come back, add them to a double opt-in list as pending; they get a confirmation email.
  • external_id holds your own user ID. source records where the contact came from: api, dashboard, import, form or sequence.
  • GET /contacts lists newest first; search matches an exact address or part of an address or name, and list_id narrows to a list's members.
  • GET /contacts/:id/activity is the contact's timeline: emails sent to it, custom events, list changes and its creation.

Every contact counts toward the plan's contact limit (see plan limits).

Properties

Properties are typed custom fields. Define them before you set them:

curl https://api.stamp0.com/properties \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "plan", "type": "string", "fallback_value": "free" }'
  • Types are string, number, boolean and date (dates are stored as ISO 8601 UTC). The key and type cannot change once created.
  • Keys use letters, digits and _, start with a letter, and cannot shadow a built-in field such as email or first_name.
  • A contact holds up to 100 property values; strings up to 2,000 characters.
  • In templates a property is {{ contact.plan }}; fallback_value fills it when a contact has no value.
  • Deleting a property removes its values from every contact.

/contact-properties is the same resource on Resend's path.

Lists

A list is a group contacts belong to, with a status per member: subscribed, unsubscribed or pending (waiting for double opt-in).

curl https://api.stamp0.com/lists \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Product updates", "visibility": "public", "double_opt_in": true }'
FieldValues
visibilityprivate (default): you manage membership. public: shown in the preference center, where contacts subscribe and unsubscribe themselves
default_subscriptionopt_in (default): only members who subscribed receive it. opt_out (public lists only): every contact counts as subscribed until they opt out
double_opt_inNew members stay pending until they click the confirmation email
  • POST /lists/:id/contacts adds up to 1,000 contacts by id or address, creating the ones that do not exist. Addresses are checked like POST /contacts: one that is not a bare address of at most 254 characters refuses the whole request (422), and an unknown id is reported per item. On a double opt-in list (or with status: "pending") a confirmation email goes out from the project's default_from.
  • DELETE /lists/:id/contacts/:contact removes a member. On an opt_out list it records an opt-out instead, since no row there means subscribed.
  • PATCH /contacts/:id/lists sets a contact's status on several lists at once; GET /contacts/:id/lists reads them.

Public lists replace Resend's topics, and private lists are Resend's manually managed segments and legacy audiences. See Resend aliases.

Segments

A segment is a filter, not a membership list: it matches whoever fits the filter at the moment it is used. A campaign resolves it when sending starts; a sequence triggered by segment_entered checks it every few minutes.

curl https://api.stamp0.com/segments \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Active Pro customers",
    "filter": {
      "and": [
        { "type": "property", "key": "plan", "operator": "eq", "value": "pro" },
        { "type": "event", "name": "app.opened", "operator": "performed", "within_days": 30 },
        { "not": { "type": "list", "list_id": "lst_…", "operator": "in" } }
      ]
    }
  }'

A filter is a tree of and, or and not groups over conditions:

ConditionTestsOperators
contactemail, email_domain, first_name, last_name, source, unsubscribed, created_at, updated_atBy type, below
propertyA custom property, by keyBy the property's type
listMembership of list_id (with status, default subscribed)in, not_in
companyAny of the contact's companies: field (name, domain) or a company property keyBy value
eventA custom event name, optionally count times and within_daysperformed, not_performed
TypeOperators
Stringeq, neq, contains, not_contains, starts_with, ends_with, in, not_in, is_set, is_not_set (case-insensitive)
Numbereq, neq, gt, gte, lt, lte, between, is_set, is_not_set
Booleaneq, is_set, is_not_set
Datebefore, after, between, within_last_days, more_than_days_ago, is_set, is_not_set
  • Negative operators (neq, not_contains, not_in) also match contacts without a value.
  • Filters nest up to 6 levels with up to 50 conditions; in lists hold up to 100 values. { "and": [] } matches every contact.
  • POST /segments/preview counts a filter's matches and returns a sample, without saving it. GET /segments/:id/preview counts a saved segment now (and a sample); the contacts_count on the segment is cached. GET /segments/:id/contacts lists a segment's contacts, paged like GET /contacts (Resend's contacts.list({ segmentId })).

/segments also serves Resend's manually managed segments, which are private lists: a create without a filter makes a list (lst_…), and GET /segments returns both kinds with kind: "filter" or "manual".

Companies

Companies group contacts for B2B sending: name, domain, external_id and their own properties. Attach contacts with POST /companies/:id/contacts (by id or address, with an optional role), and filter on company fields in segments.

POST /group is the Segment-style call: it upserts the company by group_id (its external_id) with traits, and attaches the contact named by email or contact_id.

curl https://api.stamp0.com/group \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "group_id": "org_42", "traits": { "name": "Northwind", "domain": "northwind.dev", "seats": 40 }, "email": "maya@northwind.dev", "role": "admin" }'

Import and export

Import a CSV of up to 50 MB by sending the file as the request body. consent is required: a short statement of how these people agreed to receive marketing email, kept with the import.

curl "https://api.stamp0.com/contacts/imports?consent=Signed%20up%20at%20acme.co&list_ids=lst_…" \
  -H "Authorization: Bearer $STAMP0_API_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @contacts.csv
  • The file needs an email column (email, Email Address, mail). first_name, last_name, unsubscribed, external_id and columns named like a property key are mapped; anything else is listed as ignored.
  • Commas, semicolons and tabs are detected. Existing contacts are updated unless update_existing=false; empty cells never clear stored values.
  • Imports run in the background. Poll GET /contacts/imports/:id for counters, read refused rows at /contacts/imports/:id/errors (or /errors/download as CSV), and stop one with /cancel.

GET /contacts/export streams every contact as CSV, one column per property; pass list_id or segment_id to export a list's members or a segment's matches. Text cells are always quoted, and text a spreadsheet could run as a formula (starting with =, +, - or @, also after a ;, tab or line break inside the cell) gets a ' in front of it; numbers and booleans are written as is.

Reference: Contacts, Properties, Lists, Segments, Companies.

On this page