Skip to content
Miss Blue

Reference

Campaigns

Everything the console's campaign tools do, with a key or a signed-in session. A key reaches its own project and gets 404 for every other. The same rules apply as in the console: opted-out and blocked people are left out before anybody sees a count, first contacts are paced per number, and a project's first announcement and first automation that writes to people first wait for a person at Miss Blue to read them. Announcement, scheduled and automation bodies are Liquid, filled in for each person when that message sends: {{ contact.first_name | default: "there" }}, and the contact's own fields as {{ contact.custom.<name> }}. The variables and preview routes at the end list what a project can use and show what a body will say.

Announcements in this project, newest first, with how each is going.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [{
    "id": "5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90",
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "title": "Spring hours",
    "body": "We open at nine from Monday.",
    "status": "sending",
    "send_at": null,
    "time_zone": null,
    "reply_window_hours": 72,
    "review_note": null,
    "started_at": "2026-09-26T14:00:00Z",
    "finished_at": null,
    "created_at": "2026-09-26T13:58:00Z",
    "stats": { "total": 120, "sent": 64, "skipped": 2, "invalid": 0, "failed": 0, "pending": 54, "replied": 9, "unsubscribed": 1 }
  }]
}

Draft an announcement. Nothing is sent.

Recipients are the handles you pass, everybody on each list, and everybody carrying each tag, once each. Lists and tags are read now: somebody added later is not included. Opted-out and blocked people are marked skipped before the response, so the counts are what sending would do. At most 5,000 people.

number_id*uuidOne of this project's numbers.
title*stringInternal. Recipients never see it.
bodystringThe message, in Liquid, filled in for each person as it sends: {{ contact.first_name | default: "there" }}, {{ contact.custom.<name> }}. See /docs/templates. Required unless `variants`; with them, version A's text.
variantsobject[]An A/B/C test: 2 or 3 of `{letter, body, weight}`, letters A to C with an A, weights whole percents adding up to 100 (leave them all out for an even split). Each person is given one when the draft is made, cut by the weights exactly.
test_firstobjectWith `variants`: `{percent, wait_hours}`. Send the versions to `percent` of the list first (5 to 50), wait `wait_hours` (1 to 72) from when the last of their messages left the number, then send the rest the version with the best reply rate, leaving out one that looks to cause more opt-outs. The pick waits (another `wait_hours` at a time, up to 72 hours, saying so in `pick_note`) until at least half of those whose message went have been reached.
recipientsstring[]Phone numbers or emails.
list_idsuuid[]Everybody on these lists, as they stand now.
tagsstring[]Everybody carrying these tags, as they stand now.
send_atRFC 3339When to start. Absent: when it is sent.
time_zonestringThe zone send_at was chosen in. Shown in the console.
quiet_hoursbooleanOn unless false: each person is reached only 8 AM to 9 PM in their own time (tighter where their state requires it), and held until their morning otherwise.
reply_window_hoursinteger1 to 720. How long a reply counts as a reply to this. 72 by default.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "title": "Spring hours",
    "body": "We open at nine from Monday.",
    "recipients": [
      "+15551230002"
    ],
    "list_ids": [
      "c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68"
    ],
    "tags": [
      "vip"
    ]
  }'

Response

{
  "id": "5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90",
  "status": "draft",
  "stats": { "total": 120, "sent": 0, "skipped": 2, "invalid": 0, "failed": 0, "pending": 118, "replied": 0, "unsubscribed": 0 }
}
  • 400No body, nobody to send to, more than 5,000 people, a reply window outside 1 to 720 hours, versions that are not 2 or 3 with an A, weights that do not add up to 100, a version that will not parse, or test first without versions.
  • 404The number or a list is not this project's.

Preview what sending it would do, before anybody does it.

Show this to whoever approves the send. `established` people have talked with the number before and go out at normal speed. `first_contact` people have not, and go out at `first_contact_per_day` a day from this number, which is where `first_contact_days` comes from. `skipped` were left out: opted out, blocked, or not a phone number or email. `needs_review` is true for a project's first announcement. With versions, `versions` is how many get each one; with a test first those are the test slice, and the row with `variant: null` is the rest, who get the best version after the wait.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements/5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90/split \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 120,
  "established": 70,
  "first_contact": 48,
  "skipped": 2,
  "first_contact_days": 1,
  "first_contact_per_day": 50,
  "needs_review": false,
  "versions": [
    { "variant": "A", "people": 8, "in_test": 8 },
    { "variant": "B", "people": 8, "in_test": 8 },
    { "variant": "C", "people": 8, "in_test": 8 },
    { "variant": null, "people": 94, "in_test": 0 }
  ]
}

Send a draft.

A project's first announcement becomes `awaiting_review` until a person at Miss Blue reads it. After that, one with a `send_at` becomes `scheduled` and one without starts `sending`. Preview it with the split first: this cannot be undone for the people it has already reached.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements/5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90/send \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "id": "5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90", "status": "sending" }
  • 409It has already been sent or cancelled.

Cancel a draft, a scheduled announcement, or the rest of one that is sending.

What has already gone stays gone. Cancelling stops the people not yet reached.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements/5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90/cancel \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "cancelled": true }
  • 409It has already finished.

Use this one: everybody an A/B/C announcement has not reached yet gets this version.

During a test first's wait it is the pick, made by hand instead of by the numbers. Once only. The announcement comes back with `picked_variant` and `pick_note`, and `test`: the versions side by side (the test slice only, with a test first) with a verdict, in the shape of an automation step's `test`.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/announcements/5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90/variants/B/use \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "id": "5f0c1a2e-8a4b-4c1d-9e7f-2b6d3c8a1e90", "status": "sending", "picked_variant": "B", "pick_note": "B was chosen by hand." }
  • 400It has one text.
  • 404It has no such version, or is not this project's.
  • 409A version was already picked, or it has finished.

Messages scheduled for later, and what became of them.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/scheduled \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [{
    "id": "9b2e4c61-3f8a-4d7e-b1c0-6a5d8e2f7c13",
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "recipient": "+15551230002",
    "body": "See you at nine",
    "send_at": "2026-10-02T14:00:00Z",
    "time_zone": "America/Chicago",
    "status": "pending",
    "error": null,
    "sent_message_id": null,
    "created_at": "2026-09-26T13:58:00Z"
  }]
}

Schedule one message for later.

`send_at` is absolute. Choose it in the recipient's local time and pass their offset. When it goes, it goes through the same path as a send now: opt-outs, blocks and first-contact pacing apply at that moment.

number_id*uuidOne of this project's numbers.
recipient*stringPhone number or email.
body*stringOptional only with attachment_ids. Liquid, filled in from the recipient's contact when it sends, not when it is scheduled.
send_at*RFC 3339At least 30 seconds and at most 365 days ahead.
time_zonestringThe zone it was chosen in, for display.
quiet_hoursbooleanOn unless false: a send_at in the recipient's night (8 AM to 9 PM their time, tighter where their state requires it) goes at their morning. goes_at in the response says when.
attachment_idsuuid[]Uploaded files to send with it.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/scheduled \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "recipient": "+15551230002",
    "body": "See you at nine",
    "send_at": "2026-10-02T09:00:00-05:00",
    "time_zone": "America/Chicago"
  }'

Response

{ "id": "9b2e4c61-3f8a-4d7e-b1c0-6a5d8e2f7c13", "status": "pending", "send_at": "2026-10-02T14:00:00Z" }
  • 400A time that has passed, or more than a year ahead.

Change the text or the time of a message that has not gone.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/scheduled/9b2e4c61-3f8a-4d7e-b1c0-6a5d8e2f7c13 \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "See you at ten",
    "send_at": "2026-10-02T10:00:00-05:00"
  }'

Response

{ "id": "9b2e4c61-3f8a-4d7e-b1c0-6a5d8e2f7c13", "status": "pending", "send_at": "2026-10-02T15:00:00Z" }
  • 409It has already been sent or cancelled.

Cancel a scheduled message.

curl -X DELETE https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/scheduled/9b2e4c61-3f8a-4d7e-b1c0-6a5d8e2f7c13 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "cancelled": true }
  • 409It has already been sent or cancelled.

Automations in this project: what starts each, its steps, and whether it is on.

`results` is the last 30 days: the same totals as the automation's stats.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [{
    "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21",
    "name": "Welcome",
    "enabled": true,
    "trigger_kind": "tag_added",
    "trigger_keywords": [],
    "trigger_list_id": null,
    "trigger_tag": "new-customer",
    "number_id": null,
    "allow_repeat": false,
    "stop_on_reply": true,
    "send_hours": { "start": "08:00", "end": "18:00", "time_zone": "America/New_York", "days": ["mon", "tue", "wed", "thu", "fri"] },
    "number_note": null,
    "review_status": "approved",
    "review_note": null,
    "reaches_strangers": true,
    "steps": [
      { "id": "…", "position": 10, "kind": "send", "body": "Welcome aboard.", "wait_seconds": null, "condition_kind": null, "next_step_id": null },
      { "id": "…", "position": 20, "kind": "wait", "body": null, "wait_seconds": 172800, "condition_kind": null, "next_step_id": null },
      { "id": "…", "position": 30, "kind": "condition", "body": null, "wait_seconds": null, "condition_kind": "not_replied_since_last", "next_step_id": null },
      { "id": "…", "position": 40, "kind": "send", "body": "Anything we can help with?", "wait_seconds": null, "condition_kind": null, "next_step_id": null }
    ],
    "running": 12,
    "completed": 30,
    "created_at": "2026-09-20T10:00:00Z",
    "results": { "days": 30, "sent": 118, "delivered": 104, "read": 61, "replied": 19, "opted_out": 2, "failed": 3, "cancelled": 6, "pending": 0, "reached": 105, "reply_rate": 0.181, "opt_out_rate": 0.019 }
  }]
}

Create an automation, from a one-message reply to a multi-step sequence. It starts switched off.

The list and the number it names must be this project's. Triggers: `keyword`, `first_message`, `conversation_opened`, `added_to_list` and `tag_added`. Steps run in order: `send` (body), `wait` (wait_seconds, up to 31 days), `condition` (carries on only when it holds, and otherwise ends the run), and `stop`. A send body is Liquid, filled in for each person when that step sends, from their contact as it is then: {{ contact.first_name | default: "there" }}, {{ contact.custom.checkout_link }}. Update a field before a follow-up and the follow-up says the new value. `not_replied` and `replied` count anything since the run started; `not_replied_since_last` and `replied_since_last` count only what came after its latest message. A list or tag automation with no `number_id` sends from Auto: each person gets the best of the project's numbers when their first message goes. If a pinned number leaves the project, the automation moves to another of its numbers, or is switched off when there is none, and `number_note` says which. Only point one at people who asked to hear from you.

name*string
trigger_kind*stringWhat starts it.
trigger_keywordsstring[]For `keyword`. Matched as whole messages, case-insensitively.
trigger_list_iduuidFor `added_to_list`.
trigger_tagstringFor `tag_added`.
number_iduuidThe number it listens on or sends from. Absent: every number for a keyword or message trigger, Auto for a list or tag.
allow_repeatbooleanLet one person go through it more than once.
stop_on_replybooleanEnd a person's run when they reply. On when absent. A condition that waits for a reply never holds while it is on.
send_hoursobjectOnly send during these hours: `start` and `end` as `HH:MM` local time, `time_zone` (IANA, like `America/New_York`), `days` (`mon` to `sun`, every day when absent). `end` must be after `start`; no overnight hours yet. What starts it still starts it at any time; a message due outside the hours waits until they open. Used instead of quiet hours. Absent or null: quiet hours, unless quiet_hours is false (then any time).
quiet_hoursbooleanApplies when there are no send_hours; with send_hours, those are used instead. On unless false: each message waits for 8 AM to 9 PM in its recipient's own time (tighter where their state requires it). A reply to somebody who wrote in the last hour still goes. Absent on an update keeps what it has.
steps*arrayUp to 40. A `send` step can be an A/B/C test: `variants`, 2 or 3 of `{letter, body, weight}` (letters A to C with an A, weights adding up to 100, or none for an even split) instead of `body`. A person is given a version at the first step with versions, by its weights, and keeps it for every later one (A where a step has none): a later step follows that split, and may only have that step's letters. `fresh_results: true` starts a step's comparison from now.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Cart reminder",
    "trigger_kind": "tag_added",
    "trigger_tag": "cart-abandoned",
    "stop_on_reply": true,
    "send_hours": {
      "start": "08:00",
      "end": "18:00",
      "time_zone": "America/New_York",
      "days": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ]
    },
    "steps": [
      {
        "kind": "send",
        "body": "Hi {{ contact.first_name | default: \"there\" }}, your {{ contact.custom.product }} ({{ contact.custom.amount }}) is still waiting: {{ contact.custom.checkout_link }}"
      },
      {
        "kind": "wait",
        "wait_seconds": 86400
      },
      {
        "kind": "condition",
        "condition_kind": "not_replied_since_last"
      },
      {
        "kind": "send",
        "body": "Still thinking it over, {{ contact.first_name | default: \"there\" }}? Your cart is saved: {{ contact.custom.checkout_link }}"
      }
    ]
  }'

Response

{ "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "enabled": false, "review_status": "not_needed" }
  • 400A missing trigger detail, an empty send, a wait over a month, an unknown condition, or sending hours that end before they start, have no days, or name an unknown zone.
  • 404The list or the number is not this project's.

Replace an automation's settings and steps.

Takes the same body as creating one, with the same checks. `stop_on_reply` and `send_hours` absent keep what it has, and `send_hours: null` clears the hours; `number_id` absent means Auto (or every number), not "unchanged". Saving clears `number_note`. People partway through keep their step number (their position in `steps`, counted from the top): the step now at that number is what they do next, so a step added or removed above them changes it, and one whose number no longer exists is finished, `done`, once its last message has gone. A message already on its way is not recalled. Steps sent unchanged are left as they are. Changing or clearing `send_hours` looks again at everybody waiting for the old hours. Editing a switched-on automation into a list or tag automation gets the same review as switching one on. A send step that leaves `variants` out keeps the versions of the tested step with the same text, wherever it has moved; changing a tested step's text without `variants` is 409, and `variants: []` goes back to one text.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21 \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome",
    "trigger_kind": "tag_added",
    "trigger_tag": "new-customer",
    "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d",
    "stop_on_reply": false,
    "steps": [
      {
        "kind": "send",
        "body": "Welcome aboard!"
      }
    ]
  }'

Response

{ "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "enabled": true }

Delete an automation, and end everybody partway through it.

curl -X DELETE https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "deleted": true }

Preview what switching it on would do.

How many people are already on the list or carry the tag, how many of them are first contacts and how long those take, and whether it will wait for review. `lines` is how many numbers it sends from: one when pinned, every usable one on Auto, 0 when none can take it. `first_contact_per_day` is the pace across all of them: with sending hours, what fits in them at the first-contact spacing. `first_contact_days` counts calendar days.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/effect \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "already_there": 400, "first_contact": 380, "first_contact_days": 4, "first_contact_per_day": 100, "lines": 2, "needs_review": true }

Switch an automation on or off.

Off ends every run in progress. On, a list or tag automation from a project that has never had one approved is held as `awaiting_review` until a person at Miss Blue reads it. `include_existing` also starts it for everybody already on the list or carrying the tag, which messages them: preview with the effect first.

enabled*boolean
include_existingbooleanStart it for the people already there too. Off by default.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/enabled \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "include_existing": false
  }'

Response

{ "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "enabled": true, "review_status": "awaiting_review" }
  • 400It has no steps.

Who is in an automation, how far each person got, and when they hear next.

Newest first, a page at a time; `order=next` puts the running people whose next message is soonest first. `total` is how many there are in all, so you know when you have them. `waiting_for` says what a person's next message waits for: `quiet_hours`, `sending_hours`, `paced`, `daily_limit`, `line_unavailable`, `retry`, `waiting`, `wait_step`, or null when it is going now; `next_message_at` is when. `time_zone` and `state` come from their number, to show it in their time.

statusquery`running`, `done`, `stopped` or `failed`.
orderquery`next` (soonest next message first) or `newest` (the default).
limitquery1 to 500, 100 by default.
offsetqueryHow many to skip, for the next page.
curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/runs \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 240,
  "data": [{
    "run_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "handle": "+15551230002",
    "status": "running",
    "reason": null,
    "step_position": 20,
    "due_at": "2026-09-28T14:00:00Z",
    "deferred_until": "2026-09-28T14:00:00Z",
    "waiting_for": "quiet_hours",
    "next_message_at": "2026-09-28T14:00:00Z",
    "time_zone": "America/Denver",
    "state": "CO",
    "started_at": "2026-09-26T14:00:00Z",
    "finished_at": null
  }]
}

How an automation is doing, step by step: sent, delivered, read, replied and opted out.

Counts the messages it sent in the range, by step (numbered as the editor numbers them), and how the people who started in the range ended. `sent`, `delivered` and `read` count messages. `read` is a floor: iMessage has no separate open, a read receipt is the open, and only people with read receipts on send one, so it has no rate. `replied` and `opted_out` count people, once each however many texts they sent. A reply counts for a step when that step's message was the latest thing sent to them in the conversation before they wrote (by their phone's time, with a minute's allowance), within 7 days; after a message from your team or an announcement it counts for neither. `opted_out` counts replies that opted them out, when the project acts on STOP. An automation older than the record of which step sent each message is counted from `recorded_since`, runs included, and `from_recorded_since` says the range was moved. `reached` (delivered, or sent as a text message, which reports no delivery, or answered) is the denominator of `reply_rate` and `opt_out_rate`. `variants` are one row per version of an A/B/C test, with the same counts. `test`, on a step with versions, puts them side by side with each one's text and weight and a verdict: `not_enough` until every version has reached 30 people, then the reply rate (two-proportion z-test at 95%) and, separately, the opt-out rate (Fisher's exact test at 95%, corrected for the number of versions), in one sentence (`summary`), and `best` when one is clearly ahead on replies without clearly more opt-outs. A later step has `follows_step`: it follows that step's split.

fromqueryRFC 3339, or a date for midnight UTC. Absent: since it began.
toqueryUp to, not including. Absent: now.
variantqueryOnly this version of each step's text, like `A`.
curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/stats \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "automation_id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21",
  "from": "2026-08-28T00:00:00Z",
  "to": "2026-09-27T00:00:00Z",
  "recorded_since": null,
  "from_recorded_since": false,
  "variant": null,
  "reply_window_days": 7,
  "totals": { "sent": 118, "delivered": 104, "read": 61, "replied": 19, "opted_out": 2, "failed": 3, "cancelled": 6, "pending": 0, "reached": 105, "reply_rate": 0.181, "opt_out_rate": 0.019 },
  "steps": [{
    "step": 1, "text": "Hi {{first_name}}, still looking?", "in_flow": true,
    "sent": 40, "delivered": 37, "read": 22, "replied": 11, "opted_out": 0, "failed": 1, "cancelled": 0, "pending": 0,
    "reached": 37, "reply_rate": 0.2973, "opt_out_rate": 0.0, "variants": [],
    "test": null
  }, {
    "step": 4, "text": "Thanks for coming by today!", "in_flow": true,
    "sent": 112, "delivered": 108, "read": 60, "replied": 22, "opted_out": 2, "failed": 0, "cancelled": 0, "pending": 0,
    "reached": 108, "reply_rate": 0.2037, "opt_out_rate": 0.0185,
    "variants": [{ "variant": "A", "sent": 60, "replied": 7 }, { "variant": "B", "sent": 52, "replied": 15 }],
    "test": {
      "since": null,
      "follows_step": null,
      "versions": [
        { "variant": "A", "text": "Thanks for coming by today!", "weight": 50, "sent": 60, "delivered": 58, "read": 31, "replied": 7, "reply_rate": 0.1207, "opted_out": 1, "opt_out_rate": 0.0172, "reached": 58, "pending": 0 },
        { "variant": "B", "text": "Want a second look at the unit?", "weight": 50, "sent": 52, "delivered": 50, "read": 29, "replied": 15, "reply_rate": 0.3, "opted_out": 1, "opt_out_rate": 0.02, "reached": 50, "pending": 0 }
      ],
      "verdict": {
        "status": "ready",
        "summary": "B gets more replies (30% vs 12%), and opt-outs are about the same.",
        "minimum_per_version": 30, "more_per_version": null,
        "replies": { "leader": "B", "leader_rate": 0.3, "against": "A", "against_rate": 0.1207, "z": 2.31, "p": null, "clear": true },
        "opt_outs": { "leader": "B", "leader_rate": 0.02, "against": "A", "against_rate": 0.0172, "z": 0.11, "p": 1.0, "clear": false },
        "best": "B"
      }
    }
  }],
  "variants": [],
  "runs": {
    "started": 40, "in_progress": 9, "finished": 31,
    "outcomes": { "completed": 12, "replied": 15, "stopped": 2, "number_left": 0, "gave_up": 0, "failed": 2, "unknown": 0 }
  }
}
  • 400An unreadable `from` or `to`, `from` not before `to`, or a variant that is not one to eight letters or digits.
  • 404The automation is not this project's.

Which texts got the replies: every reply, the message it answered, and a link to the conversation.

Newest first, a page at a time. `total` and `people` count every page; pass `next_cursor` as `cursor` for the next, and it is null on the last. `opted_out` marks a reply that opted them out; `said_stop` one that said STOP to a project that handles opt-outs itself, which is not counted as an opt-out.

stepqueryOnly replies to this step's messages: 1 to 40.
fromqueryOnly replies to messages sent from this time, as for stats.
toqueryAnd before this one.
variantqueryOnly replies to this version of the text.
cursorquery`next_cursor` from the page before.
limitquery1 to 100, 25 by default.
curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/replies \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [{
    "id": "4b1e2c3d-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
    "step": 4,
    "variant": null,
    "handle": "+15551230002",
    "contact_name": "Dana Whitfield",
    "text": "Sorry, just saw this. Is Saturday open?",
    "received_at": "2026-09-26T15:02:11Z",
    "opted_out": false,
    "said_stop": false,
    "answered": { "message_id": "9a0c1b2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d", "text": "Last one from us, Dana.", "sent_at": "2026-09-26T14:00:03Z" },
    "conversation": { "number_id": "8f14e45f-ceea-467a-9a1b-1b1e5f9e2c3d", "chat_id": "iMessage;-;+15551230002", "link_code": "k7Qm3xT9pa", "link": "/c/k7Qm3xT9pa" }
  }],
  "total": 23,
  "people": 19,
  "next_cursor": "1790000000123456_4b1e2c3d-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
  "variants": []
}
  • 400A step that is not 1 to 40, a limit out of range, a cursor this route did not write, or an unreadable date.
  • 404The automation is not this project's.

Take one person out of one automation. Everybody else carries on.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/runs/stop \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "+15551230002"
  }'

Response

{ "stopped": 1 }

Use this one: a step's version for everybody who reaches it from now on.

Sets that version's weight to 100 and the others' to 0. Everybody who reaches the step from now on gets it, including people given another version earlier (their own version still decides the other steps). Messages already sent stay as they were, and every version's text and results stay. `step` is the step's number from 1, as the results number it, or its id. Answers with the automation.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/steps/4/variants/B/use \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "steps": [{ "kind": "send", "body": "Thanks for coming by today!", "variants": [{ "letter": "A", "body": "Thanks for coming by today!", "weight": 0 }, { "letter": "B", "body": "Want a second look at the unit?", "weight": 100 }] }] }
  • 400The step is not a message, or has one text.
  • 404No such step or version, or the automation is not this project's.

Change the weights of the first step with versions, for people given one from now on.

Every version of the step, in letter order or by letter, adding up to 100. Anybody already given a version keeps it. Only the first step with versions has weights: a later step follows its split, and is 400. Answers with the automation.

weights*integer[] | object`[70, 30]`, or `{"A": 70, "B": 30}`.
curl -X PUT https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automations/e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21/steps/4/weights \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "weights": [
      70,
      30
    ]
  }'

Response

{ "id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "steps": [{ "kind": "send", "variants": [{ "letter": "A", "weight": 70 }, { "letter": "B", "weight": 30 }] }] }
  • 400Weights that do not add up to 100, miss a version, or name one the step does not have; a step that is not a message with versions; a later step, which follows the first step's split.
  • 404No such step, or the automation is not this project's.

Which automations are messaging one person right now.

handle*queryPhone number or email.
curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/automation-runs \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "total": 1,
  "data": [{ "run_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "automation_id": "e7d1c3a9-2b4f-4e6a-8c0d-1f3a5b7c9e21", "automation_name": "Welcome", "due_at": "2026-09-28T14:00:00Z" }]
}

Lists in this project, and how many people are on each.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [{ "id": "c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68", "name": "Spring customers", "description": null, "members": 118, "created_at": "2026-09-01T10:00:00Z" }]
}

Make a list.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Spring customers",
    "description": "Bought in March or April"
  }'

Response

{ "id": "c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68", "name": "Spring customers", "description": "Bought in March or April", "members": 0, "created_at": "2026-09-01T10:00:00Z" }
  • 409This project already has a list with that name.

Rename a list or change its description.

curl -X PATCH https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists/c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "saved": true }

Delete a list. The people on it are not deleted.

curl -X DELETE https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists/c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "deleted": true }

Who is on a list, newest first. Up to 500.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists/c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68/members \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "data": [{ "handle": "+15551230002", "source": "api", "added_at": "2026-09-26T13:50:00Z" }] }

Add people to a list.

If an automation starts on this list, each person who was not already on it starts that automation, which messages them. `started` says how many. Up to 5,000 per call.

handles*string[]Phone numbers or emails.
sourcestring`manual`, `import` or `api`. `api` by default.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists/c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68/members \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handles": [
      "+15551230002",
      "+15551230003"
    ]
  }'

Response

{ "added": 2, "already": 0, "started": 0 }

Take somebody off a list.

An automation they are already in carries on. To stop messaging somebody, record an opt-out or stop their run.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/lists/c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68/remove \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "handle": "+15551230002"
  }'

Response

{ "removed": true }

Tags in this project, and how many people carry each.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/tags \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "data": [{ "tag": "vip", "people": 14 }] }

Tag people.

Tags are stored lowercase with single spaces, so `VIP ` and `vip` are one tag. If an automation starts on this tag, each newly tagged person starts it, which messages them.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/tags \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tag": "vip",
    "handles": [
      "+15551230002"
    ]
  }'

Response

{ "added": 1, "already": 0, "started": 0 }

Take a tag off somebody.

curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/tags/remove \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tag": "vip",
    "handle": "+15551230002"
  }'

Response

{ "removed": true }

The lists one person is on and the tags they carry.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/audience/%2B15551230002 \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{ "lists": [{ "id": "c2a7e1d4-6b3f-4a8e-9d1c-5e7f0a2b4c68", "name": "Spring customers" }], "tags": ["vip"] }

The variables a message body can use in this project, built-in and custom.

The built-in ones (`kind: built_in`), then every custom field name this project's contacts carry (`kind: custom`), each once, sorted, up to 200. `name` is what to write between the braces: a field whose name is not a plain word is written with brackets, like contact.custom["Checkout link"]. A field is listed because some contact has it; the preview says how many of the people you are sending to have it empty.

curl -X GET https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/templates/variables \
  -H "Authorization: Bearer $MISS_BLUE_KEY"

Response

{
  "data": [
    { "name": "contact.name", "about": "Their name from your contacts", "kind": "built_in" },
    { "name": "contact.first_name", "about": "The first word of their name", "kind": "built_in" },
    { "name": "contact.last_name", "about": "The rest of their name, after the first word", "kind": "built_in" },
    { "name": "contact.phone_number", "about": "The number this is going to", "kind": "built_in" },
    { "name": "system.24hr", "about": "The time it sends, like 14:30", "kind": "built_in" },
    { "name": "contact.custom[\"Checkout link\"]", "about": "A custom field on their contact", "kind": "custom", "field": "Checkout link" },
    { "name": "contact.custom.product", "about": "A custom field on their contact", "kind": "custom", "field": "product" }
  ]
}

Show what a body will say for one person, and who it would leave a gap for.

The same renderer the sender uses. `text` is the body filled in for `handle` (or the first of `handles`). `missing` counts, for each variable, how many of `handles` have nothing for it, up to 500 people. A body that will not parse comes back with `text: null` and one line in `error`.

body*stringLiquid.
handlestringRender it as this person would read it.
handlesstring[]Count empty variables across these people.
curl -X POST https://api.missblue.dev/v1/projects/3c9a1f77-2d64-4b0e-8a5c-7e1d4b2f9a60/templates/preview \
  -H "Authorization: Bearer $MISS_BLUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi {{ contact.first_name | default: \"there\" }}, your {{ contact.custom.product | default: \"order\" }} is ready.",
    "handles": [
      "+15551230002",
      "+15551230003"
    ]
  }'

Response

{
  "text": "Hi Ari, your Blue hoodie is ready.",
  "error": null,
  "variables": ["contact.custom.product", "contact.first_name"],
  "unresolved": [],
  "missing": [{ "name": "contact.custom.product", "people": 1 }],
  "counted": 2
}
NextDeliveryWebhooks and per-message callbacks.