Theme

The event contract

One endpoint takes everything. Signups, opt-ins, purchases, renewals, cancellations — they all arrive as events, and events are what build a person's record in Mimeo.

POST /api/v1/events
Authorization: Bearer mm_your_token
Content-Type: application/json

What an event does

Every accepted event does four things, in this order:

  1. Upserts the person, matched on lowercased email. The same address never produces two people, no matter how it's capitalized or how many systems send it.
  2. Captures first-touch attribution. The attribution on the first event that creates a person is written once and never overwritten. Later events can carry attribution; it won't replace what's already there. This is what makes "where did this customer originally come from?" answerable months later.
  3. Appends to the timeline. The event becomes a permanent entry on that person's history.
  4. Writes the money tables, if it's one of the reserved money event names below.

Events wake the engine. An event that matches an active flow's trigger starts a run, and it also releases anyone waiting at a gate for it. Mimeo fires its own events too — tag_added, field_changed, sequence_completed, flow_exited — so those are triggers like any other.

Request body

Send one event object:

{ "email": "[email protected]", "name": "signed_up" }

Or a batch, up to 100 events per request:

{ "events": [ { … }, { … } ] }

Fields

Field Notes
email Required. The match key. Lowercased on receipt.
name Required. The event name, e.g. signed_up. Use your own names freely except for the reserved money names below.
occurred_at ISO8601 timestamp. Defaults to now. Set it explicitly when backfilling history so the timeline reads correctly.
idempotency_key Optional. A repeated key returns status duplicate and is not re-processed. Use it anywhere a retry is possible — webhook handlers especially.
details Object. Free-form for your own events; carries the defined payload for money events.
person Object: first_name, last_name, fields (a key/value object), tags (array of strings).
attribution Object: landing_page, referrer, utm_source, utm_medium, utm_campaign, utm_term, utm_content, device, country.

Custom field keys must be declared. Keys inside person.fields have to match field definitions you've created under Settings → Fields. Unknown keys sent through the API are silently ignored — no error, no data. If a value isn't showing up, check the key against GET /api/v1/field_definitions first.

Response

201 Created when everything succeeded, or 422 if any event in the request failed. Either way you get a results array in the same order as the events you sent:

{
  "results": [
    { "status": "created",   "person_id": 42, "event_id": 1087 },
    { "status": "duplicate", "person_id": 42, "event_id": 1042 },
    { "status": "invalid",   "error": "email is required" }
  ]
}

status is created, duplicate, or invalid. A batch can partially succeed — always read the array rather than trusting the status code alone.

Opt-in intake

There is no separate subscribe endpoint. Opt-ins are a convention on top of events: use the name opted_in and put the form or page in details.entry_point.

curl -X POST https://your-instance.com/api/v1/events \
  -H "Authorization: Bearer mm_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "opted_in",
    "details": { "entry_point": "footer_newsletter_form" },
    "person": {
      "first_name": "Ada",
      "tags": ["newsletter"]
    },
    "attribution": {
      "landing_page": "https://example.com/blog/why-own-your-email",
      "referrer": "https://news.ycombinator.com/",
      "utm_source": "hn",
      "utm_medium": "referral",
      "device": "desktop",
      "country": "US"
    }
  }'

Keeping entry_point consistent across your forms is what later lets you answer "which form is actually producing subscribers?"

System events

Mimeo emits some events itself when state changes, so the timeline stays complete without you sending them:

Event Details
tag_added details.tag
tag_removed details.tag
field_changed details.key, details.old, details.new

These fire whether the change came from the UI, an import, or an event you sent. Tag and field history is auditable rather than invisible.

Money events

Six event names are reserved. They behave like any other event — upserting the person, appending to the timeline — and additionally write Mimeo's money tables.

Mimeo never integrates with Stripe or any payment processor. All money data comes from your systems, sent as events. That's what keeps the money model independent of who you bill through.

purchase

Someone bought something.

details Notes
product Required. A product key. Unknown keys are auto-created, so you don't have to pre-register your catalog.
amount_cents Integer cents.
currency Defaults to usd.
promo_code Optional.
external_id Your system's ID for the purchase. Used as an idempotent upsert key — resending the same external_id updates the purchase rather than creating a second one.
metadata Object, free-form.
payment Optional nested object for the initial charge: amount_cents, external_id, occurred_at.
curl -X POST https://your-instance.com/api/v1/events \
  -H "Authorization: Bearer mm_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "purchase",
    "idempotency_key": "order_9f2c1a",
    "details": {
      "product": "pro_annual",
      "amount_cents": 24000,
      "currency": "usd",
      "promo_code": "LAUNCH20",
      "external_id": "ord_9f2c1a",
      "metadata": { "seats": 3 },
      "payment": {
        "amount_cents": 24000,
        "external_id": "ch_44b81e",
        "occurred_at": "2026-07-24T14:02:11Z"
      }
    }
  }'

Free trials

A free-trial start is a purchase with amount_cents: 0 and no nested payment. The purchase records that they took the product; the absence of a payment records that no money moved.

curl -X POST https://your-instance.com/api/v1/events \
  -H "Authorization: Bearer mm_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "purchase",
    "details": {
      "product": "pro_monthly",
      "amount_cents": 0,
      "external_id": "trial_5521"
    }
  }'

payment

Money actually moved. Payments are what lifetime spend sums.

details Notes
amount_cents Required.
currency Defaults to usd.
external_id Your charge ID.
purchase_external_id Links this payment to the purchase it belongs to.
occurred_at When the charge happened.

subscription_started

details Notes
product Product key.
purchase_external_id The originating purchase.
external_id Your subscription ID — the key later events update against.
status trialing, active, past_due, cancelled, or lifetime.
plan Your plan identifier.
interval monthly, quarterly, or yearly.
renewal_amount_cents What the next renewal will charge.
next_renewal_at When that happens.
payment Optional nested initial charge.

subscription_renewed

Renewals update the subscription and never create purchases. If you send a purchase on every renewal, a two-year customer looks like twenty-four separate buyers. Send subscription_renewed instead — one purchase, one subscription, many payments.

details Notes
external_id Which subscription renewed.
status Defaults to active.
renewal_amount_cents Updated renewal amount.
next_renewal_at The new renewal date.
payment Optional nested object for the renewal charge.
curl -X POST https://your-instance.com/api/v1/events \
  -H "Authorization: Bearer mm_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "subscription_renewed",
    "idempotency_key": "renewal_sub_311_2026_07",
    "details": {
      "external_id": "sub_311",
      "status": "active",
      "renewal_amount_cents": 2400,
      "next_renewal_at": "2026-08-24T00:00:00Z",
      "payment": {
        "amount_cents": 2400,
        "external_id": "ch_77d02f",
        "occurred_at": "2026-07-24T00:00:11Z"
      }
    }
  }'

subscription_cancelled

details: external_id, cancelled_at.

subscription_changed

details: external_id plus any of status, plan, interval, renewal_amount_cents, next_renewal_at. Use it for upgrades, downgrades, interval switches, and payment-failure status changes.

Lifetime spend

A person's lifetime spend is the sum of their payments — not their purchases and not their subscription amounts. Purchases record intent; payments record money. A free trial adds nothing until the first payment arrives, which is exactly right.

Batching

Up to 100 events per request, processed in order, with one result per event:

curl -X POST https://your-instance.com/api/v1/events \
  -H "Authorization: Bearer mm_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "email": "[email protected]",
        "name": "opted_in",
        "occurred_at": "2026-07-20T09:14:00Z",
        "details": { "entry_point": "pricing_page_form" }
      },
      {
        "email": "[email protected]",
        "name": "purchase",
        "idempotency_key": "order_7710",
        "details": {
          "product": "pro_monthly",
          "amount_cents": 2400,
          "external_id": "ord_7710",
          "payment": { "amount_cents": 2400, "external_id": "ch_7710a" }
        }
      }
    ]
  }'

Batching is the right tool for backfills. Set occurred_at on each event so history lands on the correct dates, and give each one an idempotency_key so a re-run doesn't double-count.

Discovering the field schema

GET /api/v1/field_definitions
{
  "field_definitions": [
    {
      "key": "plan",
      "label": "Plan",
      "type": "select",
      "options": ["free", "pro", "team"],
      "description": "Current subscription plan"
    }
  ]
}

This exists for agents. A coding agent pointed at your instance can read the field schema and know what this particular install understands, rather than guessing at keys that would be silently ignored. Call it before writing code that sets person.fields.

Reading a person

GET /api/v1/people/:id_or_email

Takes either the numeric ID or the email address. Returns the person's profile, custom field values, tags, first-touch attribution, suppression state, and lifetime_spend_cents.

curl https://your-instance.com/api/v1/people/[email protected] \
  -H "Authorization: Bearer mm_your_token"

Looking up by email is usually what you want — it's the key you already have, and it's the same key events match on.