REST API
Campaigns
Read campaigns with progress, display status, settings, steps and Stats totals; list enrollments; enrol existing prospects; pause and resume a campaign from the outside.
Updated 4 September 2026
Campaigns are created and first started in the app. From the outside you can read them, enrol existing prospects, pause a running campaign and resume a paused one. Creating a new person and enrolling them in one call is POST /prospects?campaign=.
GET /api/v1/campaigns
Query: status (draft, active, paused, completed, the raw column), folder_id (uuid), archived (true or false), plus limit and cursor.
curl "https://app.seegnals.com/api/v1/campaigns?status=active" -H "Authorization: Bearer $SEEGNALS_TOKEN"
const res = await fetch("https://app.seegnals.com/api/v1/campaigns?status=active", {
method: "GET",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`
}
});
const json = await res.json().catch(() => null);
console.log(res.status, json);
import os, requests
res = requests.get(
"https://app.seegnals.com/api/v1/campaigns?status=active",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"},
timeout=20,
)
print(res.status_code, res.json() if res.content else None)
{
"data": [
{
"id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Q3 outbound",
"status": "active",
"display_status": "running",
"pause_reason": null,
"campaign_type": "outbound",
"folder": { "id": "…", "name": "Sales" },
"steps": 4,
"progress": { "planned": 400, "done": 312, "enrolled": 100, "finished": 61, "pct": 78 },
"created_at": "2026-08-10T09:00:00.000+00:00",
"started_at": "2026-08-11T07:30:00.000+00:00",
"archived_at": null
}
],
"next_cursor": null
}
statusis the stored column.display_status(draft,running,paused,done,archived,other) is what the badge in the app shows: anactivecampaign at 100% progress with at least one person enrolled isdone;archived_atwins over everything.progress:plannedis people enrolled multiplied by the number of email steps;donecounts steps already passed by active people plus every step of people who stopped or finished. A prospect who replied counts as done.pctis 0 to 100.- Campaigns carry
created_at,started_atandarchived_at; there is noupdated_at.
Errors: 400 for a bad status, folder_id, archived, limit or cursor; 401; 429.
GET /api/v1/campaigns/{id}
The row above plus settings, steps, mailboxes and the Stats totals.
{
"…": "fields from the list",
"settings": {
"daily_enroll_limit": 20, "timezone": null, "use_prospect_timezone": true,
"window_start_hour": 9, "window_end_hour": 17, "send_on_weekends": false,
"send_days": null, "business_days_only": false
},
"step_list": [{ "id": "…", "position": 1, "delay_days": 0, "subject": "Quick question" }],
"mailboxes": [{ "id": "…", "email": "ada@example.com" }],
"stats": {
"sent": 312, "delivered": 305, "bounced": 7, "replied": 19, "unsubscribed": 2,
"opened": null, "clicked": null, "reply_rate": 0.0609,
"replies": { "total": 19, "interested": 6, "neutral": 9, "notInterested": 4, "autoreplied": 3 },
"prospects": { "enrolled": 100, "queued": 31, "finished": 61, "invalid": null },
"tracking_enabled": false
}
}
The numbers come from the same sources as the Stats tab: event counts per type, delivered = sent - bounced (an estimate, since SMTP has no delivery receipt), reply classification without autoresponders (they are counted separately), queued for active enrollments with a future step, finished for completed ones. opened and clicked are null until the workspace’s tracking domain is verified (tracking_enabled: false): not measured, rather than zero. settings.window_* can be null when the campaign inherits the workspace window.
Errors: 404 { "error": "Campaign not found." } for an unknown id, another workspace’s id or a non-UUID; 401; 429.
GET /api/v1/campaigns/{id}/prospects
One row per person enrolled in the campaign.
Query: status (active, stopped, completed), limit, cursor.
{
"data": [
{
"id": "e0a1…",
"prospect": { "id": "7c9e…", "email": "ada@example.com", "first_name": "Ada", "last_name": null, "company_id": "1b4e…" },
"status": "stopped",
"current_step": 3,
"next_step_at": null,
"outcome": { "label": "Stopped", "negative": true, "reason": "Bounced: 550 5.1.1 user unknown" },
"enrolled_at": "2026-08-11T07:30:00.000+00:00"
}
],
"next_cursor": null
}
idis the enrollment id, not the prospect id.current_stepis the step that goes out next (1 means nothing has been sent yet).outcome.labelis the word from the badge in the app (Active,Stopped,Completed);negativeistruewhen the stop came from a bounce, an unsubscribe or a suppression;reasonis the last recorded error or reason.
Errors: 404 Campaign not found. (checked before any enrollment is read); 400 for a bad status, limit or cursor; 401; 429.
POST /api/v1/campaigns/{id}/prospects
Enrols existing prospects of the workspace into a draft or active campaign, through the same function as Add prospects on the campaign page. The rules apply per person rather than all-or-nothing: already enrolled → already-enrolled; on the workspace or global suppression list → suppressed; active in another campaign → conflict; a snippet used in a step is empty for them → missing-field. Everyone else enters at step 1, scheduled now, on a mailbox from the campaign’s rotation. It does not create prospects.
Body (max 64 KiB), exactly one of:
{ "prospect_ids": ["7c9e6679-7425-40de-944b-e07fc1f90ae7", "1b4e28ba-2fa1-11d2-883f-0016d3cca427"] }
{ "emails": ["ada@example.com", "Bob@Example.com"] }
1 to 200 items; duplicates in the list are merged; emails are compared trimmed and lower-cased.
curl -X POST https://app.seegnals.com/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/prospects \
-H "Authorization: Bearer $SEEGNALS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "emails": ["ada@example.com", "bob@example.com", "nobody@example.com"] }'
const res = await fetch("https://app.seegnals.com/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/prospects", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SEEGNALS_TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ emails: ["ada@example.com", "bob@example.com", "nobody@example.com"] })
});
console.log(res.status, await res.json());
import os, requests
res = requests.post(
"https://app.seegnals.com/api/v1/campaigns/3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d/prospects",
headers={"Authorization": f"Bearer {os.environ['SEEGNALS_TOKEN']}"},
json={"emails": ["ada@example.com", "bob@example.com", "nobody@example.com"]},
timeout=20,
)
print(res.status_code, res.json())
Response 200
{
"campaign": { "id": "3f2b1c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "Q3 outbound", "status": "active" },
"data": [
{ "prospect_id": "7c9e…", "email": "ada@example.com", "result": "enrolled" },
{ "prospect_id": "1b4e…", "email": "bob@example.com", "result": "already-enrolled" },
{ "prospect_id": null, "email": "nobody@example.com", "result": "not-found" }
],
"summary": { "requested": 3, "enrolled": 1, "already-enrolled": 1, "suppressed": 0, "conflict": 0, "missing-field": 0, "not-found": 1 }
}
result is one of enrolled, already-enrolled, suppressed, conflict, missing-field, not-found. not-found means no such prospect in this workspace; prospect_id is then null when you sent emails, and email is null when you sent ids. The status is 200 even when nobody was enrolled: the numbers are in summary.
Errors: 400 for a bad body (Provide exactly one of `prospect_ids` or `emails`., array size and item checks); 404 Campaign not found.; 409 when the campaign cannot take prospects (This campaign can't take new prospects in its current state (paused)., Add step 1 before enrolling anyone., This campaign has no sending mailbox.); 401; 429.
POST /api/v1/campaigns/{id}/pause
No body. The same write as the Pause button: status becomes paused with pause_reason “Paused by the operator.” Enrollments stay where they are; nothing is sent from a paused campaign, and resuming picks every sequence up where it stopped.
Response 200: the campaign in the list shape, now "status": "paused", "display_status": "paused".
Errors: 404 Campaign not found.; 409 Only a running campaign can be paused — this one is a draft. (or already paused, completed); 401; 429.
POST /api/v1/campaigns/{id}/resume
No body. The Resume path for a paused campaign, with the same gates as the button: the campaign needs a sending mailbox, and every enrolled prospect needs every field used in the steps and in the active A/B variants. Status becomes active, started_at is refreshed, pause_reason is cleared.
Only a paused campaign can be resumed. Starting a draft for the first time (with its conflict review) and re-running a completed campaign are decisions with a screen, so the API answers 409 and tells you to do it in the app.
Response 200: the campaign in the list shape, "status": "active".
Errors: 404 Campaign not found.; 409 with the reason (Campaign is already running., Only a paused campaign can be resumed — this one is a draft; start it from the app., This campaign has no sending mailbox., Add at least one step first., This campaign's audience has no prospects., or N prospect(s) are missing a field used in the copy … with "issues": [{ "email": "…", "missing": ["first_name"] }]); 401; 429.