Reference
Campaigns
Orchestrate sends across an audience, sequence, and inbox set. Lifecycle: draft → start (with optional dry-run) → pause / resume.
List campaigns
/v1/campaigns curl -X GET "https://api.clement.so/v1/campaigns" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Query parameters
-
limitinteger -
starting_afterstring -
ending_beforestring -
statusstring
Response200
OK
| Property | Type | Description |
|---|---|---|
object
required
| enum (1) | "list" |
data
required
| array<Campaign> | |
has_more
required
| boolean | |
next_cursor
required
| string | null | |
prev_cursor
required
| string | null |
Example response
{
"object": "list",
"data": [
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
}
],
"has_more": false,
"next_cursor": "string",
"prev_cursor": "string"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Create a campaign (snapshots the source sequence's steps)
/v1/campaigns Copies the steps of `sequence_id` into per-campaign rows so future edits to the sequence template don't affect this campaign. The new campaign starts in state `draft`; call POST /v1/campaigns/{id}/start to launch.
curl -X POST "https://api.clement.so/v1/campaigns" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"name": "Alice Liddell",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L5",
"sending_inbox_ids": [
"string"
],
"sending_inbox_tags": [
"string"
],
"schedule": {
"timezone": "string",
"daysOfWeek": [
0
],
"startHour": 0,
"endHour": 0
},
"track_opens": false,
"track_clicks": false,
"inject_unsubscribe_footer": false
}' Request body
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name
required
| string | ||||||||||||||||
sequence_id
required
| string | ||||||||||||||||
sending_inbox_ids | array<string> | Explicit sending inboxes. Required unless `sending_inbox_tags` is set. | |||||||||||||||
sending_inbox_tags | array<string> | Send from every active inbox carrying one of these tags (case-insensitive), in addition to `sending_inbox_ids`. | |||||||||||||||
schedule | object | Show properties
| |||||||||||||||
track_opens | boolean | ||||||||||||||||
track_clicks | boolean | ||||||||||||||||
inject_unsubscribe_footer | boolean |
Response201
Created
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Retrieve a campaign (with sequence steps embedded)
/v1/campaigns/{id} curl -X GET "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Response200
OK
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Update a campaign
/v1/campaigns/{id} Mutable while in `draft` or `paused`; mutating a `running` campaign is rejected with 409 — pause first.
curl -X PATCH "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"name": "Alice Liddell",
"sending_inbox_ids": [
"string"
],
"sending_inbox_tags": [
"string"
],
"schedule": {
"timezone": "string",
"daysOfWeek": [
0
],
"startHour": 0,
"endHour": 0
},
"track_opens": false,
"track_clicks": false,
"inject_unsubscribe_footer": false
}' Path parameters
-
idstring required
Request body
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
name | string | ||||||||||||||||
sending_inbox_ids | array<string> | May be empty when the campaign sends by `sending_inbox_tags`. | |||||||||||||||
sending_inbox_tags | array<string> | null | Send from every active inbox carrying one of these tags (case-insensitive), in addition to `sending_inbox_ids`. | |||||||||||||||
schedule | object | Show properties
| |||||||||||||||
track_opens | boolean | ||||||||||||||||
track_clicks | boolean | ||||||||||||||||
inject_unsubscribe_footer | boolean |
Response200
Updated
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Delete a campaign
/v1/campaigns/{id} Cascades to its snapshot sequence_steps and campaign_contacts. Queued (not yet sent) sends of its contacts are canceled; sends already handed to the provider complete. Historical sends keep their email snapshot. Confirmation required when status is `running`.
curl -X DELETE "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Query parameters
-
confirmstring
Response200
Deleted
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
List contacts attached to this campaign with progress
/v1/campaigns/{id}/contacts curl -X GET "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/contacts" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Query parameters
-
limitinteger -
starting_afterstring -
ending_beforestring -
statusstring -
emailstring
Response200
OK
| Property | Type | Description |
|---|---|---|
object
required
| enum (1) | "list" |
data
required
| array<CampaignContactProgress> | |
has_more
required
| boolean | |
next_cursor
required
| string | null | |
prev_cursor
required
| string | null |
Example response
{
"object": "list",
"data": [
{
"id": "string",
"object": "campaign_contact",
"contact_id": "con_01HZ8X3J9K7P2QV6R5N4M3T2L2",
"contact_email": "alice@example.com",
"current_step": 0,
"status": "string",
"next_send_at": "2026-04-29T10:00:00Z",
"assigned_inbox_id": "ass_01HZ8X3J9K7P2QV6R5N4M3T2L1"
}
],
"has_more": false,
"next_cursor": "string",
"prev_cursor": "string"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Attach contacts (or an audience) to a campaign
/v1/campaigns/{id}/contacts Idempotent on `(campaign, contact)`. Pass either `contact_ids` (up to 1,000) or `audience_id` (all contacts in the audience, snapshotted now). Unsubscribed, bounced, suppressed and `invalid`/`risky`-verified contacts are skipped and counted by reason. A contact previously removed from this campaign resumes where it stopped, unless it replied. Attaching new contacts to a `completed` campaign puts it back to `running` (emits `campaign.resumed`).
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/contacts" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"contact_ids": [
"string"
],
"audience_id": "aud_01HZ8X3J9K7P2QV6R5N4M3T2L3",
"skip_if_in_other_campaign": false
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
contact_ids | array<string> | |
audience_id | string | |
skip_if_in_other_campaign | boolean | Skip contacts already enrolled (in any status but `removed`) in another campaign of this workspace. |
Response200
Result
| Property | Type | Description | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object
required
| enum (1) | "attach_result" | |||||||||||||||||||||
attached
required
| integer | Contacts the campaign gained, including `restored` ones. | |||||||||||||||||||||
skipped
required
| integer | Everything not attached: `already_enrolled` plus every skip reason. | |||||||||||||||||||||
restored
required
| integer | Previously removed from this campaign: back to `active` at the step where they stopped. | |||||||||||||||||||||
already_enrolled
required
| integer | ||||||||||||||||||||||
not_found
required
| integer | Requested contacts that don't exist in this workspace and mode. | |||||||||||||||||||||
skipped_reasons
required
| CampaignEnrollSkipReasons | Show properties
|
Example response
{
"object": "attach_result",
"attached": 0,
"skipped": 0,
"restored": 0,
"already_enrolled": 0,
"not_found": 0,
"skipped_reasons": {
"unsubscribed": 0,
"bounced": 0,
"suppressed": 0,
"verification": 0,
"replied": 0,
"in_other_campaign": 0
}
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Move or copy contacts to another campaign
/v1/campaigns/{id}/contacts/move Enrolls the contacts in `to_campaign_id` under that campaign's rules (fresh start at step 0; same skip reasons as attaching), then removes them from this campaign — only those now in the target, so a skipped contact stays where it was. `keep_in_source: true` copies instead.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/contacts/move" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"to_campaign_id": "to__01HZ8X3J9K7P2QV6R5N4M3T2L1",
"contact_ids": [
"string"
],
"status": "active",
"keep_in_source": false,
"skip_if_in_other_campaign": false
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
to_campaign_id
required
| string | |
contact_ids | array<string> | |
status | enum (6) | Without `contact_ids`: take every enrollment in this status. "active""replied""bounced""unsubscribed""finished""failed" |
keep_in_source | boolean | Copy instead of move: the source enrollments stay as they are. |
skip_if_in_other_campaign | boolean | Skip contacts enrolled in a campaign other than the source and the target. |
Response200
Result
| Property | Type | Description | |||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object
required
| enum (1) | "move_result" | |||||||||||||||||||||
attached
required
| integer | Contacts the campaign gained, including `restored` ones. | |||||||||||||||||||||
skipped
required
| integer | Everything not attached: `already_enrolled` plus every skip reason. | |||||||||||||||||||||
restored
required
| integer | Previously removed from this campaign: back to `active` at the step where they stopped. | |||||||||||||||||||||
already_enrolled
required
| integer | ||||||||||||||||||||||
not_found
required
| integer | Requested contacts that don't exist in this workspace and mode. | |||||||||||||||||||||
skipped_reasons
required
| CampaignEnrollSkipReasons | Show properties
| |||||||||||||||||||||
removed_from_source
required
| integer | Active source enrollments set to `removed` because the contact is now in the target. Closed ones (replied, finished, …) stay as history. | |||||||||||||||||||||
canceled_sends
required
| integer |
Example response
{
"object": "move_result",
"attached": 0,
"skipped": 0,
"restored": 0,
"already_enrolled": 0,
"not_found": 0,
"skipped_reasons": {
"unsubscribed": 0,
"bounced": 0,
"suppressed": 0,
"verification": 0,
"replied": 0,
"in_other_campaign": 0
},
"removed_from_source": 0,
"canceled_sends": 0
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Remove contacts from a campaign (bulk)
/v1/campaigns/{id}/contacts/remove Bulk form of `DELETE /v1/campaigns/{id}/contacts/{contact_id}`, up to 1,000 ids.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/contacts/remove" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"contact_ids": [
"string"
]
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
contact_ids
required
| array<string> |
Response200
Result
| Property | Type | Description |
|---|---|---|
object
required
| enum (1) | "remove_result" |
removed
required
| integer | |
not_active
required
| integer | Enrolled but already closed (replied, finished, …) or removed: left alone. |
not_enrolled
required
| integer | |
canceled_sends
required
| integer |
Example response
{
"object": "remove_result",
"removed": 0,
"not_active": 0,
"not_enrolled": 0,
"canceled_sends": 0
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Remove a contact from a campaign
/v1/campaigns/{id}/contacts/{contact_id} Sets an `active` enrollment to `removed` and cancels its queued (`scheduled`) send. A send already being handed to the provider (`sending`) can't be recalled. The enrollment and its sends stay for history; removed leads don't count as replied or finished. Idempotent for an already-removed contact; 409 `enrollment_not_active` when the lead already replied, finished, bounced, unsubscribed or failed.
curl -X DELETE "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/contacts/con_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required -
contact_idstring required
Response200
Removed
| Property | Type | Description |
|---|---|---|
id
required
| string | |
object
required
| enum (1) | "campaign_contact" |
contact_id
required
| string | |
contact_email
required
| string (email) | |
current_step
required
| integer | |
status
required
| string | `active`, `replied`, `bounced`, `unsubscribed`, `finished`, `failed`, or `removed` (taken out of the campaign). |
next_send_at
required
| string (date-time) | null | |
assigned_inbox_id
required
| string | null |
Example response
{
"id": "string",
"object": "campaign_contact",
"contact_id": "con_01HZ8X3J9K7P2QV6R5N4M3T2L2",
"contact_email": "alice@example.com",
"current_step": 0,
"status": "string",
"next_send_at": "2026-04-29T10:00:00Z",
"assigned_inbox_id": "ass_01HZ8X3J9K7P2QV6R5N4M3T2L1"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Duplicate a campaign
/v1/campaigns/{id}/duplicate Creates a `draft` copy with the same settings, schedule, sending inboxes/tags and steps (variants included, picked winners cleared). Leads are not copied; enroll them with POST /v1/campaigns/{id}/contacts. The copy is named `<name> (copy)` unless `name` is given.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/duplicate" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"name": "Alice Liddell"
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
name | string |
Response201
Created
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Pause a running campaign
/v1/campaigns/{id}/pause curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/pause" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Response200
Paused
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Preview a campaign's emails for a lead
/v1/campaigns/{id}/preview Renders every version of every step exactly as the scheduler would for one lead: variables and fallbacks, spintax, the sender's signature, and `Re: <previous subject>` for blank follow-up subjects. `contact_id` must be enrolled in the campaign; without it, variables render empty. The sender is `inbox_id`, else the lead's assigned inbox, else the first active sending inbox.
curl -X GET "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/preview" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Query parameters
-
contact_idstring -
inbox_idstring
Response200
OK
| Property | Type | Description | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object
required
| enum (1) | "campaign_preview" | ||||||||||||||||||||||||||||||||||||
campaign_id
required
| string | |||||||||||||||||||||||||||||||||||||
contact_id
required
| string | null | |||||||||||||||||||||||||||||||||||||
contact_email
required
| string | null | |||||||||||||||||||||||||||||||||||||
inbox_id
required
| string | null | The sender rendered as (signature, sender variables). | ||||||||||||||||||||||||||||||||||||
inbox_email
required
| string | null | |||||||||||||||||||||||||||||||||||||
steps
required
| array<object> | Show properties
|
Example response
{
"object": "campaign_preview",
"campaign_id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L4",
"contact_id": "con_01HZ8X3J9K7P2QV6R5N4M3T2L2",
"contact_email": "string",
"inbox_id": "inb_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"inbox_email": "string",
"steps": [
{
"step_id": "ste_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"step_order": 0,
"versions": [
{
"variant_id": "var_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"label": "string",
"disabled": false,
"picked": false,
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?"
}
]
}
]
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Resume a paused campaign
/v1/campaigns/{id}/resume Same preflight as start (409 `campaign_not_ready`). Resuming after a bounce-protection pause gives the campaign 24 hours and 50 sends before the bounce check looks again, counting only sends since the resume.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/resume" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Response200
Resumed
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Start a campaign (or preview with ?dry_run=true)
/v1/campaigns/{id}/start Transitions `draft` or `paused` → `running`. Refused with 409 `campaign_not_ready` (every problem listed) when the campaign has no steps, the first step has no subject or body, no active sending inbox resolves from `sending_inbox_ids` / `sending_inbox_tags`, or no contact is active. Pass `?dry_run=true` to return the projected schedule (which inboxes, when, to whom — first 10 sends) without committing.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/start" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Query parameters
-
dry_runstring
Response200
Running (or projected schedule under dry_run)
Type: object.
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Campaign aggregate stats
/v1/campaigns/{id}/stats curl -X GET "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/stats" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required
Response200
OK
| Property | Type | Description | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
object
required
| enum (1) | "campaign_stats" | |||||||||||||||||||||||||||
sent
required
| integer | ||||||||||||||||||||||||||||
failed
required
| integer | ||||||||||||||||||||||||||||
opened
required
| integer | ||||||||||||||||||||||||||||
clicked
required
| integer | ||||||||||||||||||||||||||||
replied
required
| integer | ||||||||||||||||||||||||||||
bounced
required
| integer | ||||||||||||||||||||||||||||
unsubscribed
required
| integer | ||||||||||||||||||||||||||||
contacts
required
| object | Show properties
|
Example response
{
"object": "campaign_stats",
"sent": 0,
"failed": 0,
"opened": 0,
"clicked": 0,
"replied": 0,
"bounced": 0,
"unsubscribed": 0,
"contacts": {
"total": 0,
"active": 0,
"replied": 0,
"bounced": 0,
"unsubscribed": 0,
"finished": 0,
"failed": 0,
"removed": 0
}
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Add a step to a campaign
/v1/campaigns/{id}/steps Appends, or inserts at `step_order` (only after the steps leads have already reached). Finished leads don't receive a newly appended step, and a completed campaign stays completed.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/steps" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"step_order": 0,
"delay_days": 0,
"delay_minutes": 0,
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"variants": [
{
"id": "string",
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"disabled": false
}
],
"winning_variant_id": "win_01HZ8X3J9K7P2QV6R5N4M3T2L1"
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
step_order | integer | 0-based position. Omit to append; an existing position inserts before that step. |
delay_days | integer | Wait in whole days. Prefer `delay_minutes`. |
delay_minutes | integer | Wait after the previous step's send, in minutes (sends still respect the campaign's schedule window). Takes precedence over `delay_days`. |
subject | string | May be blank (or omitted) on a follow-up step: the email then goes out in the same thread as `Re: <subject of the previous email>`. The first step needs one. |
body_html | string | |
body_text | string | |
variants | array<SequenceStepVariant> | |
winning_variant_id | string |
Response201
Created
| Property | Type | Description |
|---|---|---|
id
required
| string | |
object
required
| enum (1) | "sequence_step" |
step_order
required
| integer | |
delay_days
required
| integer | |
delay_minutes
required
| integer | Wait after the previous step, in minutes (authoritative over `delay_days`). |
subject
required
| string | Blank on a follow-up = sent as `Re: <previous subject>` in the same thread. |
body_html
required
| string | |
body_text
required
| string | |
variants
required
| array<object> | null | |
winning_variant_id
required
| string | null |
Example response
{
"id": "string",
"object": "sequence_step",
"step_order": 0,
"delay_days": 0,
"delay_minutes": 0,
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"variants": [
{
"id": "string",
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"disabled": false
}
],
"winning_variant_id": "win_01HZ8X3J9K7P2QV6R5N4M3T2L1"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Reorder a campaign's steps
/v1/campaigns/{id}/steps/reorder Pass every step id in the new order. Steps active leads have already reached must keep their position (409 otherwise).
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/steps/reorder" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"step_ids": [
"string"
]
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
step_ids
required
| array<string> | Every step id of the sequence, in the new order. On a campaign, steps active leads have already reached keep their position. |
Response200
Reordered
| Property | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id
required
| string | ||||||||||||||||
object
required
| enum (1) | "campaign" | |||||||||||||||
mode
required
| enum (2) | "live""test" | |||||||||||||||
name
required
| string | ||||||||||||||||
status
required
| enum (4) | "draft""running""paused""completed" | |||||||||||||||
sequence_id
required
| string | null | ||||||||||||||||
schedule
required
| object | Show properties
| |||||||||||||||
sending_inbox_ids
required
| array<string> | ||||||||||||||||
sending_inbox_tags
required
| array<string> | Inbox tags the rotation also sends from: every active inbox carrying one of these tags, in addition to `sending_inbox_ids`. Inboxes tagged later join automatically. | |||||||||||||||
track_opens
required
| boolean | ||||||||||||||||
track_clicks
required
| boolean | ||||||||||||||||
inject_unsubscribe_footer
required
| boolean | ||||||||||||||||
send_status
required
| enum (8) | null | Read-only. The scheduler's last verdict on why this campaign is or isn't sending: `sending`, `outside_schedule`, `no_inbox_available` (every sender at its cap or throttled), `waiting_on_followup_delays`, `quota_exceeded`, `not_entitled`, `no_active_leads`, or `bounce_protection`. Null until the scheduler has looked at it. "sending""outside_schedule""no_inbox_available""waiting_on_followup_delays""quota_exceeded""not_entitled""no_active_leads""bounce_protection" | |||||||||||||||
send_status_at
required
| string (date-time) | null | Read-only. When `send_status` last changed. | |||||||||||||||
paused_reason
required
| string | null | Read-only. `bounce_protection` when the system paused the campaign because its hard-bounce rate passed 5% (at least 100 sends); null for a manual pause. Cleared on resume. | |||||||||||||||
created_at
required
| string (date-time) | ||||||||||||||||
updated_at
required
| string (date-time) | ||||||||||||||||
sequence_steps | array<SequenceStep> |
Example response
{
"id": "cmp_01HZ8X3J9K7P2QV6R5N4M3T2L7",
"object": "campaign",
"mode": "live",
"name": "Q2 outbound — Tier 1 prospects",
"status": "running",
"sequence_id": "seq_01HZ8X3J9K7P2QV6R5N4M3T2L6",
"schedule": {
"timezone": "America/New_York",
"daysOfWeek": [
1,
2,
3,
4,
5
],
"startHour": 9,
"endHour": 17
},
"sending_inbox_ids": [
"inb_01HZ8X3J9K7P2QV6R5N4M3T2L1"
],
"sending_inbox_tags": [
"outbound"
],
"track_opens": false,
"track_clicks": true,
"inject_unsubscribe_footer": true,
"send_status": "sending",
"send_status_at": "2026-05-11T14:22:00Z",
"paused_reason": null,
"created_at": "2026-05-02T16:30:00Z",
"updated_at": "2026-05-02T16:35:00Z"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Update a campaign step
/v1/campaigns/{id}/steps/{step_id} Allowed while the campaign runs. Content, delay and variant edits apply to emails rendered after the change; already queued sends keep their content and leads already waiting keep their computed send time. Moving the step (`step_order`) follows the reorder rules.
curl -X PATCH "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/steps/ste_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"step_order": 0,
"delay_days": 0,
"delay_minutes": 0,
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"variants": [
{
"id": "string",
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"disabled": false
}
],
"winning_variant_id": "win_01HZ8X3J9K7P2QV6R5N4M3T2L1"
}' Path parameters
-
idstring required -
step_idstring required
Request body
| Property | Type | Description |
|---|---|---|
step_order | integer | Move the step to this 0-based position. |
delay_days | integer | Wait in whole days. Prefer `delay_minutes`. |
delay_minutes | integer | Wait after the previous step's send, in minutes (sends still respect the campaign's schedule window). Takes precedence over `delay_days`. |
subject | string | May be blank (or omitted) on a follow-up step: the email then goes out in the same thread as `Re: <subject of the previous email>`. The first step needs one. |
body_html | string | |
body_text | string | |
variants | array<SequenceStepVariant> | null | Replaces the step's variants. Ids are stable: keep an id to edit that variant, drop it to delete the variant (its sends keep their stats). null or [] removes A/B. |
winning_variant_id | string | null |
Response200
Updated
| Property | Type | Description |
|---|---|---|
id
required
| string | |
object
required
| enum (1) | "sequence_step" |
step_order
required
| integer | |
delay_days
required
| integer | |
delay_minutes
required
| integer | Wait after the previous step, in minutes (authoritative over `delay_days`). |
subject
required
| string | Blank on a follow-up = sent as `Re: <previous subject>` in the same thread. |
body_html
required
| string | |
body_text
required
| string | |
variants
required
| array<object> | null | |
winning_variant_id
required
| string | null |
Example response
{
"id": "string",
"object": "sequence_step",
"step_order": 0,
"delay_days": 0,
"delay_minutes": 0,
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"variants": [
{
"id": "string",
"subject": "Quick question about your stack",
"body_html": "<p>Hi {{first_name}}, would love to chat — does Tuesday work?</p>",
"body_text": "Hi {{first_name}}, would love to chat — does Tuesday work?",
"disabled": false
}
],
"winning_variant_id": "win_01HZ8X3J9K7P2QV6R5N4M3T2L1"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Delete a campaign step
/v1/campaigns/{id}/steps/{step_id} Refused with 409 once active leads have reached the step. Sends of the step keep their content and stats.
curl -X DELETE "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/steps/ste_01HZ8X3J9K7P2QV6R5N4M3T2L1" \
-H "Authorization: Bearer $CLEMENT_API_KEY" Path parameters
-
idstring required -
step_idstring required
Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.
Send a test email of one campaign step
/v1/campaigns/{id}/test_send Sends one rendered step (optionally one variant, rendered for `contact_id`) from one of the campaign's sending inboxes to a member of the workspace. Live campaigns only. It's an ad-hoc send: it never moves a lead or shows in campaign stats, but counts against the monthly sends quota. 20 per member per hour, shared with the dashboard.
curl -X POST "https://api.clement.so/v1/campaigns/obj_01HZ8X3J9K7P2QV6R5N4M3T2L1/test_send" \
-H "Authorization: Bearer $CLEMENT_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"step_id": "ste_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"to": "alice@example.com",
"variant_id": "var_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"inbox_id": "inb_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"contact_id": "con_01HZ8X3J9K7P2QV6R5N4M3T2L2"
}' Path parameters
-
idstring required
Request body
| Property | Type | Description |
|---|---|---|
step_id
required
| string | |
variant_id | string | Required when the step has variants (ids from the step's `variants`). |
to
required
| string (email) | A workspace member's email address. |
inbox_id | string | Defaults to the first active sending inbox. |
contact_id | string | Render with this enrolled lead's variables. |
Response201
Queued
| Property | Type | Description |
|---|---|---|
object
required
| enum (1) | "campaign_test_send" |
send_id
required
| string | |
to
required
| string | |
inbox_id
required
| string | |
subject
required
| string |
Example response
{
"object": "campaign_test_send",
"send_id": "snd_01HZ8X3J9K7P2QV6R5N4M3T2L8",
"to": "string",
"inbox_id": "inb_01HZ8X3J9K7P2QV6R5N4M3T2L1",
"subject": "Quick question about your stack"
} Errors
-
400Invalid request -
401Authentication failed -
403Insufficient scope -
404Not found -
409Conflict -
422Validation failed -
429Rate limited
See Errors for the full envelope and per-code reference.