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

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
limitqueryintegerMax items per page. Capped at 30.
skipqueryintegerNumber of items to skip, for pagination.
sortquerystringField to sort by.
orderquerystring
filter[<field>]querystringExact-match filter on a top-level field, e.g. filter[name]=Welcome. Repeatable for multiple fields.

Responses

StatusDescription
200OK
403Missing or invalid API key

Response body

FieldTypeRequiredDescription
itemsarray
countintegerTotal matching items across all pages, not just this page.
nextstringFull 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. Setting status "scheduled" with scheduledTo also requires the project to have usable sending credentials and enough account credit for the recipient count; missing either fails the whole create with a 400/405 rather than creating a draft.

Parameters

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.

Request body

FieldTypeRequiredDescription
namestringyes
subjectstringyes
previewTextstringOptional, 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.
timeZonestringyesIANA time zone, e.g. "America/New_York". Used to interpret scheduledTo.
subscriberListIdstringyesRequired. 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.
segmentIdstringOptional - 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.
statusstring (draft | archive | scheduled)Set to "scheduled" together with scheduledTo to schedule sending.
scheduledTostring
excludeUnengagedboolean
senderIdentitystringA 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.
replyTostring
typestring (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).
documentanyThe 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. - {{unsubscribeLink}} and {{pauseSubscriptionLink}} - available for campaign/triggered/automation emails (subscriber-list-bound), NOT for transactional emails. - 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}}.
feedsarray of objectRSS/Atom/JSON feeds to pull into this email - see variableName on each feed for how to reference it from document.
feeds[].urlstringyes
feeds[].feedTypestring (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[].variableNamestringyesNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.

Responses

StatusDescription
201Created
400Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit.
403Missing or invalid API key

Response body

FieldTypeRequiredDescription
_idstring
namestring
statusstring (draft | archive | sent | scheduled | being-sent | error)
subjectstring
previewTextstring
timeZonestring
subscriberListIdstring
segmentIdstring
scheduledTostring
senderIdentitystring
replyTostring
typestring (chamaileon | html | text)
excludeUnengagedboolean
feedsarray of object
feeds[].urlstringyes
feeds[].feedTypestring (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[].variableNamestringyesNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.
errorMessagestring
createdAtstring
updatedAtstring

Get a single campaign

GET /v1/projectId/{projectId}/campaigns/{id}

Parameters

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
idpathstringyes

Responses

StatusDescription
200OK
403Missing or invalid API key
404Campaigns not found

Response body

FieldTypeRequiredDescription
_idstring
namestring
statusstring (draft | archive | sent | scheduled | being-sent | error)
subjectstring
previewTextstring
timeZonestring
subscriberListIdstring
segmentIdstring
scheduledTostring
senderIdentitystring
replyTostring
typestring (chamaileon | html | text)
excludeUnengagedboolean
feedsarray of object
feeds[].urlstringyes
feeds[].feedTypestring (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[].variableNamestringyesNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.
errorMessagestring
createdAtstring
updatedAtstring

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

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
idpathstringyes

Request body

FieldTypeRequiredDescription
namestring
subjectstring
previewTextstringOptional, 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.
timeZonestringIANA time zone, e.g. "America/New_York". Used to interpret scheduledTo.
subscriberListIdstringRequired. 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.
segmentIdstringOptional - 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.
statusstring (draft | archive | scheduled)Set to "scheduled" together with scheduledTo to schedule sending.
scheduledTostring
excludeUnengagedboolean
senderIdentitystringA 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.
replyTostring
typestring (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).
documentanyThe 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. - {{unsubscribeLink}} and {{pauseSubscriptionLink}} - available for campaign/triggered/automation emails (subscriber-list-bound), NOT for transactional emails. - 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}}.
feedsarray of objectRSS/Atom/JSON feeds to pull into this email - see variableName on each feed for how to reference it from document.
feeds[].urlstring
feeds[].feedTypestring (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[].variableNamestringNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.

Responses

StatusDescription
200OK
400Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit.
403Missing or invalid API key
404Campaigns not found

Response body

FieldTypeRequiredDescription
_idstring
namestring
statusstring (draft | archive | sent | scheduled | being-sent | error)
subjectstring
previewTextstring
timeZonestring
subscriberListIdstring
segmentIdstring
scheduledTostring
senderIdentitystring
replyTostring
typestring (chamaileon | html | text)
excludeUnengagedboolean
feedsarray of object
feeds[].urlstringyes
feeds[].feedTypestring (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[].variableNamestringyesNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.
errorMessagestring
createdAtstring
updatedAtstring

Delete a campaign

DELETE /v1/projectId/{projectId}/campaigns/{id}

Same 6-minute-before-send lock as PATCH applies to DELETE.

Parameters

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
idpathstringyes

Responses

StatusDescription
200Deleted
403Missing or invalid API key
404Campaigns not found

Response body

FieldTypeRequiredDescription
_idstring
namestring
statusstring (draft | archive | sent | scheduled | being-sent | error)
subjectstring
previewTextstring
timeZonestring
subscriberListIdstring
segmentIdstring
scheduledTostring
senderIdentitystring
replyTostring
typestring (chamaileon | html | text)
excludeUnengagedboolean
feedsarray of object
feeds[].urlstringyes
feeds[].feedTypestring (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[].variableNamestringyesNo 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[].maxItemsintegerInformational 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[].requiredboolean
feeds[].availableFieldsarray of stringInformational only, for the app UI - not enforced or used when rendering.
errorMessagestring
createdAtstring
updatedAtstring

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.

Parameters

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
idpathstringyesThe campaign _id.
fromquerystringOnly count events at or after this date/time.
toquerystringOnly count events at or before this date/time. If both from and to are given, the range between them cannot exceed 366 days.

Responses

StatusDescription
200OK
400Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit.
403Missing or invalid API key
404Campaigns not found

Response body

FieldTypeRequiredDescription
opensintegerTotal open events (a single recipient opening twice counts twice).
clicksintegerTotal click events.
uniqueOpensintegerNumber of distinct dispatched emails with at least one open.
uniqueClicksintegerNumber of distinct dispatched emails with at least one click.
sentinteger
failedinteger
bounceinteger
complaintinteger

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

NameInTypeRequiredDescription
projectIdpathstringyesThe project _id, found in the app under Project Settings.
idpathstringyesThe campaign _id.
limitqueryintegerMax items per page. Capped at 30.
skipqueryintegerNumber of items to skip, for pagination.
sortquerystringField to sort by.
orderquerystring
filter[&lt;field&gt;]querystringExact-match filter on a top-level field, e.g. filter[name]=Welcome. Repeatable for multiple fields.

Responses

StatusDescription
200OK
400Invalid request body or query - every violation found (missing/invalid/unexpected fields) is reported in one response, semicolon-separated, not just the first one hit.
403Missing or invalid API key
404Campaigns not found

Response body

FieldTypeRequiredDescription
itemsarray
countintegerTotal matching items across all pages, not just this page.
nextstringFull URL (path + query string) of the next page, or null if this is the last page.