spree_plunk is a Spree Commerce extension that connects a Spree store to Plunk for server-side contact sync, consent-safe event tracking, and selected transactional email handoff.
It is built for marketing use cases such as workflows, campaigns, and audience segmentation. Transactional emails still stay with Spree/Rails by default unless an operator explicitly enables the Plunk transactional email master switch and one of the supported email-type switches.
- syncs Spree users and newsletter subscribers into Plunk contacts
- refreshes Plunk contact data when user or address records change
- tracks selected Spree commerce events into Plunk for workflows and segmentation
- keeps marketing consent explicit instead of inferring it from behavioral events
- supports both hosted Plunk and self-hosted Plunk through a configurable base API URL
- can optionally accept contact subscription-state callbacks from Plunk and write them back into Spree
- can optionally send the first supported transactional email types through Plunk's HTTP send API
The extension currently syncs contact state from these Spree-side sources:
- user created
- user updated
- address created
- address updated
- newsletter subscriber created
- newsletter subscriber verified
- newsletter subscriber deleted
The contact payload can include:
emailsubscribedexternal_user_idstore_codefirst_namelast_namephoneaccepts_email_marketingcityregioncountryziporder_countlast_order_numberlast_order_total
The extension currently sends these namespaced events to Plunk:
spree.newsletter.subscribedspree.newsletter.unsubscribedspree.checkout.email_enteredspree.cart.addedspree.cart.removedspree.coupon.enteredspree.coupon.removedspree.coupon.appliedspree.coupon.deniedspree.checkout.step_viewedspree.checkout.step_completedspree.order.completedspree.order.canceledspree.shipment.shippedspree.reimbursement.paid
These events are intended for:
- workflow triggers
- segmentation
- lifecycle automation
- post-purchase marketing flows
Current storefront event-source boundary:
- cart events are line-item backed so the payload stays item-specific
- checkout events are checkout-service backed so step/email tracking follows real server-side state changes
- coupon events are coupon-handler backed so apply/remove outcomes do not depend on storefront analytics
- the storefront analytics handler is currently unused for the supported event set, which avoids duplicate tracking if Spree later emits overlapping analytics events
- browse-only analytics such as product view, product list view, and product search remain deferred
Transactional email delivery through Plunk is default-off and uses Plunk's HTTP POST /v1/send API. It does not use SMTP, so local development can send through hosted Plunk as long as the app can reach Plunk and the sender domain is verified in the Plunk project.
The current direct-send handoff supports:
- Store API password reset emails from
customer.password_reset_requested - newsletter double opt-in emails from
newsletter_subscriber.subscription_requested - checkout-completion order confirmations from
order.completed - explicit admin/order-detail order confirmation resends from
order.resend_confirmation_email - order cancellation emails from
order.canceled - shipment notifications from
shipment.shipped - reimbursement notifications from
reimbursement.reimbursed - store owner new-order notifications from
order.completed - Admin payment link emails from the order payment-link action
Current ownership boundary:
spree_plunkowns only the explicitly enabled email types above.- Spree/Rails still owns invitation emails, reports, webhooks, exports, and any other extension-provided mailers that are not listed above.
- Any email type that is also sent by
spree_emailscan duplicate if both systems are configured to send it. Keep one owner per email type while this handoff is partial. - Password reset is a good early Plunk owner because the local Spree source emits
customer.password_reset_requested, but the standard consumer email subscriber set does not include a bundled password reset subscriber. - Order completion handoff preserves
notify_customer: falseandconfirmation_deliveredguards before enqueueing a Plunk send. - Reset and verification tokens are sent to Plunk as non-persistent template data.
- Storefront URLs may be local during testing, but the sender email domain must still be verified in Plunk.
- A local host-app validation on 2026-05-31 confirmed
spree_emailsabsent, all currentspree_plunktransactional switches enabled, noActionMailerdeliveries during trigger probes, and expectedspree_plunkownership for each supported trigger. Full live acceptance still requires checking Plunk delivery logs and the test inbox for one delivery per trigger window.
See docs/transactional-email-inventory.md for the detailed email attribution matrix.
- Spree admin integration form for Plunk credentials and base URL
- connectivity check against the Plunk API
- optional default sender fields for direct transactional sends
- optional public API key storage, intentionally unused by the current server-side MVP
- default-off direct transactional send settings for customer-facing
spree_emailsparity - optional inbound subscription-state webhook guarded by a bearer token
- duplicate-delivery protection for webhook intake
- retry/discard classification for async sync failures
- structured error reporting through
Rails.error.report
-
Add this extension to your Gemfile with this line:
gem 'spree_plunk', github: 'laaichiu/spree_plunk'
-
Run the install generator
bundle exec rails g spree_plunk:install
After installation:
- Open Spree Admin.
- Create or edit the Plunk integration for the target store.
- Add the Plunk base URL and secret API key.
- Verify contact sync and event tracking in a non-production environment first.
The current admin form exposes these fields.
| Field | Required | What to enter | Notes |
|---|---|---|---|
Plunk Base URL |
Yes | The Plunk API base URL, such as https://next-api.useplunk.com or your self-hosted API base like https://plunk.example.com/api |
Use the API base only. Do not paste /contacts, /events/track, or /v1/track. |
Secret API Key |
Yes | A Plunk secret server key for the workspace, typically sk_* |
This is the only key the current server-side integration needs for contact upsert, unsubscribe, and event tracking. |
Public API Key |
No | An optional browser/public key, typically pk_* |
Stored only for future use. The current server-side MVP does not use it. |
Default Sender Email |
No | A mailbox on a Plunk-verified sending domain, such as hello@shop.example.com |
Used by direct transactional sends. If blank, the extension falls back to the store mail_from_address. |
Default Sender Name |
No | A display name such as Example Store |
Used with Default Sender Email. If you set this, also set Default Sender Email so the stored sender identity is complete. |
Enable Plunk Transactional Email |
No | Check this only after the sender domain is verified in Plunk | Master switch for direct POST /v1/send delivery. Disabled by default. |
Send Password Reset Emails |
No | Check this when Plunk should send Store API password reset emails | Requires Enable Plunk Transactional Email. |
Password Reset Template ID |
No | A Plunk template ID for password reset email content | If blank, the extension sends a simple inline HTML body. |
Send Newsletter Confirmation Emails |
No | Check this when Plunk should send newsletter double opt-in emails | Requires Enable Plunk Transactional Email. Avoid enabling a second Spree/Rails owner for the same email. |
Newsletter Confirmation Template ID |
No | A Plunk template ID for newsletter confirmation content | If blank, the extension sends a simple inline HTML body. |
Send Order Confirmation Emails |
No | Check this when Plunk should send checkout-completion order confirmation emails | Requires Enable Plunk Transactional Email. Respects notify_customer: false and confirmation_delivered. |
Send Order Confirmation Resends |
No | Check this when Plunk should send explicit order confirmation resend requests | Requires Enable Plunk Transactional Email. Shares the order confirmation template. |
Order Confirmation Template ID |
No | A Plunk template ID for order confirmation content | If blank, the extension sends a simple inline HTML body. |
Send Order Cancellation Emails |
No | Check this when Plunk should send customer order cancellation emails | Requires Enable Plunk Transactional Email. Respects notify_customer: false. |
Order Cancellation Template ID |
No | A Plunk template ID for order cancellation content | If blank, the extension sends a simple inline HTML body. |
Send Shipment Shipped Emails |
No | Check this when Plunk should send shipped shipment notifications | Requires Enable Plunk Transactional Email. |
Shipment Shipped Template ID |
No | A Plunk template ID for shipment notification content | If blank, the extension sends a simple inline HTML body. |
Send Reimbursement Emails |
No | Check this when Plunk should send reimbursement notifications | Requires Enable Plunk Transactional Email. |
Reimbursement Template ID |
No | A Plunk template ID for reimbursement notification content | If blank, the extension sends a simple inline HTML body. |
Send Store Owner Notifications |
No | Check this when Plunk should send new-order notifications to the store owner address | Requires Enable Plunk Transactional Email. Respects store_owner_notification_delivered. |
Store Owner Notification Template ID |
No | A Plunk template ID for store owner new-order notification content | If blank, the extension sends a simple inline HTML body. |
Send Payment Link Emails |
No | Check this when Plunk should send Admin payment link emails | Requires Enable Plunk Transactional Email. When enabled, the Admin action does not call Spree::OrderMailer.payment_link_email. |
Payment Link Template ID |
No | A Plunk template ID for payment link email content | If blank, the extension sends a simple inline HTML body. |
Enable Subscription Webhook |
No | Check this only if you want Plunk contact subscription changes to write back into Spree | Disabled by default. |
Subscription Webhook Authorization Token |
Required only when webhook is enabled | A shared secret that you generate yourself | Plunk will send this back in the Authorization header as Bearer <token>. |
- Hosted Plunk uses
https://next-api.useplunk.com. - Self-hosted Plunk should use the API base URL that serves endpoints like
/contactsand/events/track. - Do not paste a full endpoint path into the field.
- Use a secret server key from the target Plunk workspace.
- The current extension talks to
POST /contactsandPOST /events/track, so it needs the secret key, not the public key. - A public
pk_*key will fail the connection check.
- Leave this blank unless you are preparing a later browser-side or storefront integration.
- Storing the value here does not enable client-side tracking by itself.
- Direct transactional sends use
Default Sender Emailfirst and fall back to the storemail_from_address. - Hosted Plunk validates the domain of the
fromemail address. Verifyingshop.example.comallows addresses such ashello@shop.example.com, but nothello@example.com. - These fields do not affect contact sync, newsletter sync, or event tracking.
- They are safe to leave blank if transactional email handoff is disabled.
Enable Plunk Transactional Emailis the master switch.- Each supported email type has its own opt-in switch.
- Template IDs are optional. When a template ID is present, Plunk renders the template with the provided
datapayload. When it is blank, the extension sends a simple inline HTML body. - Password reset payloads include
reset_tokenandreset_urlas non-persistent Plunk data. - Newsletter confirmation payloads include
verification_token,verification_url, andconfirmation_urlas non-persistent Plunk data. - Order confirmation, cancellation, store owner notification, and payment link payloads include non-persistent order template data such as order number, customer identity, line item fragments, totals, addresses, shipment summaries, completion time, cancellation time, and store URL.
- Shipment payloads include non-persistent shipment template data such as shipment number, order number, tracking, tracking URL, shipping method, stock location, costs, shipped time, shipped item fragments, and store URL.
- Reimbursement payloads include non-persistent reimbursement template data such as reimbursement number, order number, status, refund amounts, return item fragments, exchange item fragments, expedited exchange flags, and store URL.
- Payment link payloads include the payment URL as non-persistent Plunk data.
- Localhost storefront URLs are acceptable in test links, but the sender email domain must be verified in Plunk even for local testing.
- In Plunk, confirm that you have a usable secret API key for the correct workspace.
- In Spree Admin, set
Plunk Base URLandSecret API Key. - Run the built-in connection check from the admin UI.
- Create or update a newsletter subscriber in Spree and confirm that a Plunk contact is created.
- Complete a test order and confirm that Plunk receives
spree.order.completed. - Enable the inbound subscription webhook only after the outbound contact sync path is already behaving correctly.
- In Plunk, verify the sender domain you plan to use.
- In Spree Admin, set
Default Sender Emailto a mailbox on that verified domain, or make sure the storemail_from_addressalready uses one. - Enable
Enable Plunk Transactional Email. - Enable exactly the email types that Plunk should own.
- Add Plunk template IDs if you want Plunk-managed template content; otherwise the extension will send simple inline HTML.
- Trigger the selected email type, such as password reset, newsletter subscription request, order completion, order cancellation, shipment shipped, reimbursement, payment link, or explicit order confirmation resend.
- Watch Sidekiq and Plunk delivery logs for send failures such as unverified sender domains.
Copy-ready Plunk template HTML lives in plunk_templates/. It includes Transactional templates for the Spree Admin template ID fields and a Marketing campaign template that matches the storefront visual language.
To inspect a host app after removing spree_emails, run:
bundle exec rails runner 'load Gem.loaded_specs.fetch("spree_plunk").full_gem_path + "/script/inspect_transactional_email_handoff.rb"'The detailed spree_emails removal handoff checklist lives in docs/spree-emails-handoff-checklist.md.
Use it to distinguish Plunk direct-send smoke tests from full live handoff verification through real Spree triggers and duplicate checks.
Order-backed transactional emails expose top-level Plunk variables. Use {{order_number}}, not {{data.order_number}}, in hosted Plunk templates.
Useful variables include:
{{store_name}},{{store_code}},{{store_url}}{{recipient_email}},{{customer_email}},{{customer_name}},{{customer_first_name}},{{customer_last_name}}{{order_number}},{{order_state}},{{payment_state}},{{shipment_state}},{{completed_at}},{{canceled_at}}{{order_total}},{{item_total_display}},{{shipment_total_display}},{{tax_total_display}},{{discount_total_display}}{{line_items_html}}or{{line_items_text}}{{totals_html}}or{{totals_text}}{{shipping_address_text}},{{billing_address_text}}{{shipments_html}}or{{shipments_text}}{{payment_url}}for payment link emails
The HTML fragments are pre-rendered by spree_plunk because Plunk's current template replacement is variable-based and is not a full loop/template language. The same payload also includes structured line_items, totals, shipments, shipping_address, and billing_address values for future workflow/API use. Order-backed transactional data is sent as non-persistent Plunk data so receipts do not overwrite long-lived contact profile fields.
Shipment shipped emails expose top-level Plunk variables. Useful variables include:
{{store_name}},{{store_code}},{{store_url}}{{recipient_email}},{{customer_email}},{{customer_name}}{{order_number}},{{order_total}}{{shipment_number}},{{shipment_state}},{{shipped_at}}{{shipping_method}},{{stock_location_name}}{{tracking}},{{tracking_url}},{{tracking_link_html}}{{cost_display}},{{total_display}},{{discount_total_display}}{{shipment_items_html}}or{{shipment_items_text}}
The same payload also includes structured shipment_items for future API/workflow use. Shipment template data is sent as non-persistent Plunk data.
Reimbursement emails expose top-level Plunk variables. Useful variables include:
{{store_name}},{{store_code}},{{store_url}}{{recipient_email}},{{customer_email}},{{customer_name}}{{order_number}},{{order_total}}{{reimbursement_number}},{{reimbursement_status}},{{reimbursement_total_display}}{{paid_amount_display}},{{unpaid_amount_display}}{{return_items_html}}or{{return_items_text}}{{exchange_items_html}}or{{exchange_items_text}}{{awaiting_return_items_count}},{{expedited_exchanges}},{{expedited_exchanges_days_window}}
The same payload also includes structured return_items and exchange_items values for future API/workflow use. Reimbursement template data is sent as non-persistent Plunk data.
The current server-side integration intentionally uses:
POST /contactsfor contact upsertPOST /contactswith explicitsubscribed: truefor subscribe flowsPOST /contactswith explicitsubscribed: falsefor unsubscribe flowsPOST /events/trackfor event delivery after the contact has been ensuredPOST /v1/sendfor explicitly enabled direct transactional email handoff
This extension does not rely on /v1/track for server-side commerce sync. The reason is important: Plunk documents that /v1/track can auto-create a contact, and contacts created that way are subscribed by default. The extension avoids that path so consent stays explicit.
- behavioral activity does not imply marketing consent
- only explicit newsletter opt-in should set
subscribed: true - user, checkout, and order-backed contact creation should stay unsubscribed unless consent is known
- commerce events should preserve an existing customer consent state instead of downgrading or upgrading it implicitly
- the subscription webhook is disabled by default
- webhook writeback only accepts explicit contact subscription-state semantics
When enabled, the extension can receive a Plunk contact subscription webhook and apply that change locally.
Current behavior:
- requires explicit operator opt-in
- requires an authorization bearer token
- accepts explicit
contact.subscribedandcontact.unsubscribedsemantics - looks up the local email in Spree
- creates or verifies the matching newsletter subscriber when Plunk subscribes the contact
- removes the matching newsletter subscriber when Plunk unsubscribes the contact
- updates
accepts_email_marketingon the matching user when applicable - suppresses Spree event publication during the local writeback to avoid immediate echo loops
- ignores duplicate deliveries using replay protection
- can create a verified newsletter subscriber for a known email even when only the Plunk-side subscription state exists locally
The endpoint is:
POST /plunk/webhooks/unsubscribe/:integration_id
The request flow is:
- Spree finds the active Plunk integration by
integration_id. - The webhook must be enabled for that integration or the endpoint returns
404. - Spree checks the
Authorizationheader and requires an exact bearer-token match. - Spree accepts the request only if the payload clearly indicates contact subscription-state semantics.
- Spree resolves the contact email and applies the matching local subscribe or unsubscribe writeback.
- Duplicate deliveries are ignored so the endpoint stays idempotent.
The current webhook processor accepts these signals as proof of contact subscription state:
contact.subscribed: truecontact.subscribed: false- top-level
subscribed: true - top-level
subscribed: false - event name
contact.subscribed - event name
contact.unsubscribed
The current implementation prefers the default Plunk webhook payload and extracts the email from:
contact.email- or top-level
email
This token is not issued by Plunk.
It is a shared secret that you generate yourself and configure in both places:
- Store it in Spree Admin as
Subscription Webhook Authorization Token. - Send the same value from the Plunk webhook step as:
Authorization: Bearer your-secret-token
Example:
- Spree field
Subscription Webhook Authorization Token:12b61441e2177ef63ff91623a2c8da531c97bac8890172530f8c90a2ffb3012e
- Plunk webhook header:
- Name:
Authorization - Value:
Bearer 12b61441e2177ef63ff91623a2c8da531c97bac8890172530f8c90a2ffb3012e
- Name:
Do not put Bearer in the header name field. The header name must be Authorization, and the Bearer prefix belongs at the start of the header value.
Use a strong random value, for example:
openssl rand -hex 32or:
ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'Plunk's own webhook guidance matches the shape this extension expects: use workflow triggers such as contact.subscribed and contact.unsubscribed, send a public HTTP request, and authenticate it with a shared-secret header.
Recommended setup:
-
Save the Spree integration first so you have the integration-specific webhook URL.
-
In Plunk, create a workflow triggered by
contact.unsubscribed. -
Add a
Webhookstep. -
Set the URL to your public Spree endpoint. Use the exact integration-specific path shown in Spree Admin after you save the integration. In current Spree, integrations use prefixed params that normally look like
int_<encoded-id>, so the final URL will usually look like:https://your-spree-host.example.com/plunk/webhooks/unsubscribe/int_25Rf07xd9zThe
int_25Rf07xd9zpart is not something you invent manually. It is the saved integration'sto_param, and you can usually copy it directly from:- the webhook path shown in the Spree Admin form after saving the integration
- or the integration edit URL, which typically looks like
/admin/integrations/int_25Rf07xd9z/edit
-
Set the method to
POST. -
Add this header:
{ "Authorization": "Bearer your-secret-token" }In the Plunk UI, that means:
Name:AuthorizationValue:Bearer your-secret-token
-
Leave the body empty so Plunk sends its default payload.
-
Enable the workflow.
-
Repeat the same setup for a second Plunk workflow triggered by
contact.subscribedand send it to the same Spree endpoint.
Notes:
- The webhook URL must be publicly reachable from Plunk.
- HTTPS is preferred.
- Plunk webhook delivery is workflow-based, so no workflow means no webhook.
- This extension is built to work with Plunk's default webhook payload shape.
- If you choose a custom webhook body, it still needs to include explicit subscription-state semantics plus the contact email.
If Plunk flips a contact to subscribed and the webhook is configured correctly:
- the matching
Spree::NewsletterSubscriberrow is created or verified - the matching user's
accepts_email_marketingbecomestruewhen supported - repeating the same delivery should stay safe and idempotent
If Plunk flips a contact to unsubscribed and the webhook is configured correctly:
- the matching
Spree::NewsletterSubscriberrow is deleted when present - the matching user's
accepts_email_marketingbecomesfalsewhen supported - repeating the same delivery should stay safe and idempotent
This extension is a good fit when you want:
- Plunk to own marketing automation
- Spree customer and newsletter data to stay in sync with Plunk
- commerce events available inside Plunk workflows
- a consent-safe server-side integration without browser tracking as a requirement
The current MVP intentionally does not cover:
- automatic removal or disabling of
spree_emails - invitation, report, export, or webhook system emails
- default-enabling Plunk transactional sends without an explicit operator decision per email type
- anonymous visitor tracking
- storefront public-key or browser-side tracking
- back-in-stock workflow parity
- campaign or workflow authoring inside Spree admin