Review and Send Email Campaigns

Download skillThe skill is a reference md and part of wix-manage skill. You can use the following command to add the full wix-manage skill to your project:
Copy

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.

Step 1: List the campaigns

GET https://www.wixapis.com/email-marketing/v1/campaigns

Copy
ParameterValues
visibilityStatusesPUBLISHED (sent, sending, scheduled, in review or stopped; see the Sent? table), DRAFT, TEMPLATE. Leave it out to list campaigns of every visibility.
statusesACTIVE, ARCHIVED, DELETED. Leave it out to get ACTIVE and ARCHIVED.
optionIncludeStatisticstrue adds publishingData.statistics to published campaigns
paging.limit, paging.offsetlimit 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:

FieldWhat it holds
campaignIdThe ID every other call takes
titleThe name in the dashboard; the subject when no title was set
visibilityStatusDRAFT, PUBLISHED, TEMPLATE
distributionStatusWhere delivery stands; see the next table
publishingData.datePublishedWhen the campaign was published; only on published campaigns
publishingData.statisticsResults (Step 3), only with optionIncludeStatistics=true
editorTypeMJML, HTML, WEB or MOBILE: the format of the content (Step 2)
dateCreated, dateUpdatedRecord dates; dateUpdated changes on every edit, so it is not the send date
distributionStatusSent?
DISTRIBUTEDYes, to the whole audience
SENDING, SAMPLINGGoing out now; some contacts have it
TERMINATEDOnly to the sample: sending stopped after the sample failed its checks; email.notSent (Step 3) counts the rest
NOT_STARTEDNo
SCHEDULED, PAUSEDNo: waiting for its send time, or paused before it
IN_DETECTION, IN_MODERATIONNot yet: in review before sending
REJECTEDNo: 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.

Step 2: Read the subject and content

GET https://www.wixapis.com/email-marketing/v1/campaigns/<campaignId>/composer

Copy

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:

  1. Parse 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.
  2. Remove the <mj-head>...</mj-head>, <head>...</head> and <style>...</style> blocks.
  3. Replace every remaining tag (<...>) with a space, so that words in separate blocks do not run together.
  4. Decode entities such as &nbsp; and &amp;, 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.

  • Did a campaign about a code or topic go out: list drafts and published campaigns (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.
  • Word count: for 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.

Step 3: Read the results

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[]:

FieldWhat it holds
campaignIdThe campaign
email.deliveredEmails that reached an inbox
email.openedRecipients who opened it (unique)
email.clickedRecipients who clicked a link (unique)
email.bouncedEmails that did not arrive
email.complainedRecipients who reported it as spam
email.notSentAudience members it did not reach because sending stopped after sampling
landingPage.opened, landingPage.clickedViews 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.

Step 4: List the recipients

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 asksactivity
Who it was sent toSENT (delivered and bounced)
Who received itDELIVERED
Who opened it / did not open itOPENED / NOT_OPENED
Who clickedCLICKED
Whose email bouncedBOUNCED
Who it did not reach after sampling stoppedNOT_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:

Copy

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.

Step 5: Create a draft

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:

FieldWhat to send
composer.composerDataJsonRequired. The content, as a string that holds JSON: {"mjml":"<mjml>...</mjml>"}
campaignEditorType.valueMJML
emailSubjectThe 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
emailPreheaderOptional 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:

Copy

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.

Step 6: Send or schedule a draft

Publishing sends email to real contacts and cannot be undone. Before you send:

  1. Find the draft (Step 1) and read its subject (Step 2).
  2. Check 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.
  3. Settle the audience: contact IDs, or contact labels by their 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.
  4. Ask the user to confirm, naming the campaign title, the subject, the audience, and the time: now, or the date and time in the site's time zone (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:

Copy

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.

Copy

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

Did this help?