WP Manifestindependent plugin directory
manifest / ecommerce / sendpulse-integration-wordpress

SendPulse Integration

WooCommerce plugin that issues single-use promo coupons and delivers them via SendPulse, with Elementor Pro and Contact Form 7 adapters, a REST endpoint, double opt-in, and cart-abandonment reminders.

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/envoydev/sendpulse-integration-wordpress/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Framework-agnostic core: validate email -> issue a unique single-use WooCommerce coupon -> deliver it via SendPulse's own SMTP API using a personal SendPulse template (for real unsubscribe support) -> optionally add the contact to a SendPulse address book for list-building. A separate Cart Recovery feature reminds registered customers who left items in their cart, via its own WooCommerce email.

Requires WooCommerce and PHP 7.4+. Elementor Pro and Contact Form 7 are both optional adapters.

Architecture

The actual logic lives in SPI_Promo_Service (includes/class-spi-promo-service.php) and knows nothing about Elementor, Contact Form 7, REST, or any other trigger source - it just takes an email string. Four doors currently sit on top of it, all calling the same two methods (check_email() for a read-only duplicate check, request_code() to actually issue + deliver + sync):

  • Elementor Pro adapter (class-spi-elementor-integration.php) - hooks elementor_pro/forms/validation and elementor_pro/forms/new_record. Only wired up if Elementor Pro is active; otherwise skipped with an informational (not error) admin notice. Everything else in the plugin works without it.
  • Contact Form 7 adapter (class-spi-cf7-integration.php) - hooks wpcf7_validate (+ wpcf7_validate_email/email*) to block a bad submission with an inline field error, and wpcf7_before_send_mail to issue the code once validation passes. Self-gates on WPCF7_VERSION, so it's a no-op if CF7 isn't installed. It pulls the email from a field named email/your-email/your_email, falling back to the first value that validates as an email, so oddly-named fields still work. CF7 forms are matched by title (CF7 has no separate "form name") - see the shared Form name setting below.
  • REST endpoint - POST /wp-json/sendpulse-integration/v1/request-code with {"email": "..."}. Public, unauthenticated by design (meant for anonymous visitors submitting any form). Returns only {success, message} - never the coupon code itself, so the endpoint can't be scripted to farm codes without opening the email. No rate limiting is implemented in the plugin; add your own at the server/CDN level if that's a concern, or restrict permission_callback in class-spi-rest-api.php if you want it locked down.
  • Direct PHP call - sendpulse_integration_request_promo_code($email), a plain global function any other plugin or theme can call (e.g. from a Gravity Forms gform_after_submission hook, a custom form handler, WP-CLI, anything). Returns the coupon code on success or WP_Error on failure, same as calling SPI_Promo_Service::request_code() directly.

Adding support for another form plugin means writing a new thin adapter file that extracts an email from that plugin's own hook and calls SPI_Promo_Service::check_email() / request_code() - no changes needed to the core service or SendPulse client. The Elementor and CF7 adapters are exactly that pattern.

Install

  1. Upload the sendpulse-integration folder via Plugins > Add New > Upload Plugin (zip the folder first if needed).
  2. Activate. Requires WooCommerce. Elementor Pro and Contact Form 7 are optional - the plugin-specific hooks just don't fire if that plugin is absent, everything else still works.
  3. If you're upgrading from an earlier version of this plugin: same folder/file slugs, same option names - your existing API key, address book, and coupon settings are all preserved. Two migrations run automatically on activation/update:
    • Option-key rename (spps_* -> spi_*) - a one-time no-op-afterwards rename so older stored settings keep working.
    • Legacy coupon conversion - codes issued by older versions used WooCommerce's native email restriction, which now fails at apply time (see Coupons); this converts them to the meta-based binding the current version enforces. A Fix previously issued codes button on the Promo tab re-runs it manually.
    • If you were previously using Delivery method = WooCommerce Email for promo codes, that path no longer exists - you'll need to set a personal SendPulse Template, Subject, and Sender email on the Promo tab (the "Load templates"/"Load senders" dropdowns autoload to help with this). The old WooCommerce > Settings > Emails > "SendPulse Integration: Promo Code Delivery" entry and its saved subject/heading/body are now orphaned and unused - safe to ignore or delete, nothing reads them anymore.

Settings page (top-level "SendPulse Integration" menu item)

Three tabs: General, Promo, Cart Recovery.

General

  • SendPulse API Key - generate under SendPulse Settings > API > API keys, optionally restricted to your server's IP.
  • Test connection - verifies whatever's currently typed in the API key field against SendPulse (works before you hit Save).

Promo

  • Address Book - dropdown, autoloads as soon as this tab renders (using the API key from the General tab), no button click needed. A Reload address books button refreshes it manually - useful right after creating a new book in SendPulse. Leave empty to skip SendPulse entirely - coupon + delivery still work, just no duplicate-protection lookup and no list-add.
  • Form name (required) - the internal name of the form the plugin should act on. For Elementor, use the form's "Name" (Content > Additional Options in the editor). For Contact Form 7, use the form's title (the name at the top of the CF7 form list). Whichever matches the submitted form wins. If blank, no form is processed - a form name must be present for anything to send. (This changed from earlier behavior where blank meant "all forms".)
  • Require unique email (checkbox, default on) - when checked, an email that already has a code gets rejected with an inline form error and no new coupon is issued. When unchecked, every submission gets a fresh code regardless of history. Only has an effect if an Address Book is set above, since that's what the check runs against.
  • One active coupon per customer (checkbox, default on) - blocks a new coupon while the customer still holds an unused, unexpired one. Unlike the Address Book duplicate check, this reads the real WooCommerce coupon state, not a "we already emailed them" flag - so once the customer redeems the code or it expires, they automatically become eligible for a fresh one. Applies to both promo requests and cart reminders. Uncheck to allow stacking multiple live coupons per customer.
  • Restrict coupons to their email (checkbox, default on) - enforces "this code belongs to this address" at checkout (see Coupons). Includes the Fix previously issued codes button described in Install.
  • Coupon type / amount / expiry - standard WooCommerce coupon settings applied to every generated code.
  • Delivery mode - dropdown, Immediate (default) or Double opt-in. The fields below it change to match the selected mode (see Delivery and Double opt-in).
  • SendPulse Template - dropdown, autoloads the same way as Address Book. Only your own personal SendPulse templates are ever listed (GET /templates/?owner=me) - SendPulse's library/system templates never appear here and can't be selected.
  • Subject, Sender name, Sender email - Sender email is its own dropdown (Load senders button, manual - not autoloaded), listing only senders SendPulse has already verified for SMTP sending.

Delivery

In Immediate mode, promo code delivery goes through SendPulse's own SMTP API (POST /smtp/emails) using the personal template selected above - there is no WooCommerce Email option for promo codes.

  1. Build the template in SendPulse's own template editor. Add {{promo_code}} wherever the code should appear, {{discount_value}} for the formatted discount, and an unsubscribe link: <a href="{{unsubscribe_url}}">Unsubscribe</a>. That link is what makes unsubscribe actually work - the plugin can't add it for you, it has to be in the template itself.
  2. The sender email must be added and verified as a sender in SendPulse's SMTP service, or sending fails with 422 Sender is not valid. Picking from the Load senders dropdown instead of typing an address prevents this, since it only lists SendPulse-verified senders (is_allowed_for_smtp).
  3. Before generating a coupon, the plugin checks GET /smtp/unsubscribe/search and rejects the submission if that address already unsubscribed - no coupon gets issued for an address that can't receive it.
  4. A send failure is a hard failure - the form shows the actual error, since it's a synchronous API call with a real success/failure response. The coupon still exists in WooCommerce even if the send failed (that side effect already happened), just undelivered.
  5. If Template or Sender email aren't configured, request_code() returns a clear error (spi_no_template / spi_no_sender) rather than attempting a send that's guaranteed to fail.

Address Book sync (if configured) and the duplicate-email check are independent of delivery and work the same regardless.

The Cart Recovery feature (below) always uses its own dedicated WooCommerce email, independent of this delivery mode.

Double opt-in (two-step confirmation)

Promo tab > Delivery mode = Double opt-in. Immediate = the delivery above. Double opt-in = a two-step flow where SendPulse owns both emails:

  1. Form submit → plugin generates the coupon, writes it to the contact as promo_code / discount_value, and adds the contact to the Address Book unconfirmed (confirmation: force on the add call).
  2. SendPulse sends its own confirmation email (the confirmation template you configure).
  3. User clicks confirm → contact becomes Active (status 1).
  4. Your SendPulse Automation 360 flow (trigger: subscriber confirmed) sends the promo email, reading {{promo_code}} / {{discount_value}} from the contact.

The plugin sends no email itself in this mode - it only writes the contact + code and triggers the confirmation. Delivery is entirely SendPulse's.

Plugin fields (shown only when Double opt-in is selected):

  • Confirmation email template ID - from SendPulse: Service Settings > Subscription tools > Confirmation emails. Must pass SendPulse moderation before the ID works.
  • Confirmation sender email - a verified SendPulse sender (Load senders dropdown).
  • Confirmation email language - two-letter code (e.g. uk), passed to SendPulse.

Required SendPulse-side setup (one-time): confirmation email (moderated), promo_code/discount_value variables on the book, the promo email as an Automation 360 template, and the confirmed-trigger flow. Without these the confirmation may send but the code email won't.

Status-aware duplicate check: in this mode the "already got a code" rule reads the contact's confirmation status. A confirmed contact is blocked as a duplicate; an unconfirmed one (submitted before, never clicked) is allowed through so they can be re-sent the confirmation - they never actually received a code.

Config guards: if double opt-in is on but the book, confirmation template, or confirmation sender is missing, request_code() returns a clear error (spi_no_book / spi_no_confirm_template / spi_no_confirm_sender) rather than half-running the flow.

Autoload

Address Book and SendPulse Template dropdowns load automatically as soon as the Promo tab renders. Sender email stays manual-load-only (Load senders button), since picking a sender is a one-time setup action, not something that needs refreshing on every page view.

Placeholders

Two dynamic values, computed from the Promo tab's Coupon type/amount settings, passed as SendPulse template variables ({{promo_code}} / {{discount_value}}, double curly braces - SendPulse's syntax):

  • promo_code - the generated coupon code.
  • discount_value - human-readable: "10%" for a percentage coupon, "2000 UAH" for a fixed-cart coupon (currency is WooCommerce's actual configured currency code, not a symbol - get_woocommerce_currency()). Trailing zeros are stripped (10, not 10.00), real decimals are kept (10.5).

The Cart Recovery WooCommerce email uses single-brace {placeholder} syntax instead - see below.

Cart Recovery (registered customers only)

Settings page > Cart Recovery tab.

Reminds logged-in customers who added items to their WooCommerce cart and didn't check out. Guest checkouts are structurally excluded, not just filtered out by a setting - the tracking hooks only ever act when is_user_logged_in() is true, and the cron scan only ever runs a WP_User_Query against real WordPress accounts. There is no code path where a guest session gets tracked or reminded.

How it works:

  • Cart contents come from WooCommerce's own persistent-cart user meta (_woocommerce_persistent_cart_{blog_id}) - the same data WooCommerce already maintains to restore a logged-in customer's cart. No separate cart storage of our own.
  • Standard cart hooks (woocommerce_add_to_cart, item removed, quantity updated) stamp a _spi_cart_last_activity timestamp on the user and clear any prior "reminder sent" flag - so any fresh activity resets the abandonment clock.
  • woocommerce_cart_emptied and completing checkout (woocommerce_thankyou) clear tracking entirely - no reminder for a cart that's empty or already converted to an order.
  • A WP-Cron job (spi_check_abandoned_carts, custom 15-minute schedule, scheduled on activation / cleared on deactivation) finds accounts whose cart has been inactive past Consider abandoned after (minutes) and haven't already been reminded for that abandonment, batching 50 users per run.
  • If Include discount coupon is checked, a coupon is generated the same way as the Promo tab's coupons (but using the Cart Recovery tab's own type/amount settings) and included via {promo_code}/{discount_value}. The One active coupon per customer rule (Promo tab) applies here too.
  • Delivery is a dedicated WooCommerce email, "SendPulse Integration: Cart Reminder", under WooCommerce > Settings > Emails - same editable-body/placeholder pattern as the promo code email, with its own placeholder set: {customer_name}, {cart_items}, {cart_total}, {checkout_url}, plus {promo_code}/{discount_value} when a coupon is included. {cart_items} renders as a real HTML table in the HTML email and a plain line list in the plain-text version - the underlying {placeholder} text you write is the same for both, only that one substitution differs per format.
  • SendPulse Template delivery isn't wired up for this email yet (only WooCommerce Email) - straightforward to add later as a second delivery-method option if wanted, following the same pattern as the Promo tab.

Localization

Standard WordPress i18n - languages/sendpulse-integration-uk.mo ships a complete Ukrainian translation of every admin-facing string (labels, descriptions, buttons, AJAX messages, JS status text). WordPress loads it automatically based on the site's admin language; no in-plugin language switcher needed.

To add another language: copy languages/sendpulse-integration.pot, translate it with Poedit (or any PO editor) into sendpulse-integration-{locale}.po, save/export as .mo next to it in the same languages/ folder - WordPress picks it up automatically. Locale codes match WordPress's own (e.g. de_DE, pl_PL).

The Ukrainian file was hand-compiled in this build (no msgfmt in the build environment) - if you ever need to regenerate it from the .po, any standard msgfmt sendpulse-integration-uk.po -o sendpulse-integration-uk.mo will do, or re-save via Poedit.

Note: the email subject/heading/body you type into WooCommerce > Settings > Emails is your own content, stored as plain data - it is not part of this translation system and won't change with admin language (that's expected; it's your actual outgoing email copy, e.g. the Ukrainian promo text already configured).

How it decides "already got a code" / can't receive one

check_email() (pre-submit validation) and request_code() (issue step) run the same gate, in order:

  1. Address Book duplicate - only when Require unique email is on and an Address Book is set. Looks the email up via GET /addressbooks/{id}/emails/{email} (in double opt-in mode, via the contact's confirmation status). If it's a duplicate, the submission is rejected with an inline error instead of a new coupon being issued.
  2. Unsubscribed - always checks GET /smtp/unsubscribe/search and rejects unsubscribed addresses the same way.
  3. One active coupon - only when One active coupon per customer is on. Reads real WooCommerce coupon state (find_active_coupon_for_email) and blocks if the customer still holds an unused, unexpired plugin-issued code.

On request_code() (post-validation), checks 1-3 re-run once more to close the race window between two near-simultaneous submissions of the same address, then the coupon is generated and delivered (or handed to the double opt-in flow), and the contact is added to the Address Book if one is set.

Coupons

Each code is PROMO-XXXXXXXX (change the prefix with the spi_coupon_prefix filter), usage limit 1, individual use, expiring per your settings.

Email binding. The issuing email is always recorded as coupon meta (_spi_promo_email). When Restrict coupons to their email is on, that binding is enforced by SPI_Checkout_Validation on the woocommerce_after_checkout_validation hook - if an applied plugin coupon's recorded email doesn't match the submitted billing email, the order is blocked with a message naming a masked version of the correct address (the code may have been forwarded, so the full address is never disclosed).

This deliberately does not use WooCommerce's native set_email_restrictions(). That validates against the billing email held in the customer session, which on the cart page and on customised/block/one-page checkouts is frequently still empty when the coupon is applied - so WooCommerce rejects a perfectly correct code with "Please enter a valid email at checkout to use coupon code X". Validating at order submission instead means the email is known for certain and works on any checkout, and it's guest-compatible (no account required). The trade-off: the discount shows in the totals before the email is verified, and a mismatch is caught on Place order rather than at apply time.

Because of this, codes issued by earlier versions (which used the native restriction) need converting - see the legacy migration under Install. The migration only touches coupons matching the plugin's prefix, so the store's own email-restricted coupons are left alone.

Failure behavior

If SendPulse is unreachable during the duplicate/unsubscribe check, the plugin fails open (doesn't block the customer) and logs the error via error_log. Promo code delivery in Immediate mode is the SendPulse call, so a send failure there is returned as a real error to whichever adapter triggered it (form validation error, REST error response, etc.) rather than silently swallowed.

WC_Emails is lazy-loaded by WooCommerce. The Cart Recovery feature still delivers via a WooCommerce email, so it explicitly calls WC()->mailer() right before firing that delivery action - otherwise the reminder could fire into an empty hook with zero listeners registered yet, silently sending nothing.