People API
Look someone up: their fields, tags, suppression state, money and where they are in every flow and sequence. Plus the operator-side writes: unsubscribe, resubscribe, and internal notes on the timeline.
GET /api/v1/people/:id_or_email
Addressed by numeric id or email address — email is the universal key here, lowercased, and it's what imports and events upsert on.
GET /api/v1/people/[email protected]
{ "person": {
"id": 41, "email": "[email protected]", "name": "Ada Lovelace",
"unsubscribed": false,
"subscribed_on": "2023-04-11",
"fields": { "plan": "trial" },
"tags": ["trial"],
"attribution": { "first_landing_page": "…", "utm_source": "…" },
"lifetime_spend_cents": 29900,
"engagement_score": 83, "engagement_band": "hot",
"subscriptions": [
{ "external_id": "sub_311", "product": "pro-plan", "status": "active",
"plan": "pro", "interval": "yearly",
"renewal_amount_cents": 29900, "currency": "usd",
"next_renewal_at": "2027-08-06T00:00:00Z",
"cancels_at": null, "cancelled_at": null }
],
"active_flow_runs": [ { "flow": "trial_journey", "step": "trial_journey.7-wait-for-pricing" } ],
"recent_events": [ … ] } }
Each subscription is keyed the way money
events are: product is the product key and
external_id is what subscription events update against. A
non-null cancels_at is a scheduled cancellation — the
subscription is still live until that date, and cancelled_at
stays null until it actually ends.
engagement_score is the person's 0–100
engagement score and
engagement_band its band key — one of hot,
engaged, warm, cooling,
cold, or suppressed. It's read-only and
computed from opens, clicks, and suppression state; 0 always means the
address is suppressed.
Writing to a person
There is no person-update endpoint, and that's deliberate: record an event instead. An event upserts the person, sets fields and tags, captures first-touch attribution, records money — and leaves a timeline entry explaining why any of it changed. A silent write leaves no such account.
POST /api/v1/events
{ "email": "[email protected]", "name": "plan_changed",
"details": { "from": "trial", "to": "pro" },
"person": { "fields": { "plan": "pro" }, "tags": ["customer"] } }
The subscribe date
Every person has subscribed_on, a calendar date
(YYYY-MM-DD) recording when they joined the list. People created
through Mimeo get the day they arrived; people carried over from another tool
get whatever date you supply. It is never null, so a question like "who joined
before June?" is one comparison rather than a null check plus a fallback.
Set it explicitly under person when you record an event:
POST /api/v1/events
{ "email": "[email protected]", "name": "subscribed",
"person": { "subscribed_on": "2023-04-11" } }
Only an earlier date replaces a stored one, and the event's
occurred_at is never used for it. Both rules exist for
the same reason: an event carries a timestamp whether or not it has anything
to do with joining a list, so backfilling years of purchase history would
otherwise rewrite everyone's subscribe date to their first purchase. Because
earlier wins, those old timestamps are exactly the ones that would land.
The practical effect is that a repeat opt-in can't overwrite a real join date, and re-sending the same event is idempotent. To move a date forward, edit it on the person's profile — that's the one write that sets it outright.
Suppression
A person's own opt-out happens through the unsubscribe link and the
tokenized subscription endpoints —
which attribute it to the exact email it came from. Suppression state appears on
the person: whether they're unsubscribed, when, why, and how it happened
(link — the footer link, one_click — the mail client's
button, api — a page of yours calling the subscription API,
import, provider_sync, provider_webhook,
manual — you, or your agent).
Unsubscribe and resubscribe as the operator
The same two buttons the person's page has, for when the request reaches you rather than the link — someone writes in asking to be taken off, a list you're retiring, an opt-out the provider missed:
POST /api/v1/people/[email protected]/unsubscribe
{ "reason": "asked by email" }
POST /api/v1/people/[email protected]/resubscribe
Both take an id or an email, both are idempotent, and both answer with the
person as GET does. reason is optional (default
unsubscribe) and shows on their record; the source is
manual, so it stays distinguishable from a click. Unsubscribing
stops every send to them immediately and mirrors to your provider where the
provider supports it; resubscribing clears the suppression and mirrors that too.
"It's people, not subscribers" is the rule the data model follows: someone who unsubscribes is still a person, with their history intact.
Internal notes
Attach a plain-text internal note to a person's timeline — bookkeeping about the person (a support exchange, a manual correction, why something was done), as opposed to an event, which is something the person did. Notes land on the timeline next to everything else, shown with a note icon and their readable text.
POST /api/v1/people/:id_or_email/notes
Authorization: Bearer <token>
{
"note": "Called about billing — resolved, keeping them on the annual plan.",
"idempotency_key": "support-call-2026-08-23",
"metadata": { "ticket": 4211 }
}
Same bearer-token authentication as the rest of the API, and the person is
addressed by numeric id or email, exactly like GET /api/v1/people/:id.
Returns 201 with the note:
{
"status": "created",
"note": {
"id": 9182,
"note": "Called about billing — resolved, keeping them on the annual plan.",
"metadata": { "ticket": 4211 },
"occurred_at": "2026-08-23T14:02:11-04:00",
"person": { "id": 41, "email": "[email protected]" }
}
}
-
Notes never trigger automation. A note can't start a
flow, open a gate, or send mail, and the reserved name it's stored
under (
internal_note) never enters the event-type trigger vocabulary. There is no opt-in — internal notes are invisible to the engine, always. (The generic events API refuses the name for the same reason.) -
Validated.
noteis required and capped at 10,000 characters; anything else answers422with the problem named. -
Idempotent on request. Pass an
idempotency_keyand a retry with the same key answers200with"status": "duplicate"and the already-written note, instead of writing a second one — so a maintenance script can retry safely. -
Never creates a person. An unknown id or email is a
404— a note is about someone who exists. -
metadatais optional — structured context (IDs, timestamps, batch names) kept alongside the note and shown apart from the readable text on the timeline.
The queue
GET /api/v1/[email protected]&status=held
What's scheduled for them, what's holding and why, and what was dropped.
?status= takes pending, held,
dropped, canceled or sent;
?limit= caps at 200.
See also: Events API · Subscriptions API · People, for humans