Campaign Monitor

Add the people who submit your forms to a Campaign Monitor subscriber list, filling the list custom fields from the answers you mapped.

Campaign Monitor is an add-on for Forms. Once your Campaign Monitor account is connected, every form you edit gains a Campaign Monitor panel in its sidebar where you describe one or more actions. When the form is submitted each action adds the person to the subscriber list you chose, filling that list's custom fields from the answers you mapped onto them.

Nothing else about the form changes: validation, messages, redirects, notification emails and the Submissions list all behave as they did before — as long as Campaign Monitor accepts the subscriber. What happens when it does not is the one thing on this page worth reading before you switch it on.

Where to find it

Two places, because there are two kinds of setting. The account connection is site-wide, at XPAC → Integrations on the Campaign Monitor tab, whose screen is headed Campaign Monitor Settings. What each form sends is per-form, in the Campaign Monitor panel of the document sidebar while you edit that form.

Forms has to be active

The plugin does nothing on its own. Without XPAC Forms it registers no settings tab, no panel and no routes, and shows an admin notice asking for Forms instead.

Connecting your account

Two values are needed, not one, and both come from your Campaign Monitor account.

Create an API key in Campaign Monitor, from your account settings. The plugin reads your subscriber lists, reads each list's custom fields, and adds subscribers, so the key needs access to all three.

Find your Client ID — Campaign Monitor calls each of your accounts a client, and this is the identifier of the one whose lists you want to use. It is the value the plugin uses to ask which lists exist.

Paste them into the Api Key and Client ID fields at XPAC → Integrations → Campaign Monitor and save. That single pair serves every form on the site.

Missing credentials do not hide your actions

The Campaign Monitor panel and its saved actions remain visible when either value is missing or rejected. Account-backed pickers show the connection error, and a submission still creates its Forms entry before the queued delivery is marked failed. Correct the credentials, then retry it from XPAC → Forms → Deliveries.

A wrong Client ID and a wrong key look identical

When the values are wrong the panel shows the bare HTTP reply — Unauthorized, Not Found — and never Campaign Monitor's own explanation of what was wrong. It cannot tell you which of the two boxes to correct, so check both.

There is a worse version of this. Sending a subscriber does not use the Client ID at all: only the list dropdown does. So a Client ID that is wrong while the key is right breaks the editor panel while every action already saved on a form carries on sending perfectly well.

Treat the API key as a secret

The API key uses a password control; the public Client ID remains ordinary text. Both values are still stored in WordPress, so limit settings access and rotate the key if it ever leaks.

Adding someone to a list

Edit the form, open the Campaign Monitor panel and press the round + button (Add action). The Create contact modal opens.

Choose the list under Select audience.

Map the fields. Each row pairs a Local field from your form with a Remote fieldEmail, Name, or one of that list's custom fields — and you add as many rows as you need. One of them has to be Email; read the warning below before you save.

Save the action, then update the form. Updating the form is what stores it.

An action can create or update a subscriber, update only somebody already on the list, or unsubscribe them. A form can hold several actions, and Conditional Logic can decide which apply to a submission.

Every action needs an email mapping

Campaign Monitor identifies a subscriber by their email address, and this plugin refuses to send an action that has not got one. If no mapping row points at Email — or if one does, but the visitor left that field blank — the queued delivery fails permanently with that explanation. The entry and notification flow are not rolled back.

So map your email field to Email first, and make that field required. An optional email field means submissions that omit it cannot complete that action.

Mapping fields

  • Local field lists every field in the form that holds a value, under its label, or under its field name where the label is blank.
  • Remote field lists the chosen list's custom fields, with Email and Name always at the top. Unlike the custom fields, those two are the subscriber's own address and name rather than fields you created.
  • The custom field list belongs to the list you picked, so changing Select audience changes what Remote field offers.
  • A row with either half left empty is ignored, so a half-finished mapping costs nothing.
  • Two rows using the same Local field do not both send — only the last one counts. One answer cannot be sent to two places.

Use multi-answer fields with a matching custom field

A Campaign Monitor MultiSelectMany custom field receives one repeated field row per selected answer. Email and Name still hold one value, so do not use a multi-answer field for either. A File Upload answer is not uploaded into Campaign Monitor and should not be used as a subscriber field.

Both dropdowns are read once per editing session

The list dropdown and each list's custom fields are fetched the first time they are needed and then held for the rest of the session. A list or a custom field you create in Campaign Monitor while the editor is open will not appear until you reload the page.

Built-in Email and Name use dedicated mapping identifiers. A custom field whose key is email or fullname therefore remains a separate selectable target rather than colliding with either built-in field. Older saved built-in mappings are translated at runtime.

The action records tracking consent, not mailing consent

Consent to track opens and clicks can send No, Yes, Campaign Monitor's existing answer, or the answer from a chosen form field. It defaults to No. This is separate from permission to subscribe the address, and the integration does not provide a double-opt-in flow; make the form itself an explicit sign-up when that is required.

Re-add people who unsubscribed before is off by default. With it off, an ineligible address is treated as an expected no-op rather than a failed submission. Turn it on only where this form captures a fresh grant of consent; restarting their automated emails is another explicit choice.

Deliveries, retries and duplicate limits

Forms writes the entry first and schedules Campaign Monitor afterwards. The visitor does not wait, and a remote failure cannot erase the submission. The Deliveries screen retains Campaign Monitor's explanation. Successful actions and expected desired-state no-ops are marked done, so retrying a failed sibling does not repeat them.

Configuration, authentication and validation failures reported as 400/401/403/404/410/422 are left failed for an administrator to fix. Rate limits, 5xx replies and transport failures are retried after about 1, 5, 30 and 120 minutes, for at most five attempts. Restore the account connection or action configuration, then retry a permanent failure manually.

Create/update/unsubscribe operations describe subscriber state and are safe to repeat: already-in-state responses are terminal outcomes where appropriate. The API does not offer a separate request idempotency key, so the per-action completion marker is what prevents successful siblings from being sent again.

Editing and removing an action

Each row in the panel shows the action's name — Create contact — with a pencil (Edit action) that reopens the modal and a red X (Remove action) that asks Are you sure you want to remove the item? before acting.

Use the switch on a row to pause or resume that action without losing its settings. The X removes the selected row after confirmation; other actions keep their order and settings. Mapping-row X buttons remove only their own mapping.

For developers

The credentials are stored in the shared xpac option under the campaign_monitor_api_key and campaign_monitor_client_id keys, read together at packages/CampaignMonitor/Addon.php:174-179. Both are written by text fields on the Integrations settings module (:80-122), and the campaign_monitor page prefix at :117 is what produces those two key names (vendor/xpac/plugin/SettingsManager.php:301-307). The module is registered without show_in_rest (packages/Shared/Settings/Settings.php:213), so the option is not exposed on /wp/v2/settings; it is readable and writable on the settings module's own routes, wp-react-settings/xpac/read and …/update, both gated on manage_options (packages/Shared/Settings/Rest.php:27-37). The key never travels as a query parameter: Client::request() base64-encodes it into an Authorization: Basic header (packages/CampaignMonitor/Client.php:49), and nothing in the package logs the request or the header — neither error_log nor trigger_error appears in any of its three PHP files.

Per-form actions live in the form's form_settings post meta on the xpac-form post type, under a campaignMonitor key, registered on the Forms meta schema (Addon.php:227-268) and therefore readable and writable over the REST API at /wp-json/wp/v2/xpac-form/<id>. The default is an empty items array (:279-286):

{
	"campaignMonitor": {
		"items": [
			{
				"status": true,
				"action": "subscribe",
				"list": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
				"map": [{ "local": "email", "remote": "[email]" }]
			}
		]
	}
}

status and action are both declared in the schema. action is subscribe, update or unsubscribe. Items carry additionalProperties: true (:261), which is what lets Conditional Logic store its own condition object on an item with no schema entry of its own. Write status when you write this meta directly: Forms' shared filter reads $item['status'] with no fallback (Forms/core/base/Utils.php:58) and Conditional Logic's listener declares a non-nullable bool (ConditionalLogic/Forms/Addon.php:285-289).

remote values are Campaign Monitor custom field keys verbatim, plus the two sentinels [email] and [fullname] prepended at Addon.php:396-397, which subscribe() maps onto EmailAddress and Name at :485-494. Everything else becomes a CustomFields entry at :496-499. Because Campaign Monitor's own custom field keys are bracketed, a custom field named email or fullname produces the key [email] or [fullname], collides with a sentinel, and is unreachable — nothing at :394-408 deduplicates the merged list.

Two routes back the editor UI, both requiring manage_options (Addon.php:293-317):

GET /wp-json/xpac/v1/form/campaignMonitor/lists
GET /wp-json/xpac/v1/form/campaignMonitor/<list>/fields

The <list> segment is matched as (?P<list>[a-z0-9]+) (:308), so any list ID outside that character class 404s. Neither route turns a failure into an HTTP error: both catch the exception and answer 200 with a message key instead (:332-336, :376-380), and the editor renders that message where the dropdown would be (assets/packages/campaign-monitor/Components/FormSettings.js:22, useStoreData.js:36-38, :110-112).

packages/CampaignMonitor/Client.php talks to Campaign Monitor API v3.3 and uses a filterable ten-second timeout. Delivery exceptions retain the provider's HTTP and API codes. subscribe() is registered through xpac_forms_submit_callbacks; Forms runs it from Deliveries\Runner only after the entry has been stored. Each accepted action records its stable delivery key, so a later attempt skips it.

Resubscribe, autoresponder restart and ConsentToTrack are stored per action. Tracking consent can be No, Yes, Unchanged, or resolved from a submitted field; it is not a substitute for subscription consent.

The package fires no hooks of its own — there is no do_action and no apply_filters anywhere in its PHP — and registers no blocks. That is a fact about its design rather than a gap in this reference: everything it does is a callback on somebody else's contract. It attaches to five Forms hooks — xpac_forms_init to bootstrap (Addon.php:158, fired at Forms/Bootstrap.php:93, itself on init:200), xpac_forms_post_meta_schema and xpac_forms_post_meta_default_values for the shape of its settings (fired at Forms/core/base/PostType.php:126 and :118, both on init:1000), xpac_forms_post_script_dependencies to load its editor panel (PostType.php:329), and xpac_forms_submit_callbacks for the send — plus rest_api_init for its routes and plugins_loaded/admin_notices in Bootstrap.php. init() guards itself with doing_action('xpac_forms_init') (:170-172).

On the editor side the panel is contributed from JavaScript through the xpac-forms-form-panels filter (assets/packages/campaign-monitor/index.js:25-37, applied at assets/packages/forms/hooks/useFormPanels.js:62). It declares no priority, which is why it sorts below every core Forms panel and why its position relative to other add-ons' panels is not stable (useSortedAndFilteredPanels.js:18-21). Its action modal fires xpac-forms-after-settings-modal-content (Components/Modal.js:101-109), passing the action's current options with an onChange callback and the module id xpac-campaignMonitor; Conditional Logic is the only listener (assets/packages/conditional-logic/forms/admin.js:76-97) and it ignores that third argument. Note that the PHP side passes a differently-spelled module id, xpac-campaign-monitor, to Utils::filterActionItemsByStatus (Addon.php:439); no listener reads it today, so the two never have to agree. The editor script is registered as xpac-campaign-monitor from dist/packages/campaign-monitor/settings.js on the two post-editor admin_print_scripts-* hooks (Addon.php:185-197).

The package's only licensing touchpoint is Updater::register() in Bootstrap.php:54. Nothing in Addon.php checks a licence.

On this page