Audience

Contacts are the people you email in campaigns and sequences: one row per address per workspace, with a marketing status, tags, lists and custom fields.

Everything here belongs to the workspace, like the rest of your data. Contacts are separate from inbox mail and from the transactional Emails API. To email them see Campaigns and Sequences; to pick a subset see Segments.

The contact object

FieldTypeDescription
idstringStarts with ctc_.
emailstringLowercased and trimmed. Unique per workspace. At most 254 characters.
first_name, last_namestring | nullAt most 120 characters, no control characters.
statusstringactive, unsubscribed, bounced or complained. Only active contacts receive marketing mail.
sourcestring | nullWhere the contact came from, for example a form or an import.
revenue_centsintegerWhole cents, 0 or more. Filled by billing integrations or set by you.
fieldsobjectValues for your custom fields, keyed by field key.
tags, list_idsstring[]Tag names and list ids the contact belongs to.
created_at, updated_at, unsubscribed_atstringISO 8601.

Add, find and change contacts

curl -X POST "https://api.runearc.com/v1/contacts?upsert=true" \
  -H "Authorization: Bearer $RUNEARC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "ada@example.com", "first_name": "Ada",
        "tags": ["beta"], "fields": { "plan": "pro" } }'

Without ?upsert=true an address that already exists is refused with contact_exists (409). With it the contact is updated and the answer is 200 instead of 201.

RequestBody or queryReturns
GET /v1/contactsq, status, tag, list_id, created_after, created_before, limit 1..200 (default 50), cursorA page with data and next_cursor, newest first.
POST /v1/contactsemail, first_name, last_name, source, revenue_cents, fields, tags, list_ids201 and the contact.
GET /v1/contacts/countsnonetotal, active, unsubscribed, bounced, complained.
GET /v1/contacts/{id}noneThe contact.
PATCH /v1/contacts/{id}first_name, last_name, source, revenue_cents, fieldsThe contact. Only the keys you send change.
DELETE /v1/contacts/{id}none204.
POST /v1/contacts/{id}/statusstatusThe contact. Setting active clears unsubscribed_at.
POST /v1/contacts/{id}/tagstags: string[]The contact. Unknown tags are created.
DELETE /v1/contacts/{id}/tags/{name}noneThe contact.

Custom fields

A custom field has a key (lowercase letters, digits and underscores), a label and a kind: text, number, boolean or date. Values sent in fields are checked against the kind and an unknown key is refused with unknown_field. Use them as variables in email as {{KEY}}; see Email design.

GET/v1/contact-fields

POST/v1/contact-fields

DELETE/v1/contact-fields/{key}

Tags and lists

Tags are free labels with a name. Lists are named groups with an optional description; a contact can be on many. Deleting a list or a tag never deletes contacts.

RequestBodyReturns
GET, POST /v1/tagsnameThe tags; POST returns 201.
DELETE /v1/tags/{id}none204.
GET, POST /v1/listsname, descriptionThe lists; POST returns 201. Duplicate names give list_exists.
GET, PATCH, DELETE /v1/lists/{id}name, descriptionThe list, or 204.
POST /v1/lists/{id}/contactscontact_ids: string[] (at most 10000){ "added": n }.
DELETE /v1/lists/{id}/contacts/{contact_id}none204.

Import a CSV

Upload the file as the raw request body, then start it with a mapping from your columns to contact fields. The header row is required; the delimiter and byte order mark are detected. The upload is stored and previewed first, so nothing is written until you start it.

# 1. Upload the CSV. The answer has the columns, a sample and a suggested mapping.
curl -X POST "https://api.runearc.com/v1/imports?filename=contacts.csv" \
  -H "Authorization: Bearer $RUNEARC_API_KEY" \
  --data-binary @contacts.csv

# 2. Start it with the mapping (202 Accepted; it runs in the background).
curl -X POST https://api.runearc.com/v1/imports/$IMPORT_ID/start \
  -H "Authorization: Bearer $RUNEARC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mapping": { "email": "Email", "first_name": "Name", "fields": { "plan": "Plan" } },
        "tags": ["imported"], "list_ids": [], "update_existing": false }'

# 3. Follow it, then read the rows that failed.
curl https://api.runearc.com/v1/imports/$IMPORT_ID -H "Authorization: Bearer $RUNEARC_API_KEY"
curl "https://api.runearc.com/v1/imports/$IMPORT_ID/errors?format=csv" -H "Authorization: Bearer $RUNEARC_API_KEY"
FieldTypeDescription
mapping.emailstringRequired. The header of the email column.
mapping.first_name, mapping.last_namestringOptional column headers.
mapping.fieldsobjectCustom field key to column header.
tags, list_idsstring[]Applied to every imported contact.
update_existingbooleanMerge into contacts that already exist. Default false: they are skipped.

An import is uploaded, queued, running, done, failed or canceled, and reports total_rows, processed_rows, created_count, updated_count, skipped_count and error_count. Addresses already suppressed are skipped. Up to 25 MB and 500,000 rows per file. GET /v1/imports/{id}/errors lists row_number, email, code and message (up to 10,000 are kept); add ?format=csv for a file you can fix and import again.

RequestBody or queryReturns
POST /v1/imports?filename=, raw CSV body201, the import, columns, sample and suggested_mapping.
GET /v1/importsnoneRecent imports.
GET /v1/imports/{id}noneThe import with progress.
POST /v1/imports/{id}/startmapping, tags, list_ids, update_existing202 and the import.
POST /v1/imports/{id}/cancelnoneThe import.
GET /v1/imports/{id}/errorsafter, limit, format=csvFailed rows.

Bulk operations

Tag, add to a list, unsubscribe or delete many contacts in one background job. The selection is a list of ids (at most 5,000), a list of emails (at most 10,000) or a filter with q, status, tag and list_id. Deleting cannot be undone.

curl -X POST https://api.runearc.com/v1/bulk-operations \
  -H "Authorization: Bearer $RUNEARC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "tag", "tags": ["vip"],
        "selection": { "type": "query", "filter": { "status": "active", "tag": "pro" } } }'
FieldTypeDescription
actionstringtag (with tags), add_to_list (with list_id), unsubscribe or delete.
selection.typestringids, emails or query.

POST /v1/bulk-operations returns 202 and an operation with a status of queued, running, done or failed; poll GET /v1/bulk-operations/{id}. POST /v1/unsubscribes with { "emails": [...] } (at most 10,000) unsubscribes addresses and remembers ones that are not contacts yet.

Unsubscribes and suppression

Every marketing email carries an unsubscribe link and a one click List-Unsubscribe header; using either sets the contact to unsubscribed. Bounces and complaints set bounced or complained. Marketing sends skip every contact that is not active, and the sender-wide suppression list (hard bounces, complaints, manual blocks) applies to all mail; see Sending. Tracking and the frequency cap are in Tracking.

CLI and MCP

runearc contacts add ada@example.com --first-name Ada --tag beta
runearc contacts list --status active --tag beta
runearc contacts tag ada@example.com vip
runearc contacts unsubscribe ada@example.com
runearc contacts fields | tags | field-add plan --kind text
runearc lists create Newsletter
runearc lists add lst_... ada@example.com
runearc import start contacts.csv --tag imported --wait
runearc import status | errors <id> --csv
runearc bulk tag vip --status active   # who: --id, --email, --tag, --in-list, --status, --search
runearc bulk list lst_... --email a@example.com   # also: bulk unsubscribe, bulk delete
runearc unsubscribe a@example.com b@example.com
runearc suppressions list | counts | add <address> | remove <address>

MCP tools: add_contact, list_contacts, update_contact, unsubscribe_contact, tag_contacts, create_list, list_lists, add_to_list, import_contacts, get_import, bulk_contacts, unsubscribe_contacts and list_suppressions.

Errors

CodeStatusMeaning and fix
invalid_email400Not an address. Send one such as ada@example.com.
contact_exists409Already a contact. Use ?upsert=true or PATCH.
invalid_status400Use active, unsubscribed, bounced or complained.
invalid_revenue, invalid_name400Revenue is whole cents, 0 or more. Names are at most 120 characters.
unknown_field, invalid_field_value, invalid_field_key, invalid_field_kind400Create the field first, and send a value of its kind.
field_exists, list_exists409Pick another key or name.
too_many_contacts400At most 10,000 contacts per call.
csv_too_large, too_many_rows, empty_csv, invalid_csv, unknown_column400Fix the file: 25 MB, 500,000 rows, a header row, a mapped email column.
import_already_started, import_finished409The import already ran or ended.
empty_selection, too_many_targets, invalid_tags400Select 1 to 5,000 ids or 10,000 emails; tags must be valid names.
All errors share the shape on Errors: code, message and a fix.

Something unclear or wrong? The API answers every error with a fix, and so should these docs. Back to runearc.com