Skip to main content

Email Campaigns

SendSeven separates email design from content and from the campaign that sends it. This guide walks you through the full workflow: optionally choosing a template, creating content, and launching a campaign.

Before you send

Campaigns only reach opted-in, active subscribers. See the Email section for connection setup, opt-in / double opt-in, and subscriptions & unsubscribe.

Required Scopes​

ScopePurpose
campaigns:readView campaigns
campaigns:createCreate campaigns
campaigns:updateEdit campaign content/settings and schedule
campaigns:sendSend, pause, resume, and cancel campaigns
email:readList, read, preview and render email templates and content
email:createCreate and duplicate email templates; create, update and duplicate email content
email:updateEdit email templates
email:deleteDelete email templates
analytics:readCampaign analytics

Architecture​

EmailTemplate (optional reusable MJML design)
| its MJML is copied into the content
EmailContent (the actual email: subject + body)
|
EmailCampaign (campaign instance with recipients and schedule)

A campaign always sends an email content. A template is an optional starting point for html content: you copy its MJML into the content, and later edits to the template do not change content that was created from it.

Pricing​

Every plan is a fixed monthly base fee that includes a pool of usage shared across messages, campaign messages, AI credits, and campaign emails. Emails beyond that pool are billed per email:

PlanBase feeIncluded pool / monthPer email beyond the pool
BasicEUR 492,500EUR 0.010
ProfessionalEUR 792,500EUR 0.010
ScaleEUR 19910,000EUR 0.0075
EnterpriseEUR 49940,000EUR 0.005
API OnlyEUR 9 per connected channel1,000 per connected channelEUR 0.0025

Managed (SendSeven handles delivery) and BYOK (you connect your own SendGrid or Mailgun account) cost the same per email — BYOK is about owning your sending reputation and provider relationship, not a lower rate.

The included pool is applied to your most expensive usage first, so emails — the cheapest unit — are the last to draw from it. See the pricing page for current rates.


Step 1: Email Templates (optional)​

An email template is a reusable design written in MJML (a responsive email markup language). Templates are only a starting point for html content. You don't need one for simple or plain_text content, or for html content you design yourself; skip to Step 2 in that case.

A template has no fixed slots. Its MJML contains placeholders in double curly braces, for example {{hero_title}} or {{cta_url}}. Names without a dot are the template's own variables, which each content fills through variable_values. Names with a dot (contact.first_name, company.name, system.year, ...) are filled automatically at send time. GET /api/v1/email-contents/variables lists them.

List Templates​

curl -X GET "https://api.sendseven.com/api/v1/email-templates" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"

The response is a plain array (no items wrapper and no pagination object). It contains your own templates and the read-only system templates (is_system_template: true), but not the MJML source:

[
{
"id": "8c1f2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
"tenant_id": "your-workspace-id",
"name": "Standard Newsletter",
"description": "Single-column newsletter with header image, body, and footer",
"category": "promotional",
"thumbnail_url": null,
"is_system_template": false,
"is_active": true,
"is_hidden": false,
"version": 1,
"created_at": "2026-01-05T10:00:00Z",
"updated_at": "2026-01-05T10:00:00Z"
}
]
Query parameterDescription
categoryOnly templates of this category
is_system_templatetrue for system templates only, false for your own only
is_activeFilter by active status
include_hiddenInclude hidden templates (default false)
limit / offsetPage size (1 to 100, default 50) and offset

GET /api/v1/email-templates/{template_id} returns one template with its mjml_source and the compiled html_compiled.

Create a Template​

curl -X POST "https://api.sendseven.com/api/v1/email-templates" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Simple Announcement",
"description": "Clean single-column announcement layout",
"category": "promotional",
"mjml_source": "<mjml><mj-body><mj-section><mj-column><mj-image src=\"{{header_image}}\" /><mj-text>{{headline}}</mj-text><mj-text>{{body}}</mj-text><mj-button href=\"{{cta_url}}\">{{cta_text}}</mj-button></mj-column></mj-section></mj-body></mjml>"
}'
FieldTypeRequiredDescription
namestringYesTemplate name (max 255 characters)
mjml_sourcestringYesMJML with {{variable}} placeholders
descriptionstringNoDescription
categorystringNoFree-text category, for example promotional or transactional
color_paletteobjectNoBrand colors, for example {"primary": "#0066CC"}
font_familystringNoFont stack, for example Arial, sans-serif
thumbnail_urlstringNoPreview image URL

The MJML is validated and compiled when you save. The response (201) is the full template, including mjml_source and html_compiled. Invalid MJML returns 400 with the compiler message in detail.

Other template endpoints:

EndpointScopeDescription
PATCH /api/v1/email-templates/{template_id}email:updateChange any of the create fields
DELETE /api/v1/email-templates/{template_id}email:deleteDelete a template (204). Returns 409 while email content still references it
POST /api/v1/email-templates/{template_id}/duplicateemail:createCopy a template, including a system template, into your workspace. Body: {"new_name": "..."}
POST /api/v1/email-templates/previewemail:readCompile MJML without saving. Body: mjml_source, optional variable_values. Returns html

System templates are read-only: changing or deleting one returns 403. Duplicate it to get an editable copy.

Use a template in content​

Content does not inherit anything from a template at send time. To build html content from a template, read the template, copy its mjml_source into the content with editor_type: "mjml", and fill its own variables in variable_values:

curl -X POST "https://api.sendseven.com/api/v1/email-contents" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "March Newsletter",
"template_id": "8c1f2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
"editor_type": "mjml",
"subject_line": "March highlights from Acme",
"mjml_source": "<mjml>... copied from the template ...</mjml>",
"variable_values": {
"headline": "March Highlights",
"cta_text": "See what is new",
"cta_url": "https://acme.com/blog/march-2026"
}
}'

template_id is optional and only records which template the content came from. It must be a template of your own workspace (an unknown ID or a system template ID returns 404; duplicate a system template first). It is ignored for simple and plain_text content.


Step 2: Email Content​

Email content is the actual email: subject line plus body. There are three content types:

content_typeBody fieldUse it for
html (default)compiled_html (designer export, editor_type: "stripo") or mjml_source (editor_type: "mjml")Newsletters and designed marketing emails with images and layout
simplecompiled_html holding a rich-text fragmentPersonal-looking emails: formatted text and links only, best deliverability
plain_texttext_bodyText-only emails

Create Content​

curl -X POST "https://api.sendseven.com/api/v1/email-contents" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Quick follow-up",
"content_type": "simple",
"subject_line": "Quick question, {{contact.first_name}}",
"compiled_html": "<p>Hi {{contact.first_name}},</p><p>Do you have <strong>10 minutes</strong> this week? <a href=\"https://acme.com/book\">Pick a slot</a>.</p><p>Best,<br>Anna</p>"
}'
FieldTypeRequiredDescription
namestringYesInternal name
content_typestringNohtml (default), simple or plain_text. Cannot be changed later.
subject_linestringNoSubject line, supports variables
preview_textstringNoInbox preview text (max 150 characters)
from_name / from_emailstringNoDefault sender for campaigns using this content
editor_typestringNohtml only: stripo (default) or mjml
compiled_htmlstringhtml (stripo) and simpleDesigner HTML export, or the simple fragment
mjml_sourcestringhtml (mjml)MJML source
text_bodystringplain_textText body
variable_valuesobjectNoValues for the content's own template variables
footer_modestringNoinherit (default), both, unsubscribe_only or none
auto_attach_signaturebooleanNosimple and plain_text only. Default false. See Signature in inbox replies

The response is the full content object (201).

Signature in inbox replies​

Email content can also be loaded into a reply in the SendSeven inbox (web and mobile app). Set auto_attach_signature: true on simple or plain_text content to have the inbox composer append the sending agent's email signature when the content is inserted.

  • Omitted on create: false. Omitted on PATCH: unchanged.
  • html content always returns false; a value you send for html is ignored.
  • The flag has no effect on campaign sends, and POST /api/v1/email-contents/{content_id}/render does not add the signature: the inbox composer adds it.
  • GET /api/v1/email-contents and GET /api/v1/email-contents/{content_id} both return the field, and duplicating content copies it.

Simple content​

simple is the right choice when the email should look like a message typed by a person. The body is a fragment with these tags only: p, br, strong/b, em/i, a (with href: http, https, mailto or a variable), ul, ol, li. On every create and update the server:

  • sanitises the fragment: other tags are removed (their text is kept), as are style, class and event attributes, images and javascript: links
  • creates the text version itself and returns it in text_body, with links written as text (URL). Any text_body, mjml_source or editor_type you send for simple is ignored
  • stores editor_type: "tiptap"

The email goes out as a minimal HTML document (no layout, no web fonts) together with its text version.

StatusdetailCause
422{"code": "simple_body_empty", "message": ...}compiled_html is missing, or nothing is left after sanitising
422{"code": "simple_body_too_large", "message": ...}compiled_html is larger than 200 KB, or contains more than 10,000 markup characters (<, >, & together)

To change a simple body, PATCH /api/v1/email-contents/{content_id} with a new compiled_html. The same rules and error codes apply.

GET /api/v1/email-contents?content_type=simple lists only one type (html, plain_text or simple). POST /api/v1/email-contents/{content_id}/preview returns the complete email for simple in html_content and the text version in text_content. POST /api/v1/email-contents/{content_id}/render returns the personalised fragment without a document wrapper, ready to insert into a reply.

tip

Always use email_content_id when creating campaigns to reference your email content.

Searching email content​

GET /api/v1/email-contents accepts a search query parameter that matches on content name and subject. When search is provided, pagination is skipped and the endpoint returns up to 50 matches across all pages in a single response — this keeps the Send-Message node's email-content picker snappy without forcing the caller to paginate.

curl -X GET "https://api.sendseven.com/api/v1/email-contents?search=march" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"

For normal browse-style listing (no search parameter), the endpoint paginates as usual using page / page_size.

Personalization variables​

Subject lines and bodies accept {{namespace.field}} placeholders. GET /api/v1/email-contents/variables (scope email:read) returns the catalogue for your workspace, grouped in this order:

CategoryVariables
contactcontact.name, contact.first_name, contact.last_name, contact.email, contact.phone, contact.birthday
custom_fieldscontact.custom.<key> for each active custom field (omitted when you have none, or when your API key lacks custom_fields:read)
companycompany.name, company.legal_name, company.address, company.phone, company.email, company.website, company.logo_url, company.brand_color_primary, company.brand_color_secondary, company.brand_color_accent
agentagent.name, agent.first_name, agent.last_name, agent.email, agent.phone, agent.job_title, agent.role
campaigncampaign.unsubscribe_url, campaign.preferences_url, campaign.name
systemsystem.year, system.date

Each variable carries name, label, description, example and contexts (where it resolves: conversation, campaign, flow, scheduled). Any placeholder without a value at send time renders as an empty string. In HTML bodies, contact, company and agent values are HTML-escaped.

Who is the agent: for a conversation reply or new email, the user making the request; for a campaign or scheduled send, the team member who created it; for a flow Send Email node, the agent assigned to the conversation. If that person is no longer an active member of the workspace, or anywhere else, agent.* is empty.

To render a piece of content for a real recipient, call POST /api/v1/email-contents/{content_id}/render (scope email:read):

curl -X POST "https://api.sendseven.com/api/v1/email-contents/{content_id}/render" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{"conversation_id": "conv_uuid", "variables": {"hero_title": "Your order shipped"}}'

Pass contact_id or conversation_id (the conversation's contact is used); both are optional. variables fills the content's own template variables. The response has the same shape as /preview (html_content, text_content, subject_line, variables_used, content_type) plus unresolved_variables, the placeholders that rendered empty. An unknown contact or a conversation you cannot access returns 404. Contact values are only filled in when the caller also has contacts:read, or has conversations:read and the contact is that conversation's own contact; otherwise contact fields stay empty.


Step 3: Create and Send the Campaign​

Create an Email Campaign​

curl -X POST "https://api.sendseven.com/api/v1/email-campaigns" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "March Newsletter",
"subject_line": "What is new in March",
"mailbox_id": "mailbox_newsletter",
"email_content_id": "content_mar_newsletter",
"list_ids": ["list_newsletter"],
"scheduled_at": "2026-03-01T08:00:00Z"
}'
FieldTypeRequiredDescription
namestringYesCampaign name
subject_linestringYesEmail subject
mailbox_idstringYes*Mailbox to send from. Fills in from_email, from_name and the sending provider. The mailbox must be linked to a sending provider (SendSeven Managed, SendGrid or Mailgun), otherwise 400. *Or send email_provider_config_id together with from_email and from_name.
email_content_idstringYesEmail content to send
list_idsarrayYes*Contact list IDs. *Not needed when you send audience_conditions (then it is ignored).
audience_conditionsobjectNoAudience Builder filter on the subscribers of one or more lists (see below)
scheduled_atstringNoISO 8601 datetime for scheduled delivery
tracking_enabledbooleanNoTrack opens. Omitted: your workspace default, or false when the content is simple
track_clicksbooleanNoTrack link clicks. Omitted: your workspace default, or false when the content is simple

Campaigns send only through a sending provider. email_integration_id is not accepted: a non-null value on create, update or recipient-count returns 422.

Audience Builder​

audience_conditions narrows the subscribers of one or more lists with extra conditions. It uses the same format and rules as for messaging campaigns: a top-level AND that contains at least one list membership (has_optin, "value": true, the list_id of a static or newsletter list), plus any conditions that narrow the audience down or exclude contacts. Only contacts with an active email subscription on the membership lists receive the email. When it is set, the campaign's lists come from the membership lists and list_ids is ignored. On PATCH, omit it to leave it unchanged or send null to remove it.

POST /api/v1/email-campaigns/recipient-count accepts audience_conditions too, for a live count before you save. A request with audience_conditions needs contacts:read in addition to campaigns:read (403 otherwise). Invalid trees return 422 with audience_list_membership_required, audience_membership_list_invalid or audience_conditions_invalid.

Send Immediately​

curl -X POST "https://api.sendseven.com/api/v1/email-campaigns/ecamp_march_01/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"

Get Analytics​

curl -X GET "https://api.sendseven.com/api/v1/email-campaigns/ecamp_march_01/analytics" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"
{
"campaign_id": "ecamp_march_01",
"sent": 12500,
"delivered": 12210,
"opened": 4884,
"clicked": 1587,
"bounced": 290,
"complained": 5,
"unsubscribed": 23,
"delivery_rate": 97.68,
"open_rate": 40.00,
"click_rate": 13.00,
"bounce_rate": 2.32,
"complaint_rate": 0.04
}

Complete Workflow (Python)​

import requests

BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}

# Step 1 (templates) is skipped: simple content needs no template.

# Step 2: Create email content
content = requests.post(
f"{BASE_URL}/email-contents",
headers=HEADERS,
json={
"name": "April Promo Content",
"content_type": "simple",
"subject_line": "Spring savings - 25% off this week only!",
"from_name": "Acme Store",
"from_email": "[email protected]",
"compiled_html": (
"<p>Hi {{contact.first_name}},</p>"
"<p>Save <strong>25%</strong> on all orders this week with code SPRING25.</p>"
'<p><a href="https://shop.acme.com/spring-sale">Shop the sale</a></p>'
),
},
).json()
print(f"Created content: {content['id']}")

# Step 3: Create and schedule the campaign
campaign = requests.post(
f"{BASE_URL}/email-campaigns",
headers=HEADERS,
json={
"name": "Spring Sale Email",
"email_content_id": content["id"],
"list_ids": ["list_newsletter", "list_active_customers"],
"scheduled_at": "2026-04-01T08:00:00Z",
},
).json()
print(f"Campaign scheduled: {campaign['id']} for {campaign['scheduled_at']}")
print(f"Recipients: {campaign['recipient_count']}")

JavaScript Example​

const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};

// Create email content
const contentRes = await fetch(`${BASE_URL}/email-contents`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
name: "Product Update Email",
content_type: "simple",
subject_line: "New feature: Automated Workflows",
from_name: "Acme Product Team",
from_email: "[email protected]",
compiled_html:
"<p>Set up triggers, conditions and actions to automate your messaging.</p>" +
'<p><a href="https://acme.com/blog/automated-workflows">Learn more</a></p>',
}),
});
const content = await contentRes.json();

// Create and send the campaign
const campaignRes = await fetch(`${BASE_URL}/email-campaigns`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
name: "Workflows Announcement",
email_content_id: content.id,
list_ids: ["list_newsletter"],
}),
});
const campaign = await campaignRes.json();

// Send immediately
await fetch(`${BASE_URL}/email-campaigns/${campaign.id}/send`, {
method: "POST",
headers: HEADERS,
});
console.log("Campaign is sending!");

Error Responses​

Most errors return detail as a message string. The cases with a machine-readable code are listed with it. See Error Handling for the response shapes.

StatusdetailDescription
400"Cannot modify campaign with status: ..."The campaign is no longer a draft or scheduled
400"Cannot delete campaign that has been sent"Sent campaigns can't be deleted
401"Invalid or expired API token"Token is invalid or expired
402{"error": "insufficient_balance", "message": ..., "required_balance": ..., "current_balance": ..., "top_up_url": ...}The account balance doesn't cover the send
403"Missing required permission(s): ..."Token lacks the required campaigns:* scope
404message stringUnknown template, content or campaign ID
409message stringThe campaign is already being sent (double send, or a scheduled send already started)
422{"code": "audience_list_membership_required", "message": ...}audience_conditions has no list membership at the top level
422{"code": "audience_membership_list_invalid", "message": ...}A membership list is not a static or newsletter list in your workspace
422{"code": "simple_body_empty", "message": ...}A simple content body is empty after sanitising
422{"code": "simple_body_too_large", "message": ...}A simple content body is larger than 200 KB or has more than 10,000 markup characters
422list of field errorsInvalid request body

Next Steps​