Appearance
Campaigns
Full reference for the Campaigns resource in the bluefox.email API. See the API overview for authentication, the response envelope, and pagination.
List campaigns
GET /v1/projectId/{projectId}/campaigns
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
limit | query | integer | Max items per page. Capped at 30. | |
skip | query | integer | Number of items to skip, for pagination. | |
sort | query | string | Field to sort by. | |
order | query | string | ||
filter[<field>] | query | string | Exact-match filter on a top-level field, e.g. filter[name]=Welcome. Repeatable for multiple fields. |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 403 | Missing or invalid API key |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
items | array | ||
count | integer | Total matching items across all pages, not just this page. | |
next | string | Full URL (path + query string) of the next page, or null if this is the last page. |
Create a campaign
POST /v1/projectId/{projectId}/campaigns
subscriberListId and segmentId (if given) must belong to this same project - a valid ID from a different project 404s. Status "scheduled" requires a valid, parseable scheduledTo (a 400 otherwise), plus the project having usable sending credentials and enough account credit for the recipient count; missing any of these fails the whole create with a 400/405 rather than creating a draft.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
subject | string | yes | |
previewText | string | Optional, but meaningfully affects open rates - if the user hasn't given you one, ask what they'd like it to say rather than leaving it blank. | |
timeZone | string | yes | IANA time zone, e.g. "America/New_York". Stored with the campaign and shown next to the scheduled time in the app; it does not shift scheduledTo, which must be an absolute instant. |
subscriberListId | string | yes | Required. If the user hasn't told you which list to send to, call GET /v1/projectId/{projectId}/subscriber-lists first and ask them to choose from the real list names - don't ask them for a raw ID. |
segmentId | string | Optional - narrows the subscriberListId down further. If the user mentions targeting a specific segment by name but doesn't give an ID, call GET /v1/projectId/{projectId}/segments first and ask them to choose from the real names. | |
status | string (draft | archive | scheduled) | Set to "scheduled" together with a valid scheduledTo to schedule sending. Status "scheduled" without a parseable scheduledTo is rejected with a 400. | |
scheduledTo | string | Absolute ISO 8601 date-time (with a UTC offset or trailing "Z"). Required when status is "scheduled". | |
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | Turns on the public web version ("view in browser" page) of the campaign. Defaults to false. The page only works while this is true and the status is "sent" or "archive". | |
senderIdentity | string | A sender identity _id. If the user hasn't told you which one to use, call GET /v1/projectId/{projectId}/sender-identities first and ask them to choose from the real email addresses returned - don't ask them for a raw ID. | |
replyTo | string | ||
type | string (html | text) | Omit to keep using the visual (Chamaileon) editor document. Set to "html" or "text" to author document as a raw Handlebars template string instead (see the document field). | |
document | any | The email content. When type is omitted, this is opaque Chamaileon visual-editor JSON - not meant to be hand-authored. When type is "html" or "text", this is a plain string rendered with Handlebars at send time, and can use merge tags: - {{contact.email}}, {{contact.name}}, and {{contact.<yourCustomFieldName>}} for any field from GET /v1/projectId/{projectId}/contacts/fields. Contact data is ONLY available under the contact. prefix - bare tags like {{name}}, {{firstName}} or {{email}} render as empty strings. For a fallback use DEFAULT, e.g. Hi {{DEFAULT contact.name "there"}},. - {{unsubscribeLink}} and {{pauseSubscriptionLink}} - available for campaign/triggered/automation emails (subscriber-list-bound), NOT for transactional emails. - {{webVersionLink}} - campaigns only: a public "view in browser" link to the campaign (the same link for every recipient, also shown on the campaign card in the app). It is not personalized - contact merge tags show their DEFAULT fallback. It only works while the campaign's webVersionEnabled is true and its status is "sent" or "archive" (when adding this tag, also set webVersionEnabled: true), and always shows the latest version of the campaign (edits made after sending are visible to earlier recipients). - Any feed's variableName (see the feeds field) as a loop, e.g. {{#each news.item limit=5 skip=0}}...{{this.title}}...{{/each}}. - Standard {{#if}}/{{#unless}}, plus custom helpers AND, OR, NOT, EQ, INCLUDES, CAPITALIZE, TRUNCATE, DEFAULT, usable like {{#if (EQ contact.plan "pro")}}...{{/if}}. For transactional/triggered sends, whatever object is passed as data on POST /v1/projectId/{projectId}/send-transactional or /v1/projectId/{projectId}/send-triggered is merged directly into the TOP LEVEL of the template context (not nested under a "data" key) - so sending data: { "orderId": 123 } makes {{orderId}} available, not {{data.orderId}}. | |
feeds | array of object | RSS/Atom/JSON feeds to pull into this email - see variableName on each feed for how to reference it from document. | |
feeds[].url | string | yes | |
feeds[].feedType | string (rss-xml | json) | yes | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). |
feeds[].variableName | string | yes | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). |
Responses
| Status | Description |
|---|---|
| 201 | Created |
| 400 | Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit. |
| 403 | Missing or invalid API key |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
_id | string | ||
name | string | ||
status | string (draft | archive | sent | scheduled | being-sent | error) | ||
subject | string | ||
previewText | string | ||
timeZone | string | ||
subscriberListId | string | ||
segmentId | string | ||
scheduledTo | string | ||
senderIdentity | string | ||
replyTo | string | ||
type | string (chamaileon | html | text) | ||
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | ||
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). | |
feeds | array of object | ||
feeds[].url | string | yes | |
feeds[].feedType | string (rss-xml | json) | yes | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). |
feeds[].variableName | string | yes | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
errorMessage | string | ||
createdAt | string | ||
updatedAt | string |
Get a single campaign
GET /v1/projectId/{projectId}/campaigns/{id}
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
id | path | string | yes |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 403 | Missing or invalid API key |
| 404 | Campaigns not found |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
_id | string | ||
name | string | ||
status | string (draft | archive | sent | scheduled | being-sent | error) | ||
subject | string | ||
previewText | string | ||
timeZone | string | ||
subscriberListId | string | ||
segmentId | string | ||
scheduledTo | string | ||
senderIdentity | string | ||
replyTo | string | ||
type | string (chamaileon | html | text) | ||
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | ||
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). | |
feeds | array of object | ||
feeds[].url | string | yes | |
feeds[].feedType | string (rss-xml | json) | yes | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). |
feeds[].variableName | string | yes | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
errorMessage | string | ||
createdAt | string | ||
updatedAt | string |
Partially update a campaign
PATCH /v1/projectId/{projectId}/campaigns/{id}
Merges the given fields into the existing document - omitted fields are left unchanged. Nothing is required (unlike create) since every field is optional on a partial update.
subscriberListId/segmentId are re-validated the same way as create. A campaign cannot be updated (including un-scheduling it) within 6 minutes of its scheduledTo time - it 400s with a message that says "5 minutes" but the actual cutoff is 6.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
id | path | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ||
subject | string | ||
previewText | string | Optional, but meaningfully affects open rates - if the user hasn't given you one, ask what they'd like it to say rather than leaving it blank. | |
timeZone | string | IANA time zone, e.g. "America/New_York". Stored with the campaign and shown next to the scheduled time in the app; it does not shift scheduledTo, which must be an absolute instant. | |
subscriberListId | string | Required. If the user hasn't told you which list to send to, call GET /v1/projectId/{projectId}/subscriber-lists first and ask them to choose from the real list names - don't ask them for a raw ID. | |
segmentId | string | Optional - narrows the subscriberListId down further. If the user mentions targeting a specific segment by name but doesn't give an ID, call GET /v1/projectId/{projectId}/segments first and ask them to choose from the real names. | |
status | string (draft | archive | scheduled) | Set to "scheduled" together with a valid scheduledTo to schedule sending. Status "scheduled" without a parseable scheduledTo is rejected with a 400. | |
scheduledTo | string | Absolute ISO 8601 date-time (with a UTC offset or trailing "Z"). Required when status is "scheduled". | |
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | Turns on the public web version ("view in browser" page) of the campaign. Defaults to false. The page only works while this is true and the status is "sent" or "archive". | |
senderIdentity | string | A sender identity _id. If the user hasn't told you which one to use, call GET /v1/projectId/{projectId}/sender-identities first and ask them to choose from the real email addresses returned - don't ask them for a raw ID. | |
replyTo | string | ||
type | string (html | text) | Omit to keep using the visual (Chamaileon) editor document. Set to "html" or "text" to author document as a raw Handlebars template string instead (see the document field). | |
document | any | The email content. When type is omitted, this is opaque Chamaileon visual-editor JSON - not meant to be hand-authored. When type is "html" or "text", this is a plain string rendered with Handlebars at send time, and can use merge tags: - {{contact.email}}, {{contact.name}}, and {{contact.<yourCustomFieldName>}} for any field from GET /v1/projectId/{projectId}/contacts/fields. Contact data is ONLY available under the contact. prefix - bare tags like {{name}}, {{firstName}} or {{email}} render as empty strings. For a fallback use DEFAULT, e.g. Hi {{DEFAULT contact.name "there"}},. - {{unsubscribeLink}} and {{pauseSubscriptionLink}} - available for campaign/triggered/automation emails (subscriber-list-bound), NOT for transactional emails. - {{webVersionLink}} - campaigns only: a public "view in browser" link to the campaign (the same link for every recipient, also shown on the campaign card in the app). It is not personalized - contact merge tags show their DEFAULT fallback. It only works while the campaign's webVersionEnabled is true and its status is "sent" or "archive" (when adding this tag, also set webVersionEnabled: true), and always shows the latest version of the campaign (edits made after sending are visible to earlier recipients). - Any feed's variableName (see the feeds field) as a loop, e.g. {{#each news.item limit=5 skip=0}}...{{this.title}}...{{/each}}. - Standard {{#if}}/{{#unless}}, plus custom helpers AND, OR, NOT, EQ, INCLUDES, CAPITALIZE, TRUNCATE, DEFAULT, usable like {{#if (EQ contact.plan "pro")}}...{{/if}}. For transactional/triggered sends, whatever object is passed as data on POST /v1/projectId/{projectId}/send-transactional or /v1/projectId/{projectId}/send-triggered is merged directly into the TOP LEVEL of the template context (not nested under a "data" key) - so sending data: { "orderId": 123 } makes {{orderId}} available, not {{data.orderId}}. | |
feeds | array of object | RSS/Atom/JSON feeds to pull into this email - see variableName on each feed for how to reference it from document. | |
feeds[].url | string | ||
feeds[].feedType | string (rss-xml | json) | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). | |
feeds[].variableName | string | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. | |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 400 | Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit. |
| 403 | Missing or invalid API key |
| 404 | Campaigns not found |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
_id | string | ||
name | string | ||
status | string (draft | archive | sent | scheduled | being-sent | error) | ||
subject | string | ||
previewText | string | ||
timeZone | string | ||
subscriberListId | string | ||
segmentId | string | ||
scheduledTo | string | ||
senderIdentity | string | ||
replyTo | string | ||
type | string (chamaileon | html | text) | ||
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | ||
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). | |
feeds | array of object | ||
feeds[].url | string | yes | |
feeds[].feedType | string (rss-xml | json) | yes | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). |
feeds[].variableName | string | yes | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
errorMessage | string | ||
createdAt | string | ||
updatedAt | string |
Delete a campaign
DELETE /v1/projectId/{projectId}/campaigns/{id}
Same 6-minute-before-send lock as PATCH applies to DELETE.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
id | path | string | yes |
Responses
| Status | Description |
|---|---|
| 200 | Deleted |
| 403 | Missing or invalid API key |
| 404 | Campaigns not found |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
_id | string | ||
name | string | ||
status | string (draft | archive | sent | scheduled | being-sent | error) | ||
subject | string | ||
previewText | string | ||
timeZone | string | ||
subscriberListId | string | ||
segmentId | string | ||
scheduledTo | string | ||
senderIdentity | string | ||
replyTo | string | ||
type | string (chamaileon | html | text) | ||
excludeUnengaged | boolean | ||
webVersionEnabled | boolean | ||
trackOpens | boolean | Defaults to true. When false, no open-tracking pixel is added to this email, so opens are not recorded (open stats and open-based segments/conditions get no data from it). | |
trackClicks | boolean | Defaults to true. When false, links in this email are sent as-is instead of through the click tracker, so clicks are not recorded (click stats and click-based segments/conditions get no data from it). | |
feeds | array of object | ||
feeds[].url | string | yes | |
feeds[].feedType | string (rss-xml | json) | yes | "rss-xml" covers both RSS and Atom XML feeds - which one it actually is gets auto-detected from the feed content itself, which changes the array key inside the template (see variableName below). |
feeds[].variableName | string | yes | No spaces allowed. This becomes a top-level Handlebars variable in the document (see the document field on the parent email for the templating syntax). The feed's parsed items are NOT directly at {{variableName}} - they're nested under a feed-shape-dependent array key you loop over with the custom each helper: - feedType "rss-xml" that turns out to be RSS: {{#each <variableName>.item}}...{{/each}} (array key is the literal word "item", singular) - feedType "rss-xml" that turns out to be Atom: {{#each <variableName>.entry}}...{{/each}} (array key is "entry") - feedType "json": the array key is whatever field the source JSON actually uses for its list (e.g. "items", "articles") - inspect the feed's JSON to find it, it is not fixed. Inside the loop, each item's own fields are whatever the feed provides (e.g. RSS gives {{this.title}}, {{this.link}}, {{this.pubDate}}) - there is no fixed schema, it mirrors the feed's actual content one-to-one. |
feeds[].maxItems | integer | Informational only - NOT enforced when the email is sent. To actually limit how many items render, pass limit (and optionally skip) as hash arguments on the each tag itself, e.g. {{#each news.item limit=5 skip=0}}. | |
feeds[].required | boolean | ||
feeds[].availableFields | array of string | Informational only, for the app UI - not enforced or used when rendering. | |
errorMessage | string | ||
createdAt | string | ||
updatedAt | string |
Get stats for a single campaign
GET /v1/projectId/{projectId}/campaigns/{id}/stats
Aggregate counts only, never a per-recipient list - response size and query cost are bounded regardless of how much activity actually happened, since every count is a single indexed query scoped to this one resource. For the actual list of recipients (who received/opened/clicked/bounced/unsubscribed), see GET /v1/projectId/{projectId}/campaigns/{id}/recipients. Pass interval + metric for an hourly/daily/weekly/monthly breakdown instead of totals.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
id | path | string | yes | The campaign _id. |
from | query | string | Only count events at or after this date/time. | |
to | query | string | Only count events at or before this date/time. If both from and to are given, the range between them cannot exceed 366 days. | |
interval | query | string | Returns an EmailStatsTimeline (per-period breakdown, same as the app's stats chart) instead of totals. Requires metric. With interval, from/to are rounded to whole UTC days and default to the last 7 days (hourly) or the last month (others). Max range: hourly 7 days, daily 60 days, weekly/monthly 12 months (405 when exceeded). | |
metric | query | string | With interval: what to count per period. subscribe/unsubscribe/resubscribe/pause-subscription/unpause-subscription count subscription changes caused by this email. | |
timeZone | query | string | With interval: IANA time zone the periods are labelled in. Defaults to UTC. |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 400 | Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit. |
| 403 | Missing or invalid API key |
| 404 | Campaigns not found |
| 405 | The operation is blocked by a business rule (e.g. the resource is still in use) |
List recipients of a single campaign
GET /v1/projectId/{projectId}/campaigns/{id}/recipients
One row per actual recipient - who received it, how many times they opened/clicked, and whether they bounced/complained/unsubscribed/paused/subscribed/resubscribed as a result of this specific send. Supports the usual filter[<field>] exact-match filtering (e.g. filter[unsubscribed]=true, filter[status]=failed), plus two convenience filters not tied to a literal field: filter[opened]=true/false and filter[clicked]=true/false.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
projectId | path | string | yes | The project _id, found in the app under Project Settings. |
id | path | string | yes | The campaign _id. |
limit | query | integer | Max items per page. Capped at 30. | |
skip | query | integer | Number of items to skip, for pagination. | |
sort | query | string | Field to sort by. | |
order | query | string | ||
filter[<field>] | query | string | Exact-match filter on a top-level field, e.g. filter[name]=Welcome. Repeatable for multiple fields. |
Responses
| Status | Description |
|---|---|
| 200 | OK |
| 400 | Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit. |
| 403 | Missing or invalid API key |
| 404 | Campaigns not found |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
items | array | ||
count | integer | Total matching items across all pages, not just this page. | |
next | string | Full URL (path + query string) of the next page, or null if this is the last page. |