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
}
}| Field | Description |
|---|---|
panels[].panel | instance (Zexr's shared panel) or workspace (the workspace's own seed inboxes) |
panels[].available | Whether a test can run on the panel now. reason says why not, in one sentence |
panels[].families | Seeds 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[].metered | Whether a test on the panel counts against a monthly allowance |
usage | Free 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 tests are not metered |
usage.credits_per_test | What 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_balance | The workspace's spendable credits, null when credits are off |
seeds_per_test | The most seeds one test sends to |
spacing_seconds | The 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
| Field | Type | Required | Description |
|---|---|---|---|
sender_account_id | uuid | yes | The 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_id | uuid | no | Test with this campaign's settings: its opt-out line, tracking and attachments |
sequence_id | uuid | no | A step of campaign_id. Its subject and body are used when the request carries neither. Needs campaign_id |
contact_id | uuid | no | The 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 |
subject | string | no | The subject. Required when there is no step to take it from |
body_html | string | no | HTML body. A test needs this or body_plain |
body_plain | string | no | Plain-text body |
tracking | string | no | campaign (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) |
panel | string | no | instance (default), workspace or cloud |
families | string[] | no | Only 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 |
pace | string | no | spaced (default, about 60 seconds between copies) or quick (about 8 seconds, so a test sends in minutes) |
max_credits | integer | no | The 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_ids | uuid[] | no | Only 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": "sales@yourdomain.com",
"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
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | 1 to 100, default 25. Anything else is a 400 |
cursor | query | string | Opaque cursor from a previous pagination.next_cursor. One this API did not issue is a 400 |
campaign_id | query | uuid | Only 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": "sales@yourdomain.com",
"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.
| Field | Description |
|---|---|
status | running, completed, cancelled or failed. A test fails when no copy was delivered at all, with the reason in error |
origin | manual (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_id | The batch that started the test, null otherwise |
pace | spaced or quick |
credits_charged | Credits the test cost past the month's free tests, 0 for a free test |
credits_refunded | Credits 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_at | When 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 |
summary | Counts over every copy. delivered is inbox + promotions + other + spam + missing: every copy that left the sender and got a verdict |
summary.*_rate | Fractions from 0 to 1 of delivered, null until something is delivered. tabs_rate covers promotions and other |
families | The 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.
| Field | Description |
|---|---|
results[].seed | The seed's address, masked on the instance and cloud panels and shown in full on workspace |
results[].folder | pending, 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 |
content | The advisory content score of the test's copy, the same shape as score template content |
compare | Present 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:
| Field | Type | Description |
|---|---|---|
sender_account_ids | uuid[] | 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_scope | object | Mailboxes resolved on the server, so a fleet of thousands needs no id list |
sender_scope.type | string | campaign (every mailbox campaign_id's scheduler sends from) or workspace (every mailbox of the workspace) |
sender_scope.campaign_id | uuid | Required with type campaign |
sender_scope.providers | string[] | Only mailboxes hosted by these families, such as google_workspace, gmail, microsoft365, outlook or other |
sender_scope.domains | string[] | Only mailboxes sending from these domains |
sender_scope.tag_ids | uuid[] | Only mailboxes carrying one of these tags |
sender_scope.include_inactive | boolean | Keep disconnected mailboxes. Default false |
sender_scope.untested_days | integer | Only mailboxes with no finished placement test in this many days, 1 to 365 |
| Field | Type | Required | Description |
|---|---|---|---|
sample | object | no | Which of the resolved mailboxes to keep. Default {"mode": "all"} |
sample.mode | string | no | all, random (count mailboxes), percent (percent of them, rounded up), per_domain or per_provider (up to count from each) |
sample.stratify | string | no | With random or percent only: provider or domain splits the sample in proportion to each group's share |
on_unavailable | string | no | defer (default: retry a mailbox that cannot send when its turn comes, for up to seven days) or skip |
max_credits | integer | no | The 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 }
}
}| Field | Description |
|---|---|
status | queued 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 |
selection | How the mailboxes were chosen, and how many the scope matched before sampling. The mailboxes themselves are the snapshot under list a batch's senders |
progress | The batch's mailboxes by status. running is a mailbox whose test has not finished yet |
summary | Where the batch's copies landed so far, over the copy the campaign really sends: the tracked half of a tracking comparison |
retry_until | Deferred 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
}
}| Field | Description |
|---|---|
variants | 2 for a tracking comparison, which doubles tests and max_sends |
max_sends | The most copies the batch sends: tests times seeds_per_test. Each mailbox's daily limit can only make it fewer |
free_tests, paid_tests, credits | How tests splits against the month's free tests, and the most the paid ones cost. All 0 on an unmetered panel |
concurrency | How 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:
| Field | Description |
|---|---|
untracked | On a tracking comparison only: the untracked half's counts. summary is the tracked half |
domains | Per sending domain, worst inbox rate first: key (the domain), senders in the batch, tested (senders that finished a test) and counts |
providers | The same per sending provider, key a host family and label its name |
recipients | Counts per recipient provider family, like a test's families |
matrix | Per sending domain, in the order of domains, its counts per recipient provider in the order of recipients |
content | The 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
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | 1 to 100, default 25 |
cursor | query | string | Opaque cursor from a previous pagination.next_cursor |
sort | query | string | worst (default: lowest inbox rate first, untested last), best, email or status |
status | query | string | Only mailboxes in this status: queued, deferred, running, completed, skipped, failed or cancelled |
q | query | string | Only 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": "john@domain2.com",
"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" }
}| Field | Description |
|---|---|
status | queued, deferred (waiting to retry at next_attempt_at), running, completed, skipped, failed or cancelled |
reason, detail | Why a mailbox was deferred, skipped or failed: a code from placement test refusals and its sentence |
test_ids | The 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": "dana.seed@gmail.com",
"family": "gmail",
"label": "Gmail",
"status": "active",
"seed": true
},
{
"email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
"email": "sales@yourdomain.com",
"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
| Field | Type | Required | Description |
|---|---|---|---|
seed | boolean | yes | true 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
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the monitor runs |
interval_days | integer | 7 | Days between runs, 1 to 30 |
panel | string | instance | instance, workspace or cloud |
alert_below | integer | 70 | Inbox 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_alert | boolean | false | Also 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.