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
| Field | Type | Description |
|---|---|---|
| id | string | Starts with ctc_. |
| string | Lowercased and trimmed. Unique per workspace. At most 254 characters. | |
| first_name, last_name | string | null | At most 120 characters, no control characters. |
| status | string | active, unsubscribed, bounced or complained. Only active contacts receive marketing mail. |
| source | string | null | Where the contact came from, for example a form or an import. |
| revenue_cents | integer | Whole cents, 0 or more. Filled by billing integrations or set by you. |
| fields | object | Values for your custom fields, keyed by field key. |
| tags, list_ids | string[] | Tag names and list ids the contact belongs to. |
| created_at, updated_at, unsubscribed_at | string | ISO 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.
| Request | Body or query | Returns |
|---|---|---|
| GET /v1/contacts | q, status, tag, list_id, created_after, created_before, limit 1..200 (default 50), cursor | A page with data and next_cursor, newest first. |
| POST /v1/contacts | email, first_name, last_name, source, revenue_cents, fields, tags, list_ids | 201 and the contact. |
| GET /v1/contacts/counts | none | total, active, unsubscribed, bounced, complained. |
| GET /v1/contacts/{id} | none | The contact. |
| PATCH /v1/contacts/{id} | first_name, last_name, source, revenue_cents, fields | The contact. Only the keys you send change. |
| DELETE /v1/contacts/{id} | none | 204. |
| POST /v1/contacts/{id}/status | status | The contact. Setting active clears unsubscribed_at. |
| POST /v1/contacts/{id}/tags | tags: string[] | The contact. Unknown tags are created. |
| DELETE /v1/contacts/{id}/tags/{name} | none | The 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.
| Request | Body | Returns |
|---|---|---|
| GET, POST /v1/tags | name | The tags; POST returns 201. |
| DELETE /v1/tags/{id} | none | 204. |
| GET, POST /v1/lists | name, description | The lists; POST returns 201. Duplicate names give list_exists. |
| GET, PATCH, DELETE /v1/lists/{id} | name, description | The list, or 204. |
| POST /v1/lists/{id}/contacts | contact_ids: string[] (at most 10000) | { "added": n }. |
| DELETE /v1/lists/{id}/contacts/{contact_id} | none | 204. |
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"| Field | Type | Description |
|---|---|---|
| mapping.email | string | Required. The header of the email column. |
| mapping.first_name, mapping.last_name | string | Optional column headers. |
| mapping.fields | object | Custom field key to column header. |
| tags, list_ids | string[] | Applied to every imported contact. |
| update_existing | boolean | Merge 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.
| Request | Body or query | Returns |
|---|---|---|
| POST /v1/imports | ?filename=, raw CSV body | 201, the import, columns, sample and suggested_mapping. |
| GET /v1/imports | none | Recent imports. |
| GET /v1/imports/{id} | none | The import with progress. |
| POST /v1/imports/{id}/start | mapping, tags, list_ids, update_existing | 202 and the import. |
| POST /v1/imports/{id}/cancel | none | The import. |
| GET /v1/imports/{id}/errors | after, limit, format=csv | Failed 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" } } }'| Field | Type | Description |
|---|---|---|
| action | string | tag (with tags), add_to_list (with list_id), unsubscribe or delete. |
| selection.type | string | ids, 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
| Code | Status | Meaning and fix |
|---|---|---|
| invalid_email | 400 | Not an address. Send one such as ada@example.com. |
| contact_exists | 409 | Already a contact. Use ?upsert=true or PATCH. |
| invalid_status | 400 | Use active, unsubscribed, bounced or complained. |
| invalid_revenue, invalid_name | 400 | Revenue is whole cents, 0 or more. Names are at most 120 characters. |
| unknown_field, invalid_field_value, invalid_field_key, invalid_field_kind | 400 | Create the field first, and send a value of its kind. |
| field_exists, list_exists | 409 | Pick another key or name. |
| too_many_contacts | 400 | At most 10,000 contacts per call. |
| csv_too_large, too_many_rows, empty_csv, invalid_csv, unknown_column | 400 | Fix the file: 25 MB, 500,000 rows, a header row, a mapped email column. |
| import_already_started, import_finished | 409 | The import already ran or ended. |
| empty_selection, too_many_targets, invalid_tags | 400 | Select 1 to 5,000 ids or 10,000 emails; tags must be valid names. |