Uses the Campaign API of Wix Email Marketing. Every call in Steps 1-4 is read-only. Step 5 creates a draft, and Step 6 sends email to real contacts.
All calls are GET with query parameters unless the step says otherwise. Repeat an array parameter once per value (?visibilityStatuses=DRAFT&visibilityStatuses=PUBLISHED); a comma-joined value returns 400. Every call below is complete and was run as written: send it without looking up its schema first.
For a link to the campaign in the dashboard, use Marketing Dashboard Navigation.
GET https://www.wixapis.com/email-marketing/v1/campaigns
| Parameter | Values |
|---|---|
visibilityStatuses | PUBLISHED (sent, sending, scheduled, in review or stopped; see the Sent? table), DRAFT, TEMPLATE. Leave it out to list campaigns of every visibility. |
statuses | ACTIVE, ARCHIVED, DELETED. Leave it out to get ACTIVE and ARCHIVED. |
optionIncludeStatistics | true adds publishingData.statistics to published campaigns |
paging.limit, paging.offset | limit is 1-100; 101 and up return 400 |
There is no filter on dates, title or content, and no sort parameter. Results are sorted by dateUpdated, newest first, and the response has no total and no hasNext. Read every page: add 100 to paging.offset until a page returns fewer than 100 campaigns.
Each item in campaigns[] holds:
| Field | What it holds |
|---|---|
campaignId | The ID every other call takes |
title | The name in the dashboard; the subject when no title was set |
visibilityStatus | DRAFT, PUBLISHED, TEMPLATE |
distributionStatus | Where delivery stands; see the next table |
publishingData.datePublished | When the campaign was published; only on published campaigns |
publishingData.statistics | Results (Step 3), only with optionIncludeStatistics=true |
editorType | MJML, HTML, WEB or MOBILE: the format of the content (Step 2) |
dateCreated, dateUpdated | Record dates; dateUpdated changes on every edit, so it is not the send date |
distributionStatus | Sent? |
|---|---|
DISTRIBUTED | Yes, to the whole audience |
SENDING, SAMPLING | Going out now; some contacts have it |
TERMINATED | Only to the sample: sending stopped after the sample failed its checks; email.notSent (Step 3) counts the rest |
NOT_STARTED | No |
SCHEDULED, PAUSED | No: waiting for its send time, or paused before it |
IN_DETECTION, IN_MODERATION | Not yet: in review before sending |
REJECTED | No: blocked before sending |
When the status and the results disagree, the results win: a campaign whose email.delivered or email.bounced (Step 3) is above 0 reached contacts. Give its status too.
The last campaign sent: list with visibilityStatuses=PUBLISHED and optionIncludeStatistics=true, keep DISTRIBUTED, SENDING, SAMPLING and TERMINATED, plus any campaign whose emailCampaign.delivered or emailCampaign.bounced is above 0, and take the newest publishingData.datePublished. Sort by that field in code, because the list is sorted by dateUpdated.
One campaign by name: read every page and match title in code. When several match, ask which one, and show each one's status and date.
One campaign by ID: GET https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId> returns it in campaign, with the same fields and the same optionIncludeStatistics parameter. An unknown ID returns 404.
GET https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId>/composer
emailSubject is the subject line. The text of the email is in composer.composerDataJson, nested under composer. The list in Step 1 holds no content, so a question about what a campaign says needs this call for each campaign. To get the text of an MJML or HTML campaign:
composer.composerDataJson: it is a string that holds JSON, not an object. Take the string under its mjml key (editorType MJML) or its html key (editorType HTML). Stripping tags from the unparsed string, or reading mjml before parsing, gives an empty or wrong text.<mj-head>...</mj-head>, <head>...</head> and <style>...</style> blocks.<...>) with a space, so that words in separate blocks do not run together. and &, and collapse runs of whitespace.A word count is the number of whitespace-separated tokens in that text. For example, <mj-text>Please welcome Maya.</mj-text><mj-text>She teaches on Tuesdays.</mj-text> gives "Please welcome Maya. She teaches on Tuesdays.", which is 7 words.
visibilityStatuses=DRAFT&visibilityStatuses=PUBLISHED), read the composer of each, and look for the code in emailSubject and in the content, ignoring case. The code is often only in the content, not in the title. Report each match with its status: a published campaign that went out (table in Step 1) was sent; a DRAFT that holds the code was not sent, so say so and name the draft.MJML and HTML campaigns, count the words of the text from composer.composerDataJson as above, and do not call Preview: its rendered email adds about 40 words of header and footer. Only for WEB and MOBILE campaigns, whose content is in a Wix JSON format, take the text from Preview (below).POST https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId>/preview with the body {} returns html (the rendered email) and emailSubject. It sends nothing. Its text also holds the preheader, a "View in browser" line and the Wix footer ("This email was created with Wix...", the unsubscribe line). Leave those out of a word count.
GET https://www.wixapis.com/email-marketing/v1/campaigns/statistics?campaignIds=<id1>&campaignIds=<id2>
Up to 100 IDs, each as its own campaignIds parameter. The response has one entry per campaign in statistics[]:
| Field | What it holds |
|---|---|
campaignId | The campaign |
email.delivered | Emails that reached an inbox |
email.opened | Recipients who opened it (unique) |
email.clicked | Recipients who clicked a link (unique) |
email.bounced | Emails that did not arrive |
email.complained | Recipients who reported it as spam |
email.notSent | Audience members it did not reach because sending stopped after sampling |
landingPage.opened, landingPage.clicked | Views of and clicks on the campaign's web page |
A count that is zero is left out of the response: read a missing count as 0. A draft returns only campaignId.
Report rates against email.delivered: open rate = opened / delivered, click rate = clicked / delivered. The same numbers come from Step 1 with optionIncludeStatistics=true, under publishingData.statistics.emailCampaign, plus publishingData.statistics.total.mailsSent.
GET https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId>/statistics/recipients?activity=CLICKED&paging.limit=1000
activity is required; without it the call returns 400. Pick the value from the question:
| The user asks | activity |
|---|---|
| Who it was sent to | SENT (delivered and bounced) |
| Who received it | DELIVERED |
| Who opened it / did not open it | OPENED / NOT_OPENED |
| Who clicked | CLICKED |
| Whose email bounced | BOUNCED |
| Who it did not reach after sampling stopped | NOT_SENT |
Each item in recipients[] has contactId, emailAddress, fullName, lastActivityDate and contactDeleted. paging.limit is at most 1000. While pagingMetadata.cursors.next is present, send the same request with paging.cursor=<pagingMetadata.cursors.next>. Count the recipients yourself: pagingMetadata.total is not returned.
For more about the recipients (labels, phone, address), query their contacts by ID, up to 100 IDs per call:
The contacts are in contacts[], with labels in info.labelKeys.items. To label the people who clicked, use Bulk Label and Unlabel Contacts.
Emails used this month: GET https://www.wixapis.com/email-marketing/v1/account-details returns accountDetails.quotaPeriod.quotaUsage.emails (sent in this period), accountDetails.package.monthlyQuotaAllocation.emails (the plan's limit), accountDetails.quotaPeriod.dateFrom and dateTo. To break the emails down by campaign, add up email.delivered and email.bounced (Step 3) of the campaigns published in that period.
Create a draft when the user asks for one; a draft sends nothing. The call below is complete and was run as written: send it without looking up its schema first.
POST https://www.wixapis.com/email-marketing/v1/campaigns takes only these fields:
| Field | What to send |
|---|---|
composer.composerDataJson | Required. The content, as a string that holds JSON: {"mjml":"<mjml>...</mjml>"} |
campaignEditorType.value | MJML |
emailSubject | The subject line. It is also the campaign's name in the dashboard (title); there is no separate title field. When the user names the campaign, use that name as the subject unless they give a subject too |
emailPreheader | Optional preview text shown after the subject |
There are no sender or audience fields on a draft. The sender comes from the site's email marketing settings, and the audience is chosen when the draft is sent (Step 6).
Write the content in MJML: <mjml><mj-body>, then one <mj-section><mj-column> that holds an <mj-text> per paragraph, and an optional <mj-button href="..."> for a link. A heading is an <mj-text> with font-size and font-weight:
composerDataJson is a string, so escape the quotes inside it, and escape them twice inside an attribute value as shown; an object there returns 400 Unexpected value for field composer_data_json. campaignEditorType.value must match the key in that string (MJML with mjml; HTML with html for raw HTML). A mismatch is accepted but makes sending fail later, and the editor type cannot be changed after creation.
The response holds the new campaign in campaign: campaignId, title (the subject), editorType, visibilityStatus DRAFT and distributionStatus NOT_STARTED. That proves the draft was saved; no read-back is needed. Give the user the campaigns link from Marketing Dashboard Navigation to review the draft.
Publishing sends email to real contacts and cannot be undone. Before you send:
accountDetails.status from GET https://www.wixapis.com/email-marketing/v1/account-details. Any value other than ACTIVE blocks sending: send the user to the email marketing dashboard. For a scheduled send, also check that accountDetails.features.scheduling is true.key (such as custom.vip-customers), from GET https://www.wixapis.com/contacts/v4/labels?paging.limit=100 (labels[] with key and displayName). The call has no "all contacts" option. When the user wants everyone and no label holds them, send them to the dashboard to send it there.properties.timeZone from GET https://www.wixapis.com/site-properties/v4/properties). Send only after the user answers yes to that question. A first message that asks to send does not count as the confirmation, because it does not name the audience and the subject they will get.POST https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId>/publish
Send now, to a label:
For contacts, send "contactIds": ["<contactId>", "<contactId>"] in place of labelIds. Send only the audience fields you use.
Schedule: put sendAt inside emailDistributionOptions, in UTC, at least 30 minutes and at most one year ahead. A plan without scheduling returns 428 SCHEDULING_FEATURE_UNAVAILABLE.
A 4xx response means nothing was sent. Do not retry with a different audience or body: tell the user what the error says and give them the campaigns link from Marketing Dashboard Navigation.
Never send a body without emailDistributionOptions, such as {}: that publishes the campaign as a web page only, sends no email, and the campaign can never be emailed after that. A campaign that was already published returns 428 ALREADY_PUBLISHED. The response holds publishingData.datePublished and publishingData.landingPageUrl. Delivery then moves through SAMPLING, SENDING and DISTRIBUTED in distributionStatus.
Full contract: Publish Campaign.
Last updated: 3 November 2026