Ringmaster ingest API

Your app sends Ringmaster the people it knows about, what they do, and who must not be mailed. Flows and campaigns act on that data. The machine-readable contract is /v1/openapi.json.

Authentication

An owner makes an API key under Settings, API keys in the Ringmaster console. Send it as Authorization: Bearer rm_live_…. A key belongs to one organization and works only on /v1. It cannot read data or reach the console or MCP.

Bodies are JSON, at most 256 KB. POST /v1/events takes one event or up to 100 as { events: [...] }.

Idempotency

Every request needs an Idempotency-Key header of up to 255 characters. Keep it the same for every attempt at one piece of work. If your app sends from an outbox table, use the outbox row's id, so every retry of that row sends the same key.

Retries

Retry a network error, a timeout, 408, 425, 429 or any 5xx with exponential backoff, always with the same key. A request that already succeeded answers from the record and is not applied twice. Finish retrying within 7 days. Do not retry other 4xx answers, because the same request fails the same way.

Errors

Every error is { error: { code, message } }.

StatusCodeWhat to do
400missing_idempotency_key, invalid_idempotency_key, invalid_jsonFix the request. Do not retry it as it is.
401unauthorizedThe API key is missing, unknown or revoked.
409idempotency_key_reusedThe key was used for a different request. Do not retry; new work needs a new key.
413payload_too_largeThe body is over 256 KB. Split the batch.
422invalid_requesterror.issues lists each { path, message }. Fix the request.
422unknown_topic, wrong_topic_kindThe topic key is not one of this organization's topics, or is the wrong kind. Fix the request.
422unknown_flow, flow_not_activeThe flow key is not one of this organization's flows, or the flow is not active. Fix the request, or activate the flow.
5xxinternal_errorRetry with the same key.

Topics and consent

Every email belongs to one of your organization's topics, which you manage in the console or through MCP. Each has a key, such as product-news, and a kind:

POST /v1/suppressions stops one topic, or with no topic every opt-out and opt-in topic. Every email carries a link to the person's preference page, where they turn topics off and back on.

Sends and engagement

Ringmaster sends from your organization's verified domains, through the Resend account ZNZ runs for every organization, in the layout ZNZ designs with you. Every email prints your postal address and a link to unsubscribe from its topic, or for a service topic a link to the preference page. Opt-out and opt-in emails also carry the one-click List-Unsubscribe header. Properties your app sends appear in emails as merge fields, such as {{ profile.first_name }}, and every value is HTML-escaped.

Each email Ringmaster sends or holds back is a send. Read them on the Sending page, or through MCP with list_sends. A send's status is one of these:

statusMeaning
sentResend accepted it. The engagement times fill in as Resend reports them.
blockedConsent stopped it: the person unsubscribed from the topic, has no opt-in subscription, bounced or complained.
cappedYour organization had reached its monthly cap.
pausedSending was paused, by ZNZ or by the bounce and complaint limits, or your organization had no Ringmaster plan in good standing. error says which.
failedResend refused it, or the From address was not on a verified domain. error says which.
sendingOn its way.

firstOpenedAt and firstClickedAt keep the earliest open and click. A click counts as an open too, and a click on the unsubscribe link counts only as an open. Opens are seen only when the mail client loads images, so read them as a lower bound. A hard bounce or a spam complaint sets bouncedAt or complainedAt, suppresses the address in every topic with reason bounce or complaint, and posts profile.suppressed to your consent webhooks. A temporary bounce does neither.

Your organization may send up to its monthly cap, which ZNZ sets. Once it has sent 200 emails in 7 days, sending pauses on its own if 4% of them bounced or 0.08% drew a complaint, because one organization's bad list would put every organization's mail at risk. Only ZNZ resumes it. The Sending page shows the cap, this month's sends and both rates.

Flows and direct enrollment

A flow is a series of emails that runs from an event: its trigger names an event your app sends, with filters on the event's properties and the profile, and each step waits its delay, then goes out inside 08:00 to 20:00 in the recipient's time zone. Flows are designed in the console or through MCP, so the events you send are what start them. Send the event, and the flow decides who enters; you do not need to enroll anyone yourself.

POST /v1/enrollments is the exception, for a one-off or a backfill: it enrolls one profile in an active flow now, under the flow's reentry rule, its entry conditions and the person's consent, and answers whether it did and, if not, why (already_active, reentry, no_key, entry, exclusive, consent_blocked or no_email). It records a ringmaster.enroll event on the profile whose properties the flow's emails read as event.properties.

Segments and campaigns

A segment is a group of your profiles defined by their properties and by the events they sent, such as everyone who sent item_saved at least three times in the last 30 days, or who never sent charge-created. Ringmaster works out who is in it each time it is used, from the profiles and events you have sent so far, so keep sending them and a segment stays current on its own. Property names and event names in a segment are the ones your app sends.

A campaign is one email to a segment, in one topic, designed in the console or through MCP. Scheduling one first shows a dry run: who would get it, who consent leaves out, and when the first and last emails go. When it is scheduled, each person's email goes out inside 08:00 to 20:00 in their time zone, at most 50 every five minutes, and consent is checked again just before each one. Merge fields read profile.*, so a profile property your app sends, such as first_name, is {{ profile.first_name }} in a campaign.

Conversions

A flow can name a conversion goal: an event your app sends, such as purchase, and how many days after an email it still counts. Send that event to POST /v1/events when it happens, with occurredAt set to when it happened. To sum a value, such as revenue, give the event a numeric property and name it in the goal, such as event.properties.amount. A string such as "19.99" adds nothing.

Ringmaster credits each conversion event to the last flow email the profile got within the goal's days before it, and also counts it on every email in that window. Each flow's performance page shows both per step, beside sends, opens, clicks, unsubscribes, bounces, complaints and skipped steps. Agents read the same numbers through MCP with get_flow_performance, get_performance and get_overview.

Endpoints

POST /v1/events

Track events. Records one event, or up to 100 as { events: [...] }. Each event creates or updates its profile first, merging properties key by key.

Answers 202. Stored.

const response = await fetch(`${RINGMASTER_URL}/v1/events`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": outboxRow.id,
  },
  body: JSON.stringify({
    "event": "register",
    "profile": {
      "id": "user_8f2c",
      "email": "jane@example.com",
      "properties": {
        "first_name": "Jane",
        "wedding_date": "2027-06-12"
      },
      "country": "US",
      "timezone": "America/New_York"
    },
    "properties": {
      "source": "signup_form"
    },
    "occurredAt": "2026-10-29T12:00:00Z"
  }),
});

PUT /v1/profiles/{id}

Create or update a profile. Creates the profile with your id, or updates it. Fields you leave out keep their stored value.

Answers 200. Stored.

const response = await fetch(`${RINGMASTER_URL}/v1/profiles/${encodeURIComponent("user_8f2c")}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": outboxRow.id,
  },
  body: JSON.stringify({
    "email": "jane@example.com",
    "properties": {
      "plan": "free"
    }
  }),
});

DELETE /v1/profiles/{id}

Erase a profile. Deletes the profile and everything Ringmaster holds about it, for a GDPR erasure request. The audit log records the erasure without the email. Suppressions stay, because they are keyed by email.

Answers 200. Erased, or there was nothing to erase.

const response = await fetch(`${RINGMASTER_URL}/v1/profiles/${encodeURIComponent("user_8f2c")}`, {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Idempotency-Key": outboxRow.id,
  },
});

POST /v1/suppressions

Suppress an email address. Stops Ringmaster mailing the address in one topic or, with no topic, in every opt-out and opt-in topic. Use it when the person unsubscribes elsewhere. A service topic cannot be suppressed: only a bounce or a complaint stops one.

Answers 200. Suppressed. created is false when this suppression was already there.

const response = await fetch(`${RINGMASTER_URL}/v1/suppressions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": outboxRow.id,
  },
  body: JSON.stringify({
    "email": "jane@example.com"
  }),
});

DELETE /v1/suppressions/{email}

Lift a suppression. Removes the unsubscribes your app or the person made for the address and topic, or the every-topic ones with no topic. Suppressions made in the console, bounces and complaints stay. URL-encode the address.

Answers 200. Lifted, or there was nothing to lift.

const response = await fetch(`${RINGMASTER_URL}/v1/suppressions/${encodeURIComponent("jane@example.com")}`, {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Idempotency-Key": outboxRow.id,
  },
});

POST /v1/subscriptions

Record opt-in consent. Records that the person agreed to an opt-in topic, with the evidence you collected. An opt-in topic mails only subscribers. A suppression still stops the topic; lift it as well when the new consent should override it. A repeat keeps the first record. A new subscription whose consentedAt is within the last day enrolls each profile with the address in the flows that start on subscribing to the topic, so send the profile first; an older one is treated as a backfill and enrolls no one.

Answers 200. Recorded. created is false when the person was already subscribed.

const response = await fetch(`${RINGMASTER_URL}/v1/subscriptions`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": outboxRow.id,
  },
  body: JSON.stringify({
    "email": "jane@example.com",
    "topic": "newsletter",
    "source": "signup_form",
    "consentedAt": "2026-10-29T12:00:00Z",
    "ip": "203.0.113.7",
    "userAgent": "Mozilla/5.0"
  }),
});

DELETE /v1/subscriptions/{email}

Withdraw opt-in consent. Removes the person's subscription to an opt-in topic. URL-encode the address.

Answers 200. Removed, or there was nothing to remove.

const response = await fetch(`${RINGMASTER_URL}/v1/subscriptions/${encodeURIComponent("jane@example.com")}?topic=newsletter`, {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Idempotency-Key": outboxRow.id,
  },
});

POST /v1/enrollments

Enroll a profile in a flow. Enrolls the profile in an active flow now, without waiting for its trigger event, for a one-off or a backfill. The profile is created or updated first. The flow's reentry rule and the person's consent still apply, so the answer says whether the profile was enrolled and, if not, why: already_active, reentry (the rule refused it), no_key (the flow enters per event property and properties lacks it), entry (the profile does not meet the flow's entry conditions), exclusive (the flow runs one marketing flow at a time and the profile is in another), consent_blocked or no_email. The properties stand in for the trigger event's in the flow's emails.

Answers 200. Decided.

const response = await fetch(`${RINGMASTER_URL}/v1/enrollments`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${RINGMASTER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": outboxRow.id,
  },
  body: JSON.stringify({
    "flow": "welcome",
    "profile": {
      "id": "user_8f2c",
      "email": "jane@example.com",
      "properties": {
        "first_name": "Jane",
        "wedding_date": "2027-06-12"
      },
      "country": "US",
      "timezone": "America/New_York"
    },
    "properties": {
      "source": "backfill"
    }
  }),
});