# Audiences and segments

A contact list with double opt-in, CSV import with a dry run, and segments built from tags, attributes, join date, opens, and clicks.

An audience is a list of people who agreed to hear from you about something. A contact is a person, once, however many lists they are on. A segment is a saved filter over an audience that you can send to. All three are API resources with the same key you already use to send.

## One person is one contact, on several lists

Somebody on your product newsletter, your changelog, and your beta list is one contact row and three memberships, not three copies of an address to keep in step. Billing counts distinct contacts, so being on three lists does not cost three times. Attributes can live on the contact or on the membership: the merge fields for a particular list belong to that membership, and the person's name belongs to them.

## Double opt-in, per workspace or per audience

Turn it on for the workspace and every audience inherits it, or set it explicitly on one audience, including explicitly off. A pending member gets a confirmation link that is an HMAC over the membership, the address, and an expiry, good for 30 days and re-sendable three times. Until they click it their status is pending, and pending is not mailable. A suppressed address is never sent a confirmation at all.

## Import with a dry run that uses the same planner

POST a CSV, up to 8 MB and 10,000 rows a call, and add dry_run to get back exactly what a real import would do: how many contacts it would create, update, leave alone, or hold back, every column with how it was read and three sample values, and a per-line sample with the action and the reason. The preview and the real write run the same planner, so the preview is a prediction rather than an approximation. Unrecognised status values become pending, never subscribed, which is the one place a lenient parser would cost you a spam complaint.

## Segments are a flat list of rules, on purpose

A segment is a match mode of all or any and up to 20 rules over six fields: tag, attribute, status, joined, opened, and clicked. There is no nesting, no boolean groups, and no expression language, because a filter somebody has to parse in their head is a filter that mails the wrong people. Every segment also carries describes, which is the whole filter rendered as one English sentence, so what you are about to send to is legible without reading JSON.

## Subscription topics, so one unsubscribe is not all of them

A topic is a kind of mail rather than a list: product updates, billing notices, the weekly digest. A recipient sets a preference per topic on a hosted preference page, and the resolution order is fixed everywhere in the product: suppression blocks first, a required topic always passes, an explicit preference is obeyed, and silence falls back to the topic's default. Preferences are keyed by address rather than by contact, because a transactional recipient may have no contact row and still deserves a preference.

## The API

| Endpoint | What it does |
| --- | --- |
| `POST /v1/audiences` | Create a list. PATCH sets the double opt-in policy and the confirmation copy. |
| `POST /v1/audiences/:id/contacts` | Import, as JSON or text/csv. Add dry_run to plan without writing. |
| `GET /v1/audiences/:id/contacts?status=` | Page through members by subscription status. |
| `POST /v1/segments` | Save a filter: a match mode and up to 20 rules over one audience. |
| `GET /v1/segments/:id/members` | Keyset-paginated members, optionally only the mailable ones. |
| `GET /v1/contacts/:id/subscriptions` | One view of everything a person has consented to, per address. |
| `GET /v1/contacts/duplicates` | Candidate duplicate contacts, and a POST to merge two of them. |
| `POST /v1/topics/preferences` | Read or set one address’s preference for one topic. |

## A segment, described in English by the API

Trial contacts on the Pro plan who opened something in the last 30 days and are not tagged churned.

```bash
curl -sS https://emails.sh/v1/segments \
  -H "Authorization: Bearer $EMAILSSH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Engaged trials",
    "audience_id": "3f0c9a2e-7b41-4a0d-9c6b-1e2f5a8d4c30",
    "match": "all",
    "rules": [
      { "field": "attribute", "op": "eq", "name": "plan", "value": "pro" },
      { "field": "opened", "op": "within_days", "days": 30 },
      { "field": "tag", "op": "not_has", "value": "churned" }
    ]
  }'

# The response carries member_count and a "describes" string, which is the
# whole filter as one sentence:
#
#   Members matching all of: attribute plan is pro, opened in the last 30
#   days, not tagged churned.
```

## What this does not do

### Segment rules do not nest

One match mode across a flat list. There are no groups, no NOT blocks, and no set operations between segments. Anything shaped like "(A and B) or (C and not D)" has to become two segments and two sends.

### Engagement rules mean opened or clicked anything

The opened and clicked fields read a rolled-up engagement record, so "opened in the last 30 days" is answerable and "opened the March newsletter" is not. There is no per-campaign or per-link segmentation.

### No location, device, timezone, or revenue fields

Segments filter on tags, attributes you set, subscription status, join date, and engagement recency. Anything about where somebody is or what they bought has to be an attribute you write yourself.

### No signup forms or hosted landing pages

There is a hosted preference centre and a hosted confirmation page, and that is the extent of it. The subscribe form is yours to build, and it calls the API.

### No predictive or lookalike segmentation

Nothing here scores or clusters your list. The rules are the rules you wrote.

## Questions

### Does an import create duplicate contacts?

No. An import upserts by address. Within one file the last mention of an address wins, attributes merge rather than replace, and a contact who is already a member is updated rather than added again.

### What happens when I import an address that has bounced before?

It lands with the status cleaned rather than subscribed, and the import response reports it as held back with the reason in prose. Suppression outranks every opt-in in this product, including an explicit one.

### Are contacts the same thing as the recipients of my transactional email?

They can be, and they do not have to be. Transactional sending needs no contact record: you POST an address. Contacts exist for the marketing side, and the suppression list and topic preferences are shared across both.

### How is the marketing side priced?

By contacts stored, in tiers, billed on the highest contact count in the month. The transactional API is priced by emails sent. The two are independent: neither plan requires the other.

### Can I delete a contact entirely?

Yes. Deleting a contact removes the record and its memberships. The suppression list is separate and deliberately survives, because forgetting that somebody complained is not a feature.

## Related

- [Broadcasts](https://emails.sh/features/broadcasts.md)
- [Automations](https://emails.sh/features/automations.md)
- [Deliverability](https://emails.sh/features/deliverability.md)

Docs: https://emails.sh/docs.md