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.
by envoydev · github.com/envoydev/sendpulse-integration-wordpress
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/envoydev/sendpulse-integration-wordpress/archive/refs/heads/main.zipReadme
SendPulse Integration
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) - hookselementor_pro/forms/validationandelementor_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) - hookswpcf7_validate(+wpcf7_validate_email/email*) to block a bad submission with an inline field error, andwpcf7_before_send_mailto issue the code once validation passes. Self-gates onWPCF7_VERSION, so it's a no-op if CF7 isn't installed. It pulls the email from a field namedemail/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-codewith{"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 restrictpermission_callbackinclass-spi-rest-api.phpif 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 Formsgform_after_submissionhook, a custom form handler, WP-CLI, anything). Returns the coupon code on success orWP_Erroron failure, same as callingSPI_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
- Upload the
sendpulse-integrationfolder via Plugins > Add New > Upload Plugin (zip the folder first if needed). - 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.
- 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.
- Option-key rename (
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.
- 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. - 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). - Before generating a coupon, the plugin checks
GET /smtp/unsubscribe/searchand rejects the submission if that address already unsubscribed - no coupon gets issued for an address that can't receive it. - 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.
- 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:
- 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: forceon the add call). - SendPulse sends its own confirmation email (the confirmation template you configure).
- User clicks confirm → contact becomes Active (status 1).
- 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, not10.00), real decimals are kept (10.5).
The Cart Recovery WooCommerce email uses single-brace {placeholder} syntax instead - see below.