Endpoint reference

Inbox placement tests

Start placement tests and batches, read where each copy landed, manage the workspace's seed inboxes, and schedule a campaign's placement monitor.

An inbox placement test sends a template or a campaign step from one of the workspace's mailboxes to a panel of seed inboxes and reports where each copy landed: the primary inbox, a Gmail tab, spam, or nowhere. The placement tests guide explains how copies are rendered, sent and found, and how to read a result.

Every route is scoped to the selected organization (API keys are always bound to one), and every id in a request is checked against it: a sending mailbox, campaign, step or contact from another workspace is a 404. Errors follow the shared {error, message, code, request_id} envelope; the codes these routes add are listed under placement test refusals.

Changes are pushed live as a PLACEMENT_TEST_UPDATED event on the organization's realtime channel when a test starts, on every new verdict, and when it finishes or is cancelled. A batch sends the same event with batch_id instead of test_id when it starts, skips or defers a mailbox, and finishes.

Get the overview

GET /placement/overview

Returns the seed panels this workspace can test on, with each panel's seed count and provider mix, and the workspace's monthly allowance.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

Response

{
  "data": {
    "panels": [
      {
        "panel": "instance",
        "available": true,
        "seeds": 38,
        "families": [
          { "family": "gmail", "label": "Gmail", "seeds": 10 },
          { "family": "google_workspace", "label": "Google Workspace", "seeds": 8 },
          { "family": "microsoft365", "label": "Microsoft 365", "seeds": 8 },
          { "family": "outlook", "label": "Outlook.com", "seeds": 6 },
          { "family": "yahoo", "label": "Yahoo Mail", "seeds": 6 }
        ],
        "metered": true
      },
      {
        "panel": "workspace",
        "available": false,
        "reason": "Mark one of this workspace's mailboxes as a seed inbox to test on it.",
        "seeds": 0,
        "families": [],
        "metered": false
      }
    ],
    "usage": {
      "used": 4,
      "limit": 40,
      "credits_per_test": 25,
      "credit_balance": 1840,
      "period_start": "2026-09-01T00:00:00Z",
      "period_end": "2026-10-01T00:00:00Z"
    },
    "workspace_seeds": 0,
    "seeds_per_test": 20,
    "spacing_seconds": 60
  }
}
FieldDescription
panels[].panelinstance (the operator's panel), workspace (the workspace's own seed inboxes) or cloud (Warmbly Cloud's panel). cloud is listed on a self-hosted instance, and elsewhere only when it is available
panels[].availableWhether a test can run on the panel now. reason says why not, in one sentence
panels[].familiesSeeds per provider family. family is a stable id (gmail, google_workspace, microsoft365, outlook, yahoo, aol, icloud, zoho, fastmail, gmx, proton, yandex, other and more); label is its display name
panels[].meteredWhether a test on the panel counts against a monthly allowance
usageFree tests used this calendar month (UTC) against limit, the month's free tests. A test paid in credits, or one that finished without delivering a copy, is not counted. limit is null when the instance does not meter tests, which is always the case on a self-hosted instance
usage.credits_per_testWhat a test on the instance panel costs in credits once the free tests are used up. 0 when tests cannot be paid for, and then a workspace past its free tests waits for the next month
usage.credit_balanceThe workspace's spendable credits, null when credits are off
seeds_per_testThe most seeds one test sends to
spacing_secondsThe average gap between two copies from one sender

Start a test

POST /placement/tests

Starts a test, or two linked tests for a tracking comparison. Every check (entitlement, allowance, tests in flight, the sender's state and daily limit, available seeds) runs before anything is scheduled, so a refused request sends nothing and charges nothing. A paid test is charged when it starts and refunded by itself if no copy is delivered; an insufficient_credits (402) or usage_cap_exceeded (429) refusal charges nothing. Supports Idempotency-Key, and a retry with the same key is never charged twice.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Request body

FieldTypeRequiredDescription
sender_account_iduuidyesThe mailbox that sends every copy. It must be connected and active, cannot be a seed inbox, and must be one an API key with a mailbox allowlist may use
campaign_iduuidnoTest with this campaign's settings: its opt-out line, tracking and attachments
sequence_iduuidnoA step of campaign_id. Its subject and body are used when the request carries neither. Needs campaign_id
contact_iduuidnoThe contact merge fields and AI blocks are rendered for. Defaults to the campaign's first lead, except on the cloud panel, where it defaults to a placeholder so no lead's details leave the instance unless you choose one; AI blocks are left out when no contact is used
subjectstringnoThe subject. Required when there is no step to take it from
body_htmlstringnoHTML body. A test needs this or body_plain
body_plainstringnoPlain-text body
trackingstringnocampaign (default: the campaign's own open and click tracking, none for an ad-hoc test), on, off, or compare (one untracked and one tracked test to the same seeds, each seed getting the same rendered copy from both)
panelstringnoinstance (default), workspace or cloud
familiesstring[]noOnly seeds at these provider families, as panels[].families[].family lists them. Works on every panel; on instance and cloud it is the only way to narrow a test, since their addresses are masked. A panel with no seed at those providers is refused with placement_no_seeds
pacestringnospaced (default, about 60 seconds between copies) or quick (about 8 seconds, so a test sends in minutes)
max_creditsintegernoThe most credits you agree to pay for the test when the month's free tests on the instance panel are used up. A request whose price is higher, or that omits it, is refused with placement_quota_exceeded, whose message names the price, and nothing is charged. Ignored while free tests are left
seed_idsuuid[]noOnly with panel workspace: send to these seed inboxes of the workspace and no others. Each must be a seed inbox of the workspace that is connected and running (placement_invalid_seeds otherwise, naming any that are not), and each gets a copy: seeds_per_test does not cap a chosen list. When the sender's daily limit cannot pay for all of them the request is refused with placement_daily_budget rather than dropping some. One on the sender's domain is skipped as usual. Omit it to let the test pick
{
  "sender_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
  "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
  "sequence_id": "c7d8e9f0-0000-0000-0000-000000000002",
  "tracking": "compare"
}

Response

201 Created with the new tests: one, or two sharing a compare_group_id for a comparison. Their copies start going out within seconds.

{
  "data": [
    {
      "id": "d1e2f3a4-0000-0000-0000-000000000010",
      "sender_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
      "sender_email": "[email protected]",
      "created_by": "9c2a0000-0000-0000-0000-000000000001",
      "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
      "sequence_id": "c7d8e9f0-0000-0000-0000-000000000002",
      "contact_id": "e5f6a7b8-0000-0000-0000-000000000004",
      "monitor_id": null,
      "subject": "Quick question about outbound at Acme",
      "open_tracking": false,
      "link_tracking": false,
      "compare_group_id": "f0f1f2f3-0000-0000-0000-000000000020",
      "origin": "manual",
      "panel": "instance",
      "status": "running",
      "pace": "spaced",
      "credits_charged": 0,
      "credits_refunded": 0,
      "credits_settled_at": null,
      "created_at": "2026-09-25T14:00:00Z",
      "finished_at": null,
      "summary": {
        "total": 38, "pending": 38, "inbox": 0, "promotions": 0, "other": 0, "spam": 0,
        "missing": 0, "failed": 0, "cancelled": 0, "delivered": 0,
        "inbox_rate": null, "tabs_rate": null, "spam_rate": null, "missing_rate": null
      },
      "families": []
    }
  ]
}

The second element (omitted above) is the tracked half, with open_tracking and link_tracking set. A comparison counts as two tests against the allowance and the limit on tests in flight, and sends twice as many copies.

List tests

GET /placement/tests

Returns the workspace's tests, newest first, each with its counts. The copy's bodies are left out of the list.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

ParameterInTypeDescription
limitqueryinteger1 to 100, default 25. Anything else is a 400
cursorquerystringOpaque cursor from a previous pagination.next_cursor. One this API did not issue is a 400
campaign_idqueryuuidOnly tests of this campaign

Tests a batch started are left out; read them through their batch.

Response

{
  "data": [
    {
      "id": "d1e2f3a4-0000-0000-0000-000000000010",
      "sender_email": "[email protected]",
      "status": "completed",
      "panel": "instance",
      "origin": "manual",
      "finished_at": "2026-09-25T16:20:00Z",
      "summary": {
        "total": 38, "pending": 0, "inbox": 27, "promotions": 4, "other": 1, "spam": 4,
        "missing": 2, "failed": 0, "cancelled": 0, "delivered": 38,
        "inbox_rate": 0.7105, "tabs_rate": 0.1316, "spam_rate": 0.1053, "missing_rate": 0.0526
      },
      "families": [
        {
          "family": "gmail",
          "label": "Gmail",
          "counts": {
            "total": 10, "pending": 0, "inbox": 5, "promotions": 4, "other": 1, "spam": 0,
            "missing": 0, "failed": 0, "cancelled": 0, "delivered": 10,
            "inbox_rate": 0.5, "tabs_rate": 0.5, "spam_rate": 0, "missing_rate": 0
          }
        }
      ]
    }
  ],
  "pagination": { "total": 12, "has_more": false, "next_cursor": null }
}

Each test carries the fields shown under start a test; the example trims them.

FieldDescription
statusrunning, completed, cancelled or failed. A test fails when no copy was delivered at all, with the reason in error
originmanual (started by a person or a key), monitor (a campaign's placement monitor), batch (a placement batch, named by batch_id), admin (an operator) or remote
batch_idThe batch that started the test, null otherwise
pacespaced or quick
credits_chargedCredits the test cost past the month's free tests, 0 for a free test
credits_refundedCredits a paid test that delivered no copy got back. Monthly credits whose allowance has reset since the charge are not returned, so this can be less than credits_charged
credits_settled_atWhen a paid test's charge was settled after it finished: refunded if it delivered nothing, kept otherwise. null for a free test and until then
summaryCounts over every copy. delivered is inbox + promotions + other + spam + missing: every copy that left the sender and got a verdict
summary.*_rateFractions from 0 to 1 of delivered, null until something is delivered. tabs_rate covers promotions and other
familiesThe same counts per provider family

Get a test

GET /placement/tests/:id

Returns one test with its copy, every seed's result, the content check of the copy and, for a tracking comparison, the other half.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

Response

{
  "data": {
    "id": "d1e2f3a4-0000-0000-0000-000000000010",
    "subject": "Quick question about outbound at Acme",
    "body_html": "<p>Hi Dana,</p><p>...</p>",
    "body_plain": "Hi Dana,\n\n...",
    "status": "completed",
    "panel": "instance",
    "summary": { "total": 38, "inbox": 27, "delivered": 38, "inbox_rate": 0.7105 },
    "families": [],
    "results": [
      {
        "seed": "a***@gmail.com",
        "family": "gmail",
        "family_label": "Gmail",
        "folder": "promotions",
        "scheduled_at": "2026-09-25T14:00:05Z",
        "sent_at": "2026-09-25T14:00:06Z",
        "detected_at": "2026-09-25T14:01:30Z"
      }
    ],
    "content": {
      "score": 84,
      "issues": []
    }
  }
}

The test's own fields are the ones listed under list tests, plus body_html and body_plain; the example trims them.

FieldDescription
results[].seedThe seed's address, masked on the instance and cloud panels and shown in full on workspace
results[].folderpending, inbox, promotions, other (another Gmail tab: Updates, Social or Forums), spam, missing (not seen within 120 minutes of sent_at), failed (never left the sender; error says why) or cancelled
contentThe advisory content score of the test's copy, the same shape as score template content
comparePresent only on a tracking comparison: the other half, with its counts and without its results

Cancel a test

POST /placement/tests/:id/cancel

Stops the copies that have not been sent yet. Copies already sent keep being classified. Supports Idempotency-Key.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Response

200 OK with the test, in the list shape. A test that has already finished answers 409 with placement_not_running.

Start a batch

POST /placement/batches

Runs the same test from many sending mailboxes. The request is validated as a whole (the copy, panel, tracking, allowance and credits agreed), the senders are resolved and written down, and the batch is answered queued right away. Nothing is sent by the request itself: the backend starts the senders a few at a time, each with its own test, checking each one's daily limit, connection and worker when its turn comes. See testing a fleet with batches. Supports Idempotency-Key.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Request body

The copy fields are the ones start a test takes: campaign_id, sequence_id, contact_id, subject, body_html, body_plain, tracking, panel, families and seed_ids. pace is always spaced for a batch; quick is a 400. The copy is resolved once, when the batch is created, so every sender tests the same email. Instead of sender_account_id a batch takes exactly one of:

FieldTypeDescription
sender_account_idsuuid[]These mailboxes. Duplicates are dropped; one that is not in the workspace, or is a seed inbox, is a 404, and one an API key with a mailbox allowlist may not use is a 403. A disconnected one is kept and deferred or skipped when its turn comes
sender_scopeobjectMailboxes resolved on the server, so a fleet of thousands needs no id list
sender_scope.typestringcampaign (every mailbox campaign_id's scheduler sends from) or workspace (every mailbox of the workspace)
sender_scope.campaign_iduuidRequired with type campaign
sender_scope.providersstring[]Only mailboxes hosted by these families, such as google_workspace, gmail, microsoft365, outlook or other
sender_scope.domainsstring[]Only mailboxes sending from these domains
sender_scope.tag_idsuuid[]Only mailboxes carrying one of these tags
sender_scope.include_inactivebooleanKeep disconnected mailboxes. Default false
sender_scope.untested_daysintegerOnly mailboxes with no finished placement test in this many days, 1 to 365
FieldTypeRequiredDescription
sampleobjectnoWhich of the resolved mailboxes to keep. Default {"mode": "all"}
sample.modestringnoall, random (count mailboxes), percent (percent of them, rounded up), per_domain or per_provider (up to count from each)
sample.stratifystringnoWith random or percent only: provider or domain splits the sample in proportion to each group's share
on_unavailablestringnodefer (default: retry a mailbox that cannot send when its turn comes, for up to seven days) or skip
max_creditsintegernoThe most credits you agree to pay across the whole batch for tests past the month's free ones. Needed only when the batch needs more tests than are free: the request is refused with placement_quota_exceeded, naming the most it can cost, when it is lower. The batch never spends more
{
  "sender_scope": { "type": "campaign", "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001", "providers": ["google_workspace"] },
  "sample": { "mode": "percent", "percent": 10, "stratify": "domain" },
  "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
  "sequence_id": "c7d8e9f0-0000-0000-0000-000000000002",
  "tracking": "compare"
}

Response

201 Created with the batch.

{
  "data": {
    "id": "a9b8c7d6-0000-0000-0000-000000000030",
    "created_by": "9c2a0000-0000-0000-0000-000000000001",
    "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
    "sequence_id": "c7d8e9f0-0000-0000-0000-000000000002",
    "contact_id": "e5f6a7b8-0000-0000-0000-000000000004",
    "subject": "Quick question about outbound at Acme",
    "tracking": "compare",
    "panel": "instance",
    "pace": "spaced",
    "families": [],
    "seed_ids": [],
    "on_unavailable": "defer",
    "selection": {
      "sender_scope": { "type": "campaign", "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001", "providers": ["google_workspace"] },
      "sample": { "mode": "percent", "percent": 10, "stratify": "domain" },
      "matched": 1126
    },
    "sender_count": 113,
    "max_credits": 0,
    "credits_spent": 0,
    "status": "queued",
    "retry_until": "2026-10-02T14:00:00Z",
    "created_at": "2026-09-25T14:00:00Z",
    "started_at": null,
    "finished_at": null,
    "progress": { "total": 113, "queued": 113, "deferred": 0, "running": 0, "completed": 0, "skipped": 0, "failed": 0, "cancelled": 0 },
    "summary": { "total": 0, "pending": 0, "inbox": 0, "promotions": 0, "other": 0, "spam": 0, "missing": 0, "failed": 0, "cancelled": 0, "delivered": 0, "inbox_rate": null, "tabs_rate": null, "spam_rate": null, "missing_rate": null }
  }
}
FieldDescription
statusqueued until the first mailbox starts, then running; at the end completed, completed_with_warnings (some mailboxes were skipped or failed), failed (none finished a test, with error) or cancelled
selectionHow the mailboxes were chosen, and how many the scope matched before sampling. The mailboxes themselves are the snapshot under list a batch's senders
progressThe batch's mailboxes by status. running is a mailbox whose test has not finished yet
summaryWhere the batch's copies landed so far, over the copy the campaign really sends: the tracked half of a tracking comparison
retry_untilDeferred mailboxes are retried until then, then skipped

Preview a batch

POST /placement/batches/preview

Takes the body of start a batch and returns what it would come to, without writing anything or sending anything. The copy fields may be left out to count the mailboxes alone; once any is given, the copy is checked as on start. A random sample is drawn again when the batch starts, so the counts are the same but the mailboxes can differ.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Response

{
  "data": {
    "matched": 1347, "selected": 135, "inactive": 2, "domains": 41,
    "providers": [
      { "key": "google_workspace", "label": "Google Workspace", "senders": 80 },
      { "key": "other", "label": "Other provider", "senders": 40 },
      { "key": "microsoft365", "label": "Microsoft 365", "senders": 15 }
    ],
    "variants": 2, "tests": 270, "seeds_per_test": 20, "max_sends": 5400,
    "metered": true, "free_tests": 40, "paid_tests": 230, "credits": 5750,
    "usage": { "used": 0, "limit": 40, "credits_per_test": 25, "credit_balance": 9000, "period_start": "2026-09-01T00:00:00Z", "period_end": "2026-10-01T00:00:00Z" },
    "senders_max": 10000, "concurrency": 20
  }
}
FieldDescription
variants2 for a tracking comparison, which doubles tests and max_sends
max_sendsThe most copies the batch sends: tests times seeds_per_test. Each mailbox's daily limit can only make it fewer
free_tests, paid_tests, creditsHow tests splits against the month's free tests, and the most the paid ones cost. All 0 on an unmetered panel
concurrencyHow many of the workspace's batch mailboxes send at once

List batches

GET /placement/batches

Returns the workspace's batches, newest first, in the shape above. Takes limit (1 to 100, default 25) and an opaque cursor, and returns data plus pagination.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

Get a batch

GET /placement/batches/:id

Returns one batch with its placement grouped across the fleet.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

Response

The batch's fields as above, plus:

FieldDescription
untrackedOn a tracking comparison only: the untracked half's counts. summary is the tracked half
domainsPer sending domain, worst inbox rate first: key (the domain), senders in the batch, tested (senders that finished a test) and counts
providersThe same per sending provider, key a host family and label its name
recipientsCounts per recipient provider family, like a test's families
matrixPer sending domain, in the order of domains, its counts per recipient provider in the order of recipients
contentThe content score of the batch's copy

The groups read the same copy as summary. They are not broken down by worker: a worker signs in to the mailbox provider, which delivers from its own servers, so a recipient's filter never sees it.

List a batch's senders

GET /placement/batches/:id/senders

Returns the batch's mailboxes with where each one's copies landed.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

ParameterInTypeDescription
limitqueryinteger1 to 100, default 25
cursorquerystringOpaque cursor from a previous pagination.next_cursor
sortquerystringworst (default: lowest inbox rate first, untested last), best, email or status
statusquerystringOnly mailboxes in this status: queued, deferred, running, completed, skipped, failed or cancelled
qquerystringOnly addresses containing this text

Response

{
  "data": [
    {
      "id": "c0c1c2c3-0000-0000-0000-000000000040",
      "batch_id": "a9b8c7d6-0000-0000-0000-000000000030",
      "email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
      "sender_email": "[email protected]",
      "sender_domain": "domain2.com",
      "sender_family": "google_workspace",
      "sender_family_label": "Google Workspace",
      "status": "completed",
      "attempts": 1,
      "next_attempt_at": "2026-09-25T14:00:00Z",
      "started_at": "2026-09-25T14:02:00Z",
      "finished_at": "2026-09-25T16:40:00Z",
      "summary": { "total": 20, "pending": 0, "inbox": 8, "promotions": 0, "other": 0, "spam": 12, "missing": 0, "failed": 0, "cancelled": 0, "delivered": 20, "inbox_rate": 0.4, "tabs_rate": 0, "spam_rate": 0.6, "missing_rate": 0 },
      "test_ids": ["d1e2f3a4-0000-0000-0000-000000000011", "d1e2f3a4-0000-0000-0000-000000000012"]
    }
  ],
  "pagination": { "total": 113, "has_more": true, "next_cursor": "MjU" }
}
FieldDescription
statusqueued, deferred (waiting to retry at next_attempt_at), running, completed, skipped, failed or cancelled
reason, detailWhy a mailbox was deferred, skipped or failed: a code from placement test refusals and its sentence
test_idsThe mailbox's tests, readable with get a test: the untracked half first on a comparison

Cancel a batch

POST /placement/batches/:id/cancel

Stops the batch: no mailbox starts again, copies not sent yet are cancelled, and copies already sent keep being classified.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Response

200 OK with the batch. A batch that has already finished answers 409 with placement_batch_not_running.

Get fleet coverage

GET /placement/coverage

How many of the workspace's connected sending mailboxes finished a placement test recently.

Auth: Scope READ_ANALYTICS · Org permission view_analytics

Response

{
  "data": { "mailboxes": 1347, "tested_7d": 312, "tested_30d": 947, "never_tested": 126 }
}

List seed inboxes

GET /placement/seeds

Returns every mailbox of the workspace with whether it is one of the workspace's seed inboxes. A key restricted to certain mailboxes sees only those.

Auth: Scope READ_EMAILS · Org permission view_campaigns

Response

{
  "data": [
    {
      "email_account_id": "a0a1a2a3-0000-0000-0000-000000000009",
      "email": "[email protected]",
      "family": "gmail",
      "label": "Gmail",
      "status": "active",
      "seed": true
    },
    {
      "email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
      "email": "[email protected]",
      "family": "google_workspace",
      "label": "Google Workspace",
      "status": "active",
      "seed": false,
      "blocker": "A placement test is sending from this mailbox"
    }
  ]
}

blocker is present when the mailbox cannot become a seed inbox right now, and says why.

Mark a seed inbox

PUT /placement/seeds/:email_account_id

Makes a workspace mailbox one of its seed inboxes, or an ordinary mailbox again. A seed never warms up (its warmup is turned off here, and starting it again is refused with mailbox_is_seed), never sends campaign mail, and cannot send a placement test. The body states the value to hold, so a retry lands on the same state.

Auth: Scope WRITE_EMAILS · Org permission manage_emails

Request body

FieldTypeRequiredDescription
seedbooleanyestrue to make the mailbox a seed inbox, false to make it an ordinary mailbox
{ "seed": true }

Response

200 OK with the mailbox, in the shape of list seed inboxes. A mailbox on the instance panel, or one a test is still sending from, answers 409 with placement_seed_unavailable; a workspace that already has 50 seed inboxes answers placement_seed_limit.

Get a campaign's placement monitor

GET /campaigns/:id/placement-monitor

Returns the campaign's scheduled placement test, or null when it has none.

Auth: Scope READ_CAMPAIGNS · Org permission view_campaigns

Response

{
  "data": {
    "id": "a9b8c7d6-0000-0000-0000-000000000030",
    "campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
    "created_by": "9c2a0000-0000-0000-0000-000000000001",
    "enabled": true,
    "interval_days": 7,
    "panel": "instance",
    "alert_below": 70,
    "pause_on_alert": false,
    "next_run_at": "2026-10-02T14:05:00Z",
    "last_run_at": "2026-09-25T14:05:00Z",
    "last_test_id": "d1e2f3a4-0000-0000-0000-000000000010",
    "last_alert_at": null,
    "created_at": "2026-09-18T14:00:00Z",
    "updated_at": "2026-09-25T14:05:00Z"
  }
}

last_error is present when the last run could not start a test, and says why (for example, the campaign was not running); the monitor then tries again a day later.

Set a campaign's placement monitor

PUT /campaigns/:id/placement-monitor

Creates or updates the campaign's monitor. Every field is optional: an absent field keeps its current value, or its default on a new monitor. The body states values rather than changes, so a retry lands on the same state. A new or re-enabled monitor runs its first test within about five minutes; changing interval_days moves the next run to that interval after the last one.

Each run tests the campaign's first email step with a subject and a body, rendered for its first lead with the campaign's own tracking, from the next of the campaign's sending mailboxes in turn. It runs only while the campaign is active, and counts against the allowance and the mailbox's daily limit like any other test.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Request body

FieldTypeDefaultDescription
enabledbooleantrueWhether the monitor runs
interval_daysinteger7Days between runs, 1 to 30
panelstringinstanceinstance, workspace or cloud
alert_belowinteger70Inbox percentage, 0 to 100. A run whose primary-inbox share lands below it notifies every member who can view campaigns with a placement_alert notification
pause_on_alertbooleanfalseAlso pause the campaign when a run alerts. The campaign moves to paused_guardrail with the reason in its activity log and stays paused until it is started again
{ "interval_days": 3, "alert_below": 80, "pause_on_alert": true }

Response

200 OK with the monitor, in the shape above.

Remove a campaign's placement monitor

DELETE /campaigns/:id/placement-monitor

Removes the campaign's monitor. Tests it already ran stay in the list.

Auth: Scope SEND_CAMPAIGNS · Org permission send_campaigns

Response

204 No Content. A campaign with no monitor answers 404.

On this page