Skip to content
Developers

Email Marketing

Contacts

Upsert is idempotent on email. Sending the same address again updates that person instead of creating a duplicate.

One email per organization

Each organization stores one contact row per email address. Addresses are compared case-insensitively (stored lowercased). Audiences are lists: the same person can belong to many audiences via separate memberships, but they never get two contact records for the same address.

Typical flow: seed an audience from a CSV in the app, then keep it in sync with the API (or more CSV imports). Overlapping addresses are expected — they merge into the existing contact instead of failing the job.

Re-imports, duplicates, and partial success

Single-contact API — POST /api/v1/email/contacts upserts on email. A repeat address returns 200 and updates fields you send; it does not create a second person. Suppressed addresses return 409 with code SUPPRESSED unless you are recording an opt-out.

Batch API — See Batch sync. Each row succeeds or fails on its own. One invalid row does not roll back the rest of the payload.

CSV in the app — On an audience, Add members → Import CSV accepts up to 200,000 rows and up to 128 MB per file (large files upload directly to object storage with a progress bar; use plain .csv, not Excel .xlsx). After upload you map columns, then the worker processes the file in the background. The import completes even when some rows are skipped; it does not abort because an address is already on the list.

  • New email — Creates the contact and adds them to that audience. Contacts are not capped: a contact list is unbilled until you send to it, and a cap would only block mail you would have paid for.
  • Email already in the organization — Updates the contact where CSV only fills empty name fields; custom attributes from mapped columns are applied. Counted as updated, not as a failure.
  • Already on this audience — Membership is unchanged (skipDuplicates). No error.
  • Duplicate row in the same file — Later rows for the same address are skipped; the first row wins.
  • Invalid email — Row skipped; other rows still import. Up to 1,000 problem rows can be exported in an error report on the job.

Fields

emailRequired. The identity of the contact.
firstNameOptional.
lastNameOptional.
audienceIdOptional on a plain upsert. Required when status is pending.
statussubscribed (default), pending, or unsubscribed.
tagsOptional string array. On create and upsert, tags are merged with tags already stored.
attributesOptional object of custom fields.

Consent

subscribed adds the person as mailable, the same as a manual add in the app. pending needs an audienceId and sends the double opt-in confirmation. They are not mailable until they confirm. unsubscribed records an opt-out so a later import does not revive the address.

Create or update

POST/api/v1/email/contactscontacts:write
curl
curl -sS https://api.thealtermeai.com/api/v1/email/contacts \
  -H "Authorization: Bearer emk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "audienceId": "YOUR_AUDIENCE_ID",
    "status": "subscribed",
    "tags": ["launch"],
    "attributes": { "industry": "media" }
  }'

The body includes contact, created, membershipStatus, and confirmationSent. If the contact is saved but the confirmation email fails, the API returns 502 with code CONFIRMATION_FAILED and still includes the contact.

For many rows, use batch sync.

List, get, edit, delete

GET/api/v1/email/contactscontacts:read

Query with audienceId, status, and q. limit defaults to 50 and caps at 200. offset defaults to 0.

GET/api/v1/email/contacts/{contactId}contacts:read
PATCH/api/v1/email/contacts/{contactId}contacts:write
DELETE/api/v1/email/contacts/{contactId}contacts:write

On PATCH, an explicit tags: [] or attributes: {} clears that field. Omitting the field leaves it unchanged.