Developer Reference
Technical reference for developers extending or integrating with Super Speedy Emails.
Last updated for DB version 2.5.0 / plugin version 1.05. If
get_option( 'sse_db_version' )reports a higher number, this doc may be behind.
Table of Contents
Database Tables
All tables use the WordPress prefix (e.g. wp_sse_lists). Schema changes go through SSE_Activator::create_tables() via dbDelta.
Phase 1 tables (signup / forms / subscription management)
sse_lists
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | List name |
slug | varchar(255) | URL-friendly slug (unique) |
description | text | Optional description |
status | varchar(20) | active or archived |
settings | longtext | JSON for misc settings |
requires_confirmation | tinyint(1) | Double opt-in flag (default 1) — Phase 2B |
is_newsletter | tinyint(1) | 1 = this list is the article-newsletter audience (single-select) |
category | varchar(40) | Email category this list maps to (list_broadcast default) — Phase 2C |
created_at / updated_at | datetime |
sse_subscribers
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email | varchar(255) | Email address (unique) |
first_name / last_name | varchar(255) | |
wp_user_id | bigint(20) unsigned | Linked WordPress user ID (nullable) |
cookie_id | varchar(64) | Guest identification cookie value |
ip_address | varchar(45) | IP at time of subscription |
status | varchar(20) | pending / active / bounced / complained / suppressed — Phase 2A |
confirmed_at | datetime | Double opt-in confirmation timestamp — Phase 2B |
marketing_opted_out_at | datetime | When marketing-opt-out happened |
marketing_opt_out_reason | varchar(64) | user_request / complaint / bounce / migrated_unsubscribed |
digest_frequency | varchar(20) | instant / digest — Phase 2C |
unsubscribe_token | varchar(64) | Long random token for one-click unsubscribe URLs |
source | varchar(64) | form / migration_mailpoet / migration_mailerlite / wc_purchase / admin |
created_at / updated_at | datetime |
sse_subscriber_lists
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK to sse_subscribers |
list_id | bigint(20) unsigned | FK to sse_lists |
status | varchar(20) | subscribed, unsubscribed, or pending |
subscribed_at | datetime | |
unsubscribed_at | datetime | nullable |
Unique on (subscriber_id, list_id).
sse_forms
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | Internal form name |
list_ids | longtext | JSON array of list IDs |
fields | longtext | JSON: {"first_name": true, "last_name": false} |
style | longtext | JSON: preset, colors, border radius, button text |
popup_settings | longtext | JSON popup config (null if not a popup) |
success_message | text | |
status | varchar(20) | active or inactive |
created_at / updated_at | datetime |
Phase 2 tables (sending pipeline)
sse_suppressions
The authoritative do-not-send store. Scopes: global, marketing, category:<term_id>, list:<list_id>, product_marketing:<product_id>.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email | varchar(255) | Normalized lowercase |
scope | varchar(80) | See above |
reason | varchar(64) | user_unsubscribe / complaint / bounce_hard / bounce_soft_repeated / manual / migrated |
source | varchar(64) | Where the suppression came from |
created_at | datetime |
Unique on (email, scope).
sse_newsletter_prefs
Per-subscriber, per-category preferences. Missing rows mean defaulted-in.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK to sse_subscribers |
term_id | bigint(20) unsigned | WP term_id of a top-level post category |
subscribed | tinyint(1) | 1 = wants this category |
updated_at | datetime |
Unique on (subscriber_id, term_id).
sse_campaigns
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | Internal name |
category | varchar(40) | See Email Categories |
type | varchar(20) | broadcast / automation_step / digest |
subject | varchar(255) | |
from_name / from_email / reply_to | varchar(255) | Optional per-campaign overrides |
body_text | longtext | Plain-text body (with merge tags) |
body_html | longtext | HTML body (optional; auto-generated when omitted) |
audience_query | longtext | JSON segment query — see Segments |
audience_snapshot | longtext | Frozen recipient list at sending transition |
automation_id | bigint(20) unsigned | FK to sse_automations (null for broadcasts) |
schedule_at | datetime | nullable |
status | varchar(20) | draft / scheduled / sending / sent / cancelled |
sent_at | datetime | |
recipient_count | int unsigned | Frozen count from snapshot |
created_at / updated_at | datetime |
sse_automations
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | |
trigger_type | varchar(40) | wc_product_purchased / manual / custom (via filter) |
trigger_config | longtext | JSON, e.g. { "product_ids": [...], "match": "any" } |
category | varchar(40) | Email category for all steps |
status | varchar(20) | active / paused |
created_at / updated_at | datetime |
sse_automation_steps
Ordered steps for an automation. Cumulative offsets from enrolment timestamp.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
automation_id | bigint(20) unsigned | FK |
position | int unsigned | Sort order (0-indexed) |
offset_days / offset_hours | int | Cumulative offset from enrollment_created_at |
subject | varchar(255) | |
body_text | longtext | |
body_html | longtext | |
created_at / updated_at | datetime |
Index on (automation_id, position).
sse_automation_enrollments
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
automation_id | bigint(20) unsigned | FK |
subscriber_id | bigint(20) unsigned | FK |
context | longtext | JSON, e.g. { "order_id": 123, "product_id": 456 } |
context_hash | varchar(32) | md5 of dedup-relevant context (e.g. order_id) |
current_step | int unsigned | Position of the next step to send |
next_send_at | datetime | When the current step is due |
status | varchar(20) | active / complete / cancelled |
created_at / updated_at | datetime |
Unique on (automation_id, subscriber_id, context_hash). Index on (next_send_at, status).
sse_email_log
One row per attempted send. Drives reporting + compliance audit trail.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | nullable for fully ad-hoc |
email | varchar(255) | Captured at send-time |
campaign_id / automation_id | bigint(20) unsigned | nullable |
category | varchar(40) | Denormalised for fast filtering |
legal_basis | varchar(40) | consent / legitimate_interest / service |
subject | varchar(255) | |
mailgun_message_id | varchar(255) | |
status | varchar(20) | queued / sent / delivered / bounced / complained / failed |
opened_at / clicked_at / bounced_at / complained_at / unsubscribed_at / delivered_at | datetime | First-event timestamps |
open_count / click_count | int unsigned | Cumulative counters |
error | text | Provider error text |
context | longtext | JSON merge-data snapshot |
source_plugin / source_reference | varchar | For 3rd-party integrations (e.g. ssp-product-renewals / renewal:order:123) |
test_mode | varchar(20) | live / log_only / redirect |
created_at / updated_at | datetime |
Indexes: (subscriber_id, created_at), (campaign_id, status), mailgun_message_id, category, test_mode.
sse_email_events
Raw event stream from MailGun webhooks. Append-only.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email_log_id | bigint(20) unsigned | FK (nullable — events can arrive before match) |
mailgun_message_id | varchar(255) | |
event | varchar(40) | delivered / opened / clicked / unsubscribed / complained / permanent_fail / temporary_fail |
payload | longtext | Full webhook payload (JSON) |
occurred_at | datetime | From the webhook payload |
received_at | datetime | When we received it |
sse_email_queue
Cron-drained send queue. Small (rows live only while pending).
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email_log_id | bigint(20) unsigned | FK — log row is created up-front |
priority | int | Lower = sooner (transactional 1, marketing 10) |
attempts | int unsigned | |
next_attempt_at | datetime | Exponential backoff (1m → 4m → 16m → 64m → 256m) |
locked_by | varchar(64) | Worker ID |
locked_until | datetime | |
last_error | text | |
created_at | datetime |
sse_product_owners
Derived cache populated from completed/processing WC orders. Bundle expansion applied at write time.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK |
product_id | bigint(20) unsigned | The resolved product (direct line item OR bundle component) |
direct | tinyint(1) | 1 = bought it directly, 0 = via a bundle |
first_purchased_at / last_purchased_at | datetime |
Unique on (subscriber_id, product_id, direct).
sse_confirmation_tokens
Double opt-in tokens. 7-day TTL.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
token | varchar(64) | Unique |
subscriber_id | bigint(20) unsigned | FK |
list_ids | longtext | JSON array of list IDs being confirmed |
form_id | bigint(20) unsigned | nullable |
expires_at | datetime | |
consumed_at | datetime | nullable |
created_at | datetime |
PHP Classes
Phase 1
SSE_Lists
SSE_Lists::create( array $data ): int|WP_Error
SSE_Lists::get( int $id ): object|null
SSE_Lists::get_all( array $args ): array
SSE_Lists::count( array $args ): int
SSE_Lists::update( int $id, array $data ): bool|WP_Error
SSE_Lists::delete( int $id ): bool
SSE_Lists::get_subscriber_count( int $id ): int
SSE_Subscribers
SSE_Subscribers::create( array $data ): int|WP_Error
SSE_Subscribers::get( int $id ): object|null
SSE_Subscribers::get_by_email( string $email ): object|null
SSE_Subscribers::get_by_cookie( string $cookie_id ): object|null
SSE_Subscribers::get_by_user_id( int $user_id ): object|null
SSE_Subscribers::update( int $id, array $data ): bool
SSE_Subscribers::delete( int $id ): bool
SSE_Subscribers::get_all( array $args ): array
SSE_Subscribers::count( array $args ): int
SSE_Subscribers::subscribe_to_list( int $subscriber_id, int $list_id, string $status = 'subscribed' ): bool
SSE_Subscribers::unsubscribe_from_list( int $subscriber_id, int $list_id ): bool
SSE_Subscribers::get_lists( int $subscriber_id ): array
SSE_Subscribers::export_csv( array $args ): void // Streams CSV
SSE_Subscribers::total_count(): int
SSE_Forms
SSE_Forms::create( array $data ): int|WP_Error
SSE_Forms::get( int $id ): object|null // JSON fields auto-decoded
SSE_Forms::get_all( array $args ): array
SSE_Forms::count( array $args ): int
SSE_Forms::update( int $id, array $data ): bool
SSE_Forms::delete( int $id ): bool
SSE_Forms::render( int $id, string $context = 'inline' ): string
SSE_Forms::get_popups_for_page( int $page_id, string $url ): array
Phase 2 — sending pipeline
sse_send() (global function)
The public send API. Used by ssp-product-renewals and any other plugin that wants SSE to deliver one message.
sse_send( array $args ): int|WP_Error
$args keys:
| Key | Type | Description |
|---|---|---|
email (or subscriber_email) | string | Recipient email (required) |
category | string | One of: newsletter / list_broadcast / product_marketing / product_support / onboarding / transactional. Default: transactional. |
subject | string | Subject line (required) |
body_text | string | Plain-text body (required) |
body_html | string | Optional HTML body |
from_name / from_email / reply_to | string | Optional per-message overrides |
subscriber_id | int | Optional; resolves automatically from email if omitted |
campaign_id / automation_id | int | Optional FKs for log row |
gate_context | array | Per-call hints for the suppression gate (e.g. { category_term_id: 12 } for newsletter sends) |
context | array | Snapshot data persisted to sse_email_log.context |
source_plugin / source_reference | string | For third-party integrations |
priority | int | Queue priority (lower = sooner). Default 10 for marketing, 1 for transactional. |
Returns: the new sse_email_log row ID, or WP_Error (invalid email, blocked by suppression gate, queue insert failure).
A return of WP_Error( 'sse_suppressed', ... ) is normal — it means the recipient is suppressed for that category and the send was correctly skipped.
SSE_Suppressions
SSE_Suppressions::add( string $email, string $scope = 'global', string $reason = '', string $source = '' ): int|false
SSE_Suppressions::remove( string $email, string $scope = 'global' ): bool
SSE_Suppressions::is_suppressed( string $email, string $scope ): bool
SSE_Suppressions::is_globally_suppressed( string $email ): bool
SSE_Suppressions::get_scopes_for_email( string $email ): string[]
SSE_Suppressions::get_all( array $args ): array
SSE_Suppressions::count( array $args ): int
Constants: REASON_USER_UNSUBSCRIBE, REASON_COMPLAINT, REASON_BOUNCE_HARD, REASON_BOUNCE_SOFT_REPEAT, REASON_MANUAL, REASON_MIGRATED.
SSE_Suppression_Gate
SSE_Suppression_Gate::check( string $email, string $category, array $context = array() ): array
// Returns: [ 'allowed' => bool, 'reason' => string|null ]
Encapsulates the category × suppression matrix. Every send goes through this.
SSE_Email_Log
SSE_Email_Log::create( array $data ): int // Always reads test_mode from option
SSE_Email_Log::get( int $id ): object|null
SSE_Email_Log::record_event( int $log_id, string $event, string|null $occurred_at = null ): bool
SSE_Email_Log::find_by_mailgun_id( string $message_id ): object|null
SSE_Email_Log::for_subscriber( int $subscriber_id, array $args = array() ): array
SSE_Email_Log::campaign_stats( int $campaign_id ): array
SSE_Email_Queue
SSE_Email_Queue::init(): void // Registers cron schedule + drain hook
SSE_Email_Queue::enqueue( int $log_id, int $priority = 10 ): int|false
SSE_Email_Queue::drain( int|null $batch_size = null ): int // Returns messages sent
SSE_Email_Queue::pending_count(): int
SSE_Sender
SSE_Sender::send( array $args ): bool|WP_Error
// Internal — wraps wp_mail with category-aware From + Mailgun header injection.
// You don't call this directly; sse_send() / the drainer do.
SSE_Campaigns
SSE_Campaigns::create( array $data ): int|WP_Error
SSE_Campaigns::get( int $id ): object|null
SSE_Campaigns::get_all( array $args ): array
SSE_Campaigns::update( int $id, array $data ): bool|WP_Error
SSE_Campaigns::delete( int $id ): bool
SSE_Campaigns::send( int $id ): array|WP_Error // Refuses unless status in draft/scheduled
SSE_Campaigns::send_test( int $id, string $to_email ): int|WP_Error
SSE_Campaigns::reset_to_draft( int $id ): bool
SSE_Campaigns::resolve_audience( object $campaign ): int[] // Subscriber IDs
Status constants: STATUS_DRAFT, STATUS_SCHEDULED, STATUS_SENDING, STATUS_SENT, STATUS_CANCELLED.
SSE_Confirmations
SSE_Confirmations::issue( int $subscriber_id, int[] $list_ids, int|null $form_id = null ): string // Token
SSE_Confirmations::consume( string $token ): object|WP_Error // Returns the subscriber row
SSE_Confirmations::send_confirmation_email( int $subscriber_id, string $token ): int|WP_Error
SSE_Newsletter
SSE_Newsletter::init(): void // Registers transition_post_status + hourly tick
SSE_Newsletter::enabled_terms(): int[] // Allowed top-level term_ids
SSE_Newsletter::dispatch_instant( int $post_id ): int // Recipients queued
SSE_Newsletter::run_digest( int|null $now_gmt = null ): int // Recipients queued
SSE_Newsletter::resolve_top_level_terms( int $post_id ): int[]
SSE_Newsletter::is_schedule_match( int $now_gmt = null ): bool
SSE_Bundles
SSE_Bundles::components_of( int $product_id ): int[] // [] if not a bundle
SSE_Bundles::is_available(): bool // True if WC Product Bundles tables present
SSE_Product_Owners
SSE_Product_Owners::init(): void // Registers WC hooks
SSE_Product_Owners::on_status_changed( int $order_id, string $old, string $new, $order = null ): void
SSE_Product_Owners::rebuild_for_order( int $order_id ): void
SSE_Product_Owners::rebuild_all( int $days = 0 ): int // Backfill (used by migration)
SSE_Product_Owners::subscriber_ids_owning( int $product_id, bool $include_bundles = true ): int[]
Constant: PAID_STATUSES = [ 'completed', 'processing' ].
SSE_Segment
SSE_Segment::resolve( array $query, string $category = 'list_broadcast' ): int[] // Wraps with suppression mask
SSE_Segment::resolve_raw( array $query ): int[] // No mask — for testing
See Product Audiences & Segments for the predicate language.
SSE_Automations
SSE_Automations::create( array $data ): int|WP_Error
SSE_Automations::get( int $id ): object|null // trigger_config auto-decoded
SSE_Automations::get_all( array $args = array() ): array
SSE_Automations::update( int $id, array $data ): bool
SSE_Automations::delete( int $id ): bool
SSE_Automations::add_step( int $automation_id, array $data ): int
SSE_Automations::update_step( int $step_id, array $data ): bool
SSE_Automations::delete_step( int $step_id ): bool
SSE_Automations::get_steps( int $automation_id ): array
SSE_Automations::count_steps( int $automation_id ): int
SSE_Automation_Scheduler
SSE_Automation_Scheduler::init(): void // Registers WC hooks + tick
SSE_Automation_Scheduler::enroll( int $automation_id, int $subscriber_id, array $context = [] ): int|WP_Error // Idempotent on (auto, sub, context_hash)
SSE_Automation_Scheduler::tick(): int // Number of steps fired
SSE_Automation_Scheduler::on_wc_order_status_changed( int $order_id, string $old, string $new, $order = null ): void
SSE_Automation_Scheduler::on_wc_order_refunded( int $order_id ): void
SSE_Automation_Scheduler::match_wc_product_purchased( object $automation, array $purchase_data ): bool
SSE_Automation_Scheduler::registered_triggers(): array // Filterable via 'sse_automation_triggers'
SSE_Mailgun_Webhook
SSE_Mailgun_Webhook::handle( WP_REST_Request $request ): WP_REST_Response
// Registered at /wp-json/sse/v1/mailgun-webhook. HMAC-verified.
SSE_Migrate (Phase 2F)
SSE_Migrate::apply_subscriber( array $rec, bool $commit = false ): array
SSE_Migrate::import_wc_customers( int $days = 0, bool $commit = false ): array
Action constants: ACTION_ACTIVE, ACTION_PENDING, ACTION_UNSUBSCRIBED, ACTION_BOUNCED, ACTION_COMPLAINED.
SSE_Migrate_MailPoet::is_installed(): bool
SSE_Migrate_MailPoet::build_list_map( bool $commit = false, array $existing_map = [] ): array
SSE_Migrate_MailPoet::run( bool $commit = false ): array // Report
SSE_Migrate_MailerLite::parse_csv( string $path ): array
SSE_Migrate_MailerLite::run( string $path, bool $commit = false ): array // Report
Filters & Actions
apply_filters( 'sse_automation_triggers', array $triggers ): array
Register custom triggers for SSE automations. Used by ssp-product-renewals (and anyone else).
add_filter( 'sse_automation_triggers', function( $triggers ) {
$triggers['renewal_due_t30'] = array(
'label' => 'Renewal due in 30 days',
'matcher' => 'my_plugin_match_renewal_t30', // callable
);
return $triggers;
} );
Your matcher is called by the scheduler when external code invokes SSE_Automation_Scheduler::enroll() for that trigger type. See class-sse-automation-scheduler.php for the calling convention.
AJAX Endpoints
sse_subscribe
Form submissions. Registered for both logged-in and anonymous users.
POST: sse_form_id, sse_email, sse_first_name, sse_last_name, sse_nonce, sse_hp (honeypot)
Response: { success, data }
sse_update_subscriptions
Subscription preference updates from the management page. Also handles newsletter category prefs + frequency.
POST: nonce, list_ids[], newsletter_terms[], digest_frequency
Response: { success, data: { message } }
REST API
GET /wp-json/sse/v1/forms
Returns active forms. Used by the Gutenberg block editor.
Permission: edit_posts Response: [ { id, name }, ... ]
GET /wp-json/sse/v1/confirm?token=...
Double opt-in confirmation landing. Consumes the token, flips subscriber to active.
Permission: public
POST /wp-json/sse/v1/mailgun-webhook
MailGun event ingestion endpoint. HMAC-verified using sse_mailgun_webhook_key.
Permission: public (signature is the security)
WP-CLI Commands
wp sse migrate mailpoet [--dry-run|--commit]
wp sse migrate mailerlite --file=<path> [--dry-run|--commit]
wp sse migrate wc-customers [--days=<n>] [--dry-run|--commit]
wp sse migrate report
Default mode is --dry-run. --commit writes. Re-runs are idempotent.
See Email Migration Guide for usage.
Cron Schedules
Registered by SSE_Activator::schedule_cron() on activation:
| Hook | Interval | Class |
|---|---|---|
sse_drain_email_queue | every minute (sse_every_minute) | SSE_Email_Queue::drain() |
sse_newsletter_digest_tick | hourly | SSE_Newsletter::run_digest_if_due() |
sse_automation_tick | every minute | SSE_Automation_Scheduler::tick() |
Custom interval sse_every_minute is added via the cron_schedules filter in SSE_Email_Queue::init().
Constants
Defined in super-speedy-emails.php:
| Constant | Value |
|---|---|
SSE_VERSION | 1.04 (read from plugin header) |
SSE_PLUGIN_DIR | Absolute path to plugin directory |
SSE_PLUGIN_URL | URL to plugin directory |
SSE_PLUGIN_FILE | Path to main plugin file |
SSE_PLUGIN_BASENAME | Plugin basename for hooks |
Options
Phase 1
| Option | Default | Description |
|---|---|---|
sse_db_version | 2.5.0 | Database schema version |
sse_from_name | Site name | Global sender name |
sse_from_email | Admin email | Global sender email |
sse_manage_page_id | 0 | ID of the subscription management page |
sse_wc_myaccount_enabled | false | WooCommerce My Account tab toggle |
sse_wc_endpoint_slug | email-preferences | WooCommerce endpoint slug |
sse_cookie_lifetime | 365 | Cookie lifetime (days) |
sse_delete_data_on_uninstall | false | Delete all data on uninstall |
Phase 2 — sending
| Option | Default | Description |
|---|---|---|
sse_test_mode | live | live / log_only / redirect |
sse_test_redirect_email | empty | Destination for redirect mode |
sse_mailgun_webhook_key | empty | MailGun webhook signing key |
sse_from_name__<category> | empty | Per-category sender name override |
sse_from_email__<category> | empty | Per-category sender email override |
sse_reply_to__<category> | empty | Per-category Reply-To override |
sse_newsletter_enabled_terms | [] | JSON array of term_ids enabled for the newsletter |
sse_newsletter_schedule | [{day:tue,hour:12},{day:sat,hour:12}] | Digest schedule (GMT) |
sse_newsletter_digest_intro | empty | Optional intro text above the post list |
sse_newsletter_last_run_at | 0 | Unix timestamp of last digest run |
sse_migrate_mailpoet_prefix | empty | Override MailPoet table prefix (testing) |
sse_support_email_cron_enabled | 0 | Opt-in: run every active Support Email on the daily sse_support_email_check cron |
Support Emails (product-update / changelog engine)
Added in DB 2.3.0. A Support Email is a record in sse_support_rules; the engine fans out over its product checklist (one email per product to that product’s owners), applying a per-day guard, the suppression gate and recency rules. Full guide: Automated Support Emails — Developer Guide.
Table sse_support_rules: id, name, status (draft/active/paused), product_ids (JSON — the fan-out checklist), match_mode (all/any), conditions (JSON recency rules), subject, body, last_run_at, created_at, updated_at. (reference_mode/freq_cap_days columns are legacy/unused.)
Classes:
SSE_Support_Rules— CRUD for the records +passes_audience($record,$context)(recency rules) +describe().SSE_Support_Emails— the engine:plan_support_email($record,$is_preview,$only_product),run_support_email($record,$only_product),daily_check()(cron, opt-in), AJAXajax_preview()/ajax_send().SSE_Support_Context— the object passed to every support-email hook (support_email_id,product_id,subscriber,matching_product,matching_products[],all_products[],is_preview,name; helpersis_product_mode(),owns(),bought_within(),owned_row(),wc_customer()).
Hooks (each callback gets ($value, SSE_Support_Context $context)):
sse_should_send_support_email(filter, per product, default true)sse_support_email_subject(filter, optional subject override)sse_support_email_content(filter, returns{ body, name })sse_support_email_should_send_to_user(filter, per owner, default true)sse_support_email_sent(action, per queued email, real sends only)
There is no sse_support_email_product_ids filter and no version/reference dedup — repeat behaviour is the per-day guard (sse_email_log.source_reference = support:<record_id>:<product_id>). Changelog behaviour lives in the separate ssp-changelog-emails companion plugin.
AJAX actions: sse_support_email_preview (per-product preview from the unsaved editor form), sse_support_email_send (send one product of a saved record).
CSS Classes
Frontend form classes for custom styling — unchanged from Phase 1.
| Class | Element |
|---|---|
.sse-form | Form wrapper |
.sse-form--minimal / --clean / --branded | Preset modifier |
.sse-form--loading | Applied during AJAX submission |
.sse-form--success | Applied after successful submission |
.sse-form__inner | The <form> element |
.sse-form__fields | Fields container (flexbox) |
.sse-form__field | Individual field wrapper |
.sse-form__input | Input elements |
.sse-form__input.sse-has-error | Input with validation error |
.sse-form__submit | Submit button |
.sse-form__message | Error/info message area |
.sse-form__success | Success message (hidden until success) |
[data-form-id="X"] | Target a specific form by ID |
Enqueued Assets
| Handle | Type | When loaded |
|---|---|---|
sse-forms | CSS + JS | When a form is rendered |
sse-popup | CSS + JS | When a popup form matches the current page |
sse-admin | CSS + JS | Admin pages with sse- in the hook |
Extending: a worked example
A third-party plugin (e.g. ssp-product-renewals) that wants to send a renewal email through SSE:
// In your plugin:
$result = sse_send( array(
'email' => $customer_email,
'category' => 'product_marketing',
'subject' => 'Your Scalability Pro renewal is due',
'body_text' => $rendered_body,
'subscriber_id' => $sub_id,
'context' => array(
'order_id' => $order_id,
'product_id' => $product_id,
'renewal_due_at' => $due_at,
),
'source_plugin' => 'ssp-product-renewals',
'source_reference' => "renewal:order:{$order_id}:t-{$days_until}",
) );
if ( is_wp_error( $result ) ) {
if ( $result->get_error_code() === 'sse_suppressed' ) {
// Recipient is suppressed for this category — that's a normal outcome.
// Log it for your own audit if needed, but don't treat as a failure.
} else {
// Genuine error — invalid email, DB failure, etc.
error_log( 'SSE send failed: ' . $result->get_error_message() );
}
}
// $result is the sse_email_log row ID on success.
SSE handles the gate check, queue insert, From: address per category, audit logging, and delivery via the cron drainer. The other plugin doesn’t need to know about MailGun, suppressions, or queues — just compose and call.