API / Campaigns
Campaigns
Outreach sequences: build them, enrol people, read replies, pause and resume.
List campaigns
GET/api/v1/campaigns
Every outreach campaign, most recently updated first, with member and step counts. Status is draft, active, paused or completed. Returns up to 200.
Example request
curl "https://crowdstack.org/api/v1/campaigns" \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"campaigns": [
{
"id": "cmf3k2x9a0009abcd",
"name": "Q4 outbound",
"status": "active",
"memberCount": 148,
"stepCount": 3,
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
]
}Get a campaign
GET/api/v1/campaigns/:id
One campaign with its sending settings, the mailboxes it sends from, and every step in order. Step bodies are raw templates with merge fields intact.
Step type is email or task. Email steps are sent by the runner; task steps open a task for a person and taskKind says what kind. delayDays is the wait after the previous step, 0 on the first.
An email step with a non-null bBody is running an A/B test: bSubject and bBody are version B, bNote says what B tries. Each person gets one version, split evenly.
Responses
| Status | Meaning |
|---|---|
| 404 | No campaign with that id. |
Example request
curl "https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd" \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"campaign": {
"id": "cmf3k2x9a0009abcd",
"name": "Q4 outbound",
"status": "active",
"timezone": "Europe/London",
"sendWindowStart": 9,
"sendWindowEnd": 17,
"weekdaysOnly": true,
"stopOnReply": true,
"unsubscribeLink": true,
"memberCount": 148,
"mailboxes": [
{
"id": "cmf3k2x9a0020abcd",
"name": "Outbound 1",
"fromEmail": "jane@acme-mail.com",
"active": true
}
],
"steps": [
{
"id": "cmf3k2x9a0030abcd",
"position": 0,
"type": "email",
"taskKind": null,
"delayDays": 0,
"subject": "Quick question, {{firstName}}",
"body": "Hi {{firstName | there}},\n\nI put together a short demo of Acme: {{fields.demo_url}}\n\n{{senderName}}"
},
{
"id": "cmf3k2x9a0031abcd",
"position": 1,
"type": "email",
"taskKind": null,
"delayDays": 3,
"subject": "",
"body": "Hi {{firstName | there}},\n\nJust following up on my last note.\n\n{{senderName}}"
}
],
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
}Create a campaign
POST/api/v1/campaigns
Creates a campaign as a draft, sending from every active mailbox in the workspace, with the steps you give it. Add members, then set status to active to start sending.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | — |
stepsrequired | object[] | The sequence, in order. |
timezone | string | IANA zone for the send window. Defaults to Europe/London. |
sendWindowStart | number | Hour the send window opens. Default 9. |
sendWindowEnd | number | Hour it closes. Default 17. |
weekdaysOnly | boolean | — |
stopOnReply | boolean | — |
unsubscribeLink | boolean | Add an opt-out link and List-Unsubscribe headers. Default false. |
Responses
| Status | Meaning |
|---|---|
| 201 | Body: { campaign }. |
| 422 | A step is invalid, or the timezone is unknown. |
Example request
curl -X POST https://crowdstack.org/api/v1/campaigns \
-H "Authorization: Bearer $CROWDSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q4 outbound",
"steps": [
{
"kind": "email",
"subject": "Quick question, {{firstName}}",
"body": "Hi {{firstName | there}},\n\n...",
"delayDays": 0
},
{
"kind": "email",
"subject": "",
"body": "Just following up.",
"delayDays": 3
}
]
}'Example response
{
"campaign": {
"id": "cmf3k2x9a0009abcd",
"name": "Q4 outbound",
"status": "draft",
"timezone": "Europe/London",
"sendWindowStart": 9,
"sendWindowEnd": 17,
"weekdaysOnly": true,
"stopOnReply": true,
"unsubscribeLink": true,
"memberCount": 0,
"mailboxes": [
{
"id": "cmf3k2x9a0020abcd",
"name": "Outbound 1",
"fromEmail": "jane@acme-mail.com",
"active": true
}
],
"steps": [
{
"id": "cmf3k2x9a0030abcd",
"position": 0,
"type": "email",
"taskKind": null,
"delayDays": 0,
"subject": "Quick question, {{firstName}}",
"body": "Hi {{firstName | there}},\n\nI put together a short demo of Acme: {{fields.demo_url}}\n\n{{senderName}}"
},
{
"id": "cmf3k2x9a0031abcd",
"position": 1,
"type": "email",
"taskKind": null,
"delayDays": 3,
"subject": "",
"body": "Hi {{firstName | there}},\n\nJust following up on my last note.\n\n{{senderName}}"
}
],
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
}Update a campaign
PATCH/api/v1/campaigns/:id
Rename a campaign, change its sending settings, replace its steps, or change its status. Setting status to active starts sending on the runner's next tick and is refused until the campaign has a first email step with a subject and at least one mailbox.
Replacing steps re-numbers them. Members already past the end of a shorter sequence are marked completed.
Request body
| Field | Type | Description |
|---|---|---|
name | string | — |
status | active | paused | completed | — |
steps | object[] | Replaces every step. |
timezone | string | — |
sendWindowStart | number | — |
sendWindowEnd | number | — |
weekdaysOnly | boolean | — |
stopOnReply | boolean | — |
unsubscribeLink | boolean | — |
Responses
| Status | Meaning |
|---|---|
| 200 | Body: { campaign }. |
| 404 | No campaign with that id. |
| 422 | The campaign is not ready to be activated, or a step is invalid. |
Example request
curl -X PATCH https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd \
-H "Authorization: Bearer $CROWDSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "paused"
}'Example response
{
"campaign": {
"id": "cmf3k2x9a0009abcd",
"name": "Q4 outbound",
"status": "paused",
"timezone": "Europe/London",
"sendWindowStart": 9,
"sendWindowEnd": 17,
"weekdaysOnly": true,
"stopOnReply": true,
"unsubscribeLink": true,
"memberCount": 148,
"mailboxes": [
{
"id": "cmf3k2x9a0020abcd",
"name": "Outbound 1",
"fromEmail": "jane@acme-mail.com",
"active": true
}
],
"steps": [
{
"id": "cmf3k2x9a0030abcd",
"position": 0,
"type": "email",
"taskKind": null,
"delayDays": 0,
"subject": "Quick question, {{firstName}}",
"body": "Hi {{firstName | there}},\n\nI put together a short demo of Acme: {{fields.demo_url}}\n\n{{senderName}}"
},
{
"id": "cmf3k2x9a0031abcd",
"position": 1,
"type": "email",
"taskKind": null,
"delayDays": 3,
"subject": "",
"body": "Hi {{firstName | there}},\n\nJust following up on my last note.\n\n{{senderName}}"
}
],
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-15T10:12:00.000Z"
}
}List campaign members
GET/api/v1/campaigns/:id/members
The people enrolled in a campaign with their sequence state: which step is next, when it sends, and whether they replied. Status is active, replied, stopped, completed, bounced, blocked (dropped by a pre-send check) or unsubscribed.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Only members with this status. |
replied | true | false | true for members who have replied. |
limit | number | Page size, 1 to 100. Default 50. |
cursor | string | The nextCursor from the previous page. |
Example request
curl "https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd/members?replied=true" \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"members": [
{
"id": "cmf3k2x9a0010abcd",
"status": "replied",
"nextStep": 1,
"nextSendAt": "2026-09-18T09:00:00.000Z",
"lastSentAt": "2026-09-15T10:12:30.000Z",
"repliedAt": "2026-09-16T11:00:00.000Z",
"contact": {
"id": "cmf3k2x9a0001abcd",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com"
},
"createdAt": "2026-09-15T10:12:30.000Z"
}
],
"nextCursor": null
}Add a contact to a campaign
POST/api/v1/campaigns/:id/members
Enrols one contact, identified by contactId or email. Idempotent: a contact already in the campaign is returned with outcome already_member and nothing changes.
Nothing is sent from this request. The new member is due immediately and the runner sends the first step on its next tick, inside the campaign's send window, from a mailbox with quota left. The campaign must be active for that to happen; campaignStatus in the response tells you.
Contacts on the do-not-email list are never added, and neither are contacts without an email address.
Request body
| Field | Type | Description |
|---|---|---|
contactId | string | Either this or email is required. |
email | string | — |
Responses
| Status | Meaning |
|---|---|
| 201 | outcome: added. The member row is returned. |
| 200 | outcome: already_member, suppressed, or no_email. member is the existing row or null. |
| 404 | No campaign with that id, or no contact matched. |
Example request
curl -X POST https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd/members \
-H "Authorization: Bearer $CROWDSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@acme.com"
}'Example response
{
"outcome": "added",
"contactId": "cmf3k2x9a0001abcd",
"campaignStatus": "active",
"member": {
"id": "cmf3k2x9a0010abcd",
"status": "active",
"nextStep": 0,
"nextSendAt": "2026-09-15T10:12:30.000Z",
"createdAt": "2026-09-15T10:12:30.000Z"
}
}Stop or resume a member
PATCH/api/v1/campaigns/:id/members/:memberId
Set a member's status to stopped to halt their sequence, or active to resume it from where it left off. A member with no steps left is marked completed instead.
Request body
| Field | Type | Description |
|---|---|---|
statusrequired | active | stopped | — |
Responses
| Status | Meaning |
|---|---|
| 200 | Body: { member }. |
| 404 | No such campaign or member. |
Example request
curl -X PATCH https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd/members/cmf3k2x9a0010abcd \
-H "Authorization: Bearer $CROWDSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "stopped"
}'Example response
{
"member": {
"id": "cmf3k2x9a0010abcd",
"status": "stopped",
"nextStep": 1,
"nextSendAt": null,
"lastSentAt": "2026-09-15T10:12:30.000Z",
"repliedAt": null,
"contact": {
"id": "cmf3k2x9a0001abcd",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com"
},
"createdAt": "2026-09-15T10:12:30.000Z"
}
}Read a member's thread
GET/api/v1/campaigns/:id/members/:memberId/messages
Every email sent to and received from one campaign member, oldest first. direction is outbound or inbound. Use it to read replies before deciding what to do next.
Example request
curl "https://crowdstack.org/api/v1/campaigns/cmf3k2x9a0001abcd/members/cmf3k2x9a0010abcd/messages" \
-H "Authorization: Bearer $CROWDSTACK_API_KEY"Example response
{
"messages": [
{
"id": "cmf3k2x9a0130abcd",
"direction": "outbound",
"fromEmail": "jane@acme-mail.com",
"toEmail": "jane@acme.com",
"subject": "Quick question, Jane",
"textBody": "Hi Jane, ...",
"status": "sent",
"occurredAt": "2026-09-15T10:12:30.000Z"
},
{
"id": "cmf3k2x9a0131abcd",
"direction": "inbound",
"fromEmail": "jane@acme.com",
"toEmail": "jane@acme-mail.com",
"subject": "Re: Quick question, Jane",
"textBody": "Sure, send it over.",
"status": "received",
"occurredAt": "2026-09-16T11:00:00.000Z"
}
]
}