Seegnals

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
}
  • status is the stored column. display_status (draft, running, paused, done, archived, other) is what the badge in the app shows: an active campaign at 100% progress with at least one person enrolled is done; archived_at wins over everything.
  • progress: planned is people enrolled multiplied by the number of email steps; done counts steps already passed by active people plus every step of people who stopped or finished. A prospect who replied counts as done. pct is 0 to 100.
  • Campaigns carry created_at, started_at and archived_at; there is no updated_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
}
  • id is the enrollment id, not the prospect id.
  • current_step is the step that goes out next (1 means nothing has been sent yet).
  • outcome.label is the word from the badge in the app (Active, Stopped, Completed); negative is true when the stop came from a bounce, an unsubscribe or a suppression; reason is 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.