Automations
Reading drip sequences and their runs. Authoring and editing automations is a dashboard act — see Automations for the concepts, step shape and worked examples.
The asymmetry is deliberate. A multi-step sequence that mails people on a schedule is exactly the kind of thing that should be built where a human can see the whole shape of it, so the API surface reads and the dashboard writes.
GET /api/automations
Every automation in the project.
Scope: automations:read
| Parameter | In | Type | Notes |
|---|---|---|---|
projectId |
query | string | Required. |
{
"automations": [
{
"id": "aut_…",
"name": "Welcome drip",
"trigger": "tag_added",
"triggerConfig": "{\"tag\":\"signed-up\"}",
"enabled": true,
"stepCount": 3,
"runCount": 1204,
"createdAt": "2026-06-12T08:00:00.000Z"
}
]
}
triggerConfig is a JSON string, not an object — parse it before reading the
tag.
GET /api/automations/{id}
One automation in full: its steps in order, run counts by status, and the ten most recent runs with any errors.
Scope: automations:read
{
"automation": {
"id": "aut_…",
"name": "Welcome drip",
"trigger": "tag_added",
"enabled": true,
"createdAt": "2026-06-12T08:00:00.000Z"
},
"steps": [
{ "id": "stp_…", "position": 1, "delayHours": 0, "subject": "Welcome" }
],
"runs": {
"counts": { "pending": 12, "completed": 1180, "failed": 3 },
"recent": [
{
"id": "run_…",
"contactEmail": "reader@example.com",
"status": "completed",
"scheduledFor": "2026-07-30T09:00:00.000Z",
"executedAt": "2026-07-30T09:00:11.000Z",
"error": null
}
]
}
}
runs.counts is keyed by status across the automation's whole history;
runs.recent is the last ten. A run carrying an error is the first place to
look when a sequence stops mid-way.
Triggering an automation
Automations fire on tags, so your application starts one by tagging a contact rather than by naming a sequence. That indirection is the point: your app knows the user did something, and the sequence attached to it can change without redeploying the app.
Two ways to add the tag.
PATCH /api/contacts/{id} — the API-key path
Scope: contacts:write. Tags are replaced wholesale, so read the current
list first and add to it. See
Contacts.
This is the right choice for anything new.
POST /api/contacts/event — the legacy webhook path
Predates project API keys and authenticates differently: a shared
WEBHOOK_SECRET passed as a query parameter, not a bearer token.
curl -X POST "https://pharosbase.com/api/contacts/event?secret=$WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
-d '{
"projectId": "acme-app",
"email": "reader@example.com",
"tag": "signed-up",
"action": "add"
}'
| Field | Type | Notes |
|---|---|---|
projectId |
string | Required. |
email |
string | Required. Must already exist as a contact. |
tag |
string | Required. |
action |
enum | add (default) or remove. |
Responds { "added": true }, { "removed": true }, or { "noop": true } when
the tag was already in the requested state. An unknown email returns 404;
the endpoint does not create contacts.
The secret is one value shared across every project, and a secret in a URL tends to end up in access logs and browser history in a way a header does not. Prefer the API-key path for new integrations, and keep this one server-side.