Theme

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/ada@example.com

{ "person": {
    "id": 41, "email": "ada@example.com", "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

To change a person's fields or tags through the API, record an event. An event upserts the person, sets fields, adds person.tags and removes person.remove_tags, leaving a timeline entry explaining why they changed. These tag changes do not unsubscribe or resubscribe anyone; use the dedicated operator suppression endpoints for those actions.

POST /api/v1/events
{ "email": "ada@example.com", "name": "plan_changed",
  "details": { "from": "trial", "to": "pro" },
  "person": { "fields": { "plan": "pro" }, "tags": ["customer"] } }

See the selective preference example to remove daily-news membership and add weekly news in one event. Unrelated tags remain intact. A normalized tag cannot appear in both the add and remove arrays: that returns 422 without writes. Operator API tokens belong on your server, not in a subscriber's browser.

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": "ada@example.com", "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/ada@example.com/unsubscribe
{ "reason": "asked by email" }

POST /api/v1/people/ada@example.com/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": "ada@example.com" }
  }
}

The queue

GET /api/v1/queue?person=ada@example.com&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