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
| Required. The identity of the contact. | |
| firstName | Optional. |
| lastName | Optional. |
| audienceId | Optional on a plain upsert. Required when status is pending. |
| status | subscribed (default), pending, or unsubscribed. |
| tags | Optional string array. On create and upsert, tags are merged with tags already stored. |
| attributes | Optional 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
/api/v1/email/contactscontacts:writecurl -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
/api/v1/email/contactscontacts:readQuery with audienceId, status, and q. limit defaults to 50 and caps at 200. offset defaults to 0.
/api/v1/email/contacts/{contactId}contacts:read/api/v1/email/contacts/{contactId}contacts:write/api/v1/email/contacts/{contactId}contacts:writeOn PATCH, an explicit tags: [] or attributes: {} clears that field. Omitting the field leaves it unchanged.