Contacts
Reading and editing individual contacts. For keeping a whole audience in sync
from your application, use POST /api/v1/contacts/sync
instead — it is idempotent, takes up to 5,000 contacts at a time, and respects
suppressions.
Every endpoint here names its project with projectId (slug or prj_… id).
See API conventions.
GET /api/contacts
List contacts with their subscription status. Filtering happens server-side.
Scope: contacts:read
| Parameter | In | Type | Notes |
|---|---|---|---|
projectId |
query | string | Required. |
limit |
query | integer | Max rows returned. |
search |
query | string | Case-insensitive substring match on email. |
status |
query | enum | subscribed, unsubscribed, or no list. |
curl "https://pharosbase.com/api/contacts?projectId=acme-app&status=subscribed&limit=50" \
-H "Authorization: Bearer $PHAROS_API_KEY"
{
"contacts": [
{
"id": "cnt_…",
"email": "reader@example.com",
"firstName": "Sam",
"lastName": null,
"language": "en",
"tags": ["beta"],
"source": "website_signup",
"status": "subscribed",
"createdAt": "2026-07-14T09:12:00.000Z"
}
],
"total": 4821,
"matched": 4102,
"returned": 50
}
total is the whole project. matched is how many rows the filters
selected. returned is how many came back after limit. The three differ on
purpose — a count you can trust is worth more than a page you have to
reassemble.
status is derived from list subscriptions, not stored on the contact.
no list means the contact belongs to no list at all.
POST /api/contacts
Create one contact. Auto-subscribes it to the project's default list if there is one. For more than a handful, use the sync endpoint.
Scope: contacts:write
| Field | Type | Notes |
|---|---|---|
projectId |
string | Required. |
email |
string | Required. Normalised to lowercase. |
firstName |
string | |
lastName |
string | |
language |
enum | en or pt-BR. Defaults to en. |
tags |
string[] |
201:
{ "id": "cnt_…", "subscribed": true }
subscribed reports whether a default list existed to subscribe to.
PATCH /api/contacts/{id}
Update a contact. Only the fields you pass change.
Scope: contacts:write
| Field | Type | Notes |
|---|---|---|
email |
string | |
firstName |
string | null | |
lastName |
string | null | |
language |
enum | en or pt-BR. |
tags |
string[] | Replaces the whole list — include existing tags to keep them. |
{ "success": true }
Tags are actions, not annotations. Adding one can fire a
tag_addedautomation and start a drip sequence. Read the current tags first, and add to them rather than overwriting.
Passing no recognised field returns 400. An id in another project returns
404.
DELETE /api/contacts/{id}
Delete a contact and its subscriptions.
Scope: contacts:write
{ "success": true }
This is a hard delete, and it is not a substitute for unsubscribing: deleting a contact discards the record that they opted out, so a later sync can re-add them. To stop mailing someone, unsubscribe them.
PATCH /api/contacts/{id}/subscriptions/{listId}
Change one contact's status on one list — the granular consent operation.
Scope: contacts:write
| Field | Type | Notes |
|---|---|---|
status |
enum | subscribed, unsubscribed or pending. |
{ "success": true }
Consent is per list, so this is the endpoint to use when someone opts out of one thing and not everything.
POST /api/contacts/import
Bulk import of CSV-shaped rows onto one list. Backs the dashboard's import screen.
Scope: contacts:write
| Field | Type | Notes |
|---|---|---|
projectId |
string | Required. |
contacts |
object[] | Required. Email plus optional name, language, tags. |
listSlug |
string | Target list. Defaults to the project's default list. |
defaultLanguage |
string | Language for rows that do not carry one. Falls back to pt-BR. |
The target list is created if it does not exist.
{
"success": true,
"imported": 812,
"updated": 104,
"skipped": 3,
"skippedUnsubscribed": 27,
"total": 946,
"listName": "Newsletter"
}
skippedUnsubscribed counts rows left alone because the contact had
explicitly opted out. A spreadsheet upload does not reverse an opt-out.
Prefer POST /api/v1/contacts/sync for anything
recurring: it is versioned, expresses desired state so retries are free, and
protects hard bounces and complaints as well as opt-outs.
GET /api/contacts/export
Export contacts as CSV.
Scope: contacts:read
| Parameter | In | Type | Notes |
|---|---|---|---|
projectId |
query | string | Required. |
ids |
query | string | Comma-separated contact ids. Omit to export everything. |
Returns text/csv as a file attachment, not JSON — one row per contact, with
columns email, first_name, last_name, language, status, tags, source, created_at. Tags are pipe-separated inside their cell. A contact on no list
exports with status no list.
curl "https://pharosbase.com/api/contacts/export?projectId=acme-app" \
-H "Authorization: Bearer $PHAROS_API_KEY" -o contacts.csv