campaigns_manage
CRUD + send-control verbs for marketing campaigns (email or SMS, single send for v1). Use action='list' to see what's there; action='create' to author a new draft; action='update' to modify a draft (only allowed when status is draft/scheduled/paused); action='send_now' to dispatch immediately to the audience; action='test_send' to fire a single message to one address for preview. Drip campaigns: pass send_mode='drip' plus steps. Single sends use content. get/list return steps, sending_window, send_mode and scheduled_at so a session can audit what is configured without the UI. content for email = { subject, preheader?, from_name?, from_email?, reply_to?, blocks: [...] }. from_email may be any address on ANY domain verified in the connected Resend account — call resend_manage(action='domains') first to see which are verified. An unverified domain is not rejected here; it fails later, at send time, per recipient. reply_to sets where replies land — use it when replies should go somewhere other than from_email. IMPORTANT for drip campaigns: exit_on:{reply} can only fire for replies Groundbase can SEE, which means an address on a mailbox connected under Settings -> Email. Point reply_to at an unconnected address and the user still gets their replies, but reply-exit stays blind and the drip keeps sending to people who answered. Settable per step as well as per campaign. Each block is { type, props }. Block types: text (props.html: HTML), header (props.html: text, props.level: 1|2|3), image (props.src: url, props.alt, props.width), button (props.text, props.url, props.bgColor?), divider (props.color?), spacer (props.height: px). content for SMS = { body: text, media_urls?: [urls] }. Merge tags inside any string field: {{first_name}}, {{last_name}}, {{full_name}}, {{email}}, {{phone}}, {{company_name}}, {{cf.}}. Unknown tags render empty. steps authors a DRIP campaign (requires send_mode='drip'). Array of { delay_seconds, content_json, exit_on?, channel? } in order. content_json is the canonical key; content is accepted as an alias. Until 2026-08-30 this doc said content while the dispatcher read only content_json, so steps authored from this description silently sent step 0 every time. Step 0 fires when the campaign starts; every later step fires delay_seconds after THAT recipient's actual previous send, so a step held back by a sending window or the daily cap still spaces correctly. Each step's content uses the same block format as the single-send content param. exit_on is { reply?, open?, click?, meeting? } — when true and the contact has done that since the previous step, the rest of the drip is skipped for them. channel overrides the campaign channel for that step alone (mixed email/SMS drips); it also decides whether the step counts against the daily EMAIL cap. sending_window restricts when this campaign may send: { days: [0-6, 0=Sunday], start_hour: 0-23, end_hour: 1-24, timezone?: IANA or 'auto' }. end_hour is exclusive and must exceed start_hour. 'auto' (the default) means the account owner's timezone. A step falling due outside the window WAITS — the recipient stays queued and sends at the next open hour. Nothing fails or is skipped. Omit or pass null for no restriction. audience_filter shape: { all: [rules], any?: [rules] }. Rules are {type, ...}. Supported in v1: { type: "all_contacts" } — every contact with a valid channel address; { type: "tag", op: "has" | "has_not", value: "" } — tag membership filter. Multiple all[] rules AND together; any[] rules OR together. Behavior queries (opened campaign X / clicked X) and custom-field queries arrive in a follow-up.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | Free-text name search for action=list. | |
| id | No | Campaign UUID — required for everything except list+create. | |
| to | No | For action='test_send' — recipient email or phone. | |
| name | No | ||
| limit | No | ||
| steps | No | For create/update when send_mode='drip'. `steps` authors a DRIP campaign (requires send_mode='drip'). Array of { delay_seconds, content_json, exit_on?, channel? } in order. `content_json` is the canonical key; `content` is accepted as an alias. Until 2026-08-30 this doc said `content` while the dispatcher read only `content_json`, so steps authored from this description silently sent step 0 every time. Step 0 fires when the campaign starts; every later step fires delay_seconds after THAT recipient's actual previous send, so a step held back by a sending window or the daily cap still spaces correctly. Each step's `content` uses the same block format as the single-send `content` param. `exit_on` is { reply?, open?, click?, meeting? } — when true and the contact has done that since the previous step, the rest of the drip is skipped for them. `channel` overrides the campaign channel for that step alone (mixed email/SMS drips); it also decides whether the step counts against the daily EMAIL cap. | |
| action | Yes | 'list' returns user's campaigns. 'get' returns one by id. 'create' makes a draft (requires name + channel). 'update' patches a draft. 'delete' soft-deletes. 'duplicate' clones as a draft. 'test_send' fires one message to `to` without queuing recipients. 'send_now' resolves the audience and queues all recipients for dispatch. 'schedule' sets scheduled_at. 'pause' freezes. 'resume' un-pauses. 'stop' ends an ONGOING campaign (queued people dropped, sent ones kept). 'add_recipients' names contacts on a campaign by id: on a draft, scheduled or paused one they join the audience; on an ongoing campaign that is sending they are queued now, at the first message, under the campaign's rules. | |
| status | No | Filter for action=list. | |
| channel | No | ||
| content | No | For create/update, single-send. `content` for email = { subject, preheader?, from_name?, from_email?, reply_to?, blocks: [...] }. `from_email` may be any address on ANY domain verified in the connected Resend account — call resend_manage(action='domains') first to see which are verified. An unverified domain is not rejected here; it fails later, at send time, per recipient. `reply_to` sets where replies land — use it when replies should go somewhere other than from_email. IMPORTANT for drip campaigns: exit_on:{reply} can only fire for replies Groundbase can SEE, which means an address on a mailbox connected under Settings -> Email. Point reply_to at an unconnected address and the user still gets their replies, but reply-exit stays blind and the drip keeps sending to people who answered. Settable per step as well as per campaign. Each block is { type, props }. Block types: text (props.html: HTML), header (props.html: text, props.level: 1|2|3), image (props.src: url, props.alt, props.width), button (props.text, props.url, props.bgColor?), divider (props.color?), spacer (props.height: px). `content` for SMS = { body: text, media_urls?: [urls] }. Merge tags inside any string field: {{first_name}}, {{last_name}}, {{full_name}}, {{email}}, {{phone}}, {{company_name}}, {{cf.<custom_field_key>}}. Unknown tags render empty. | |
| send_mode | No | ||
| contact_ids | No | For add_recipients: contact ids, at most 500. | |
| scheduled_at | No | Unix seconds for schedule/create. | |
| enrolment_mode | No | For create/update. 'once' (default) settles its audience when it starts and finishes when the last message goes. 'ongoing' stays sending until action='stop' and takes new entrants at any time: the audience filter is re-run every five minutes, add_recipients works while it sends, and a workflow add_to_campaign step can enrol someone. Cannot be changed to 'once' while sending — stop it instead. | |
| sending_window | No | For create/update. `sending_window` restricts when this campaign may send: { days: [0-6, 0=Sunday], start_hour: 0-23, end_hour: 1-24, timezone?: IANA or 'auto' }. end_hour is exclusive and must exceed start_hour. 'auto' (the default) means the account owner's timezone. A step falling due outside the window WAITS — the recipient stays queued and sends at the next open hour. Nothing fails or is skipped. Omit or pass null for no restriction. | |
| audience_filter | No | For create/update. `audience_filter` shape: { all: [rules], any?: [rules] }. Rules are {type, ...}. Supported in v1: { type: "all_contacts" } — every contact with a valid channel address; { type: "tag", op: "has" | "has_not", value: "<tag_name>" } — tag membership filter. Multiple all[] rules AND together; any[] rules OR together. Behavior queries (opened campaign X / clicked X) and custom-field queries arrive in a follow-up. |