Developers

Developer Reference

Published · Last updated

Technical reference for developers extending or integrating with Super Speedy Emails.

Last updated for DB version 2.10.0 / plugin version 1.16.0. If get_option( 'sse_db_version' ) reports a higher number, this doc may be behind.


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

ColumnTypeDescription
idbigint(20) unsignedPrimary key
namevarchar(255)List name
slugvarchar(255)URL-friendly slug (unique)
descriptiontextOptional description
statusvarchar(20)active or archived
settingslongtextJSON for misc settings
requires_confirmationtinyint(1)Double opt-in flag (default 1) – Phase 2B
is_newslettertinyint(1)1 = this list is the article-newsletter audience (single-select)
categoryvarchar(40)Email category this list maps to (list_broadcast default) – Phase 2C
created_at / updated_atdatetime

sse_subscribers

ColumnTypeDescription
idbigint(20) unsignedPrimary key
emailvarchar(255)Email address (unique)
first_name / last_namevarchar(255)
wp_user_idbigint(20) unsignedLinked WordPress user ID (nullable)
cookie_idvarchar(64)Guest identification cookie value
ip_addressvarchar(45)IP at time of subscription
statusvarchar(20)pending / active / bounced / complained / suppressed – Phase 2A
confirmed_atdatetimeDouble opt-in confirmation timestamp – Phase 2B
marketing_opted_out_atdatetimeWhen marketing-opt-out happened
marketing_opt_out_reasonvarchar(64)user_request / complaint / bounce / migrated_unsubscribed
digest_frequencyvarchar(20)instant / digest – Phase 2C
unsubscribe_tokenvarchar(64)Long random token for one-click unsubscribe URLs
sourcevarchar(64)form / migration_mailpoet / migration_mailerlite / wc_purchase / admin
created_at / updated_atdatetime

sse_subscriber_lists

ColumnTypeDescription
idbigint(20) unsignedPrimary key
subscriber_idbigint(20) unsignedFK to sse_subscribers
list_idbigint(20) unsignedFK to sse_lists
statusvarchar(20)subscribed, unsubscribed, or pending
subscribed_atdatetime
unsubscribed_atdatetimenullable

Unique on (subscriber_id, list_id).

sse_forms

ColumnTypeDescription
idbigint(20) unsignedPrimary key
namevarchar(255)Internal form name
list_idslongtextJSON array of list IDs
fieldslongtextJSON: {"first_name": true, "last_name": false}
stylelongtextJSON: preset, colors, border radius, button text
popup_settingslongtextJSON popup config (null if not a popup)
success_messagetext
statusvarchar(20)active or inactive
created_at / updated_atdatetime

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>.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
emailvarchar(255)Normalized lowercase
scopevarchar(80)See above
reasonvarchar(64)user_unsubscribe / complaint / bounce_hard / bounce_soft_repeated / manual / migrated
sourcevarchar(64)Where the suppression came from
created_atdatetime

Unique on (email, scope).

sse_newsletter_prefs

Per-subscriber, per-category preferences. Missing rows mean defaulted-in.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
subscriber_idbigint(20) unsignedFK to sse_subscribers
term_idbigint(20) unsignedWP term_id of a top-level post category
subscribedtinyint(1)1 = wants this category
updated_atdatetime

Unique on (subscriber_id, term_id).

sse_campaigns

ColumnTypeDescription
idbigint(20) unsignedPrimary key
namevarchar(255)Internal name
categoryvarchar(40)See Email Categories
typevarchar(20)broadcast / automation_step / digest
subjectvarchar(255)
from_name / from_email / reply_tovarchar(255)Optional per-campaign overrides
body_textlongtextPlain-text body (with merge tags)
body_htmllongtextHTML body (optional; auto-generated when omitted)
audience_querylongtextJSON segment query – see Segments
audience_snapshotlongtextFrozen recipient list at sending transition
automation_idbigint(20) unsignedFK to sse_automations (null for broadcasts)
schedule_atdatetimenullable
statusvarchar(20)draft / scheduled / sending / sent / cancelled
sent_atdatetime
recipient_countint unsignedFrozen count from snapshot
created_at / updated_atdatetime

sse_automations

ColumnTypeDescription
idbigint(20) unsignedPrimary key
namevarchar(255)
trigger_typevarchar(40)wc_product_purchased / manual / custom (via filter)
trigger_configlongtextJSON, e.g. { "product_ids": [...], "match": "any" }
categoryvarchar(40)Email category for all steps
statusvarchar(20)active / paused
created_at / updated_atdatetime

sse_automation_steps

Ordered steps for an automation. Cumulative offsets from enrolment timestamp.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
automation_idbigint(20) unsignedFK
positionint unsignedSort order (0-indexed)
offset_days / offset_hoursintCumulative offset from enrollment_created_at
subjectvarchar(255)
body_textlongtext
body_htmllongtext
created_at / updated_atdatetime

Index on (automation_id, position).

sse_automation_enrollments

ColumnTypeDescription
idbigint(20) unsignedPrimary key
automation_idbigint(20) unsignedFK
subscriber_idbigint(20) unsignedFK
contextlongtextJSON, e.g. { "order_id": 123, "product_id": 456 }
context_hashvarchar(32)md5 of dedup-relevant context (e.g. order_id)
current_stepint unsignedPosition of the next step to send
next_send_atdatetimeWhen the current step is due
statusvarchar(20)active / complete / cancelled
created_at / updated_atdatetime

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.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
subscriber_idbigint(20) unsignednullable for fully ad-hoc
emailvarchar(255)Captured at send-time
campaign_id / automation_idbigint(20) unsignednullable
categoryvarchar(40)Denormalised for fast filtering
legal_basisvarchar(40)consent / legitimate_interest / service
subjectvarchar(255)
mailgun_message_idvarchar(255)
statusvarchar(20)queued / sent / delivered / bounced / complained / failed
opened_at / clicked_at / bounced_at / complained_at / unsubscribed_at / delivered_atdatetimeFirst-event timestamps
open_count / click_countint unsignedCumulative counters
errortextProvider error text
contextlongtextJSON merge-data snapshot
source_plugin / source_referencevarcharFor 3rd-party integrations (e.g. ssp-product-renewals / renewal:order:123)
test_modevarchar(20)live / log_only / redirect
created_at / updated_atdatetime

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.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
email_log_idbigint(20) unsignedFK (nullable – events can arrive before match)
mailgun_message_idvarchar(255)
eventvarchar(40)delivered / opened / clicked / unsubscribed / complained / permanent_fail / temporary_fail
payloadlongtextFull webhook payload (JSON)
occurred_atdatetimeFrom the webhook payload
received_atdatetimeWhen we received it

sse_email_queue

Cron-drained send queue. Small (rows live only while pending).

ColumnTypeDescription
idbigint(20) unsignedPrimary key
email_log_idbigint(20) unsignedFK – log row is created up-front
priorityintLower = sooner (transactional 1, marketing 10)
attemptsint unsigned
next_attempt_atdatetimeExponential backoff (1m → 4m → 16m → 64m → 256m)
locked_byvarchar(64)Worker ID
locked_untildatetime
last_errortext
created_atdatetime

sse_product_owners

Derived cache populated from completed/processing WC orders. Bundle expansion applied at write time.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
subscriber_idbigint(20) unsignedFK
product_idbigint(20) unsignedThe resolved product (direct line item OR bundle component)
directtinyint(1)1 = bought it directly, 0 = via a bundle
first_purchased_at / last_purchased_atdatetime

Unique on (subscriber_id, product_id, direct).

Performance Tip

sse_product_owners is a derived cache with bundle expansion applied at write time, so “owners of product X” segments resolve with a single indexed lookup rather than walking orders at send time. Rebuild it with SSE_Product_Owners::rebuild_all() after a bulk import or a data fix.

sse_confirmation_tokens

Double opt-in tokens. 7-day TTL.

ColumnTypeDescription
idbigint(20) unsignedPrimary key
tokenvarchar(64)Unique
subscriber_idbigint(20) unsignedFK
list_idslongtextJSON array of list IDs being confirmed
form_idbigint(20) unsignednullable
expires_atdatetime
consumed_atdatetimenullable
created_atdatetime

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:

KeyTypeDescription
email (or subscriber_email)stringRecipient email (required)
categorystringOne of: newsletter / list_broadcast / product_marketing / product_support / onboarding / transactional. Default: transactional.
subjectstringSubject line (required)
body_textstringPlain-text body (required)
body_htmlstringOptional HTML body
from_name / from_email / reply_tostringOptional per-message overrides
subscriber_idintOptional; resolves automatically from email if omitted
campaign_id / automation_idintOptional FKs for log row
gate_contextarrayPer-call hints for the suppression gate (e.g. { category_term_id: 12 } for newsletter sends)
contextarraySnapshot data persisted to sse_email_log.context
source_plugin / source_referencestringFor third-party integrations
priorityintQueue 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:

HookIntervalClass
sse_drain_email_queueevery minute (sse_every_minute)SSE_Email_Queue::drain()
sse_newsletter_digest_tickhourlySSE_Newsletter::run_digest_if_due()
sse_automation_tickevery minuteSSE_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:

ConstantValue
SSE_VERSION1.04 (read from plugin header)
SSE_PLUGIN_DIRAbsolute path to plugin directory
SSE_PLUGIN_URLURL to plugin directory
SSE_PLUGIN_FILEPath to main plugin file
SSE_PLUGIN_BASENAMEPlugin basename for hooks

Options

Phase 1

OptionDefaultDescription
sse_db_version2.5.0Database schema version
sse_from_nameSite nameGlobal sender name
sse_from_emailAdmin emailGlobal sender email
sse_manage_page_id0ID of the subscription management page
sse_wc_myaccount_enabledfalseWooCommerce My Account tab toggle
sse_wc_endpoint_slugemail-preferencesWooCommerce endpoint slug
sse_cookie_lifetime365Cookie lifetime (days)
sse_delete_data_on_uninstallfalseDelete all data on uninstall

Phase 2 – sending

OptionDefaultDescription
sse_test_modelivelive / log_only / redirect
sse_test_redirect_emailemptyDestination for redirect mode
sse_mailgun_webhook_keyemptyMailGun webhook signing key
sse_from_name__<category>emptyPer-category sender name override
sse_from_email__<category>emptyPer-category sender email override
sse_reply_to__<category>emptyPer-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_introemptyOptional intro text above the post list
sse_newsletter_last_run_at0Unix timestamp of last digest run
sse_migrate_mailpoet_prefixemptyOverride MailPoet table prefix (testing)
sse_support_emails_paused1Scheduled Support Email sending is paused. Defaults to paused; the manual “Send now” button is unaffected

Support Emails (product-update / changelog engine)

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. Policy comes from the record’s type, which a plugin registers in code. Full guide: Automated Support Emails – Developer Guide.

Table sse_support_rules: id, name, status (draft/active/paused), type (registered type key), uid, product_ids (JSON – the fan-out checklist), match_mode (all/any), conditions (JSON recency rules), subject, body, stages (JSON – the authored reminder sequence), settings (JSON – the type’s own per-record options), footer, freq_cap_max, freq_cap_days, stage_reset_days, last_run_at, created_at, updated_at.

Registering a type:

sse_register_support_email_type( 'my-type', array(
    'label'            => 'My email type',
    'mode'             => 'per_product',   // or 'per_user'
    'stages'           => 1,               // N for an escalating reminder sequence
    'stage_reset_days' => 0,
    'placeholders'     => array( '{token}' => 'What it is' ),
    'freq_cap'         => array( 'max' => 0, 'days' => 0 ),
    'footer'           => "You're receiving this because you own {product_name}.",
    'admin_fields'     => array( /* per-record options - see below */ ),
    'callbacks'        => array( /* below */ ),
) );

Register on the sse_register_support_email_types action (fired on init, priority 5) or on plugins_loaded.

Callbacks (each gets ($value, SSE_Support_Context $context), except the actions which get ($context)):

CallbackKindRunsDefault
should_sendfilterOnce per product, before owners loadtrue
contentfilterPer product, then per recipient. Returns { body, name } and optionally subjectpassthrough
should_send_to_userfilterOnce per ownertrue
on_sentactionPer queued email, real sends only–
on_completeactionOnce per product, after its fan-out finished–

Advance a per-product baseline in on_complete, not on_sent: on_sent fires on recipient one of the run, so a run that dies partway strands everyone it never reached until the next baseline change.

admin_fields – per-record options the type declares, rendered in the editor, stored on the record and resolved onto $context->settings:

'admin_fields' => array(
    'audience_scope' => array(
        'type'        => 'select',        // select | text | number | checkbox
        'label'       => 'Send to',
        'description' => 'Who this reaches.',
        'default'     => 'all',
        'options'     => array(
            array( 'value' => 'all',            'label' => 'All owners' ),
            array( 'value' => 'active_licence', 'label' => 'Active licence only' ),
        ),
    ),
),

Read with $context->setting( 'audience_scope', 'all' ). Values are validated against the declaration on every write path (editor, config import, MCP): undeclared keys are discarded and an out-of-range value falls back to the declared default. When the type is not registered, stored settings are left untouched rather than wiped.

Classes:

  • SSE_Support_Types – the type registry: register(), get(), all(), fields(), callback(), record_type(), sanitize_settings(), resolve_settings().
  • 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), preview_content($record,$product_id), daily_check() (cron), is_paused() / set_paused(), AJAX ajax_preview() / ajax_preview_content() / ajax_send().
  • SSE_Support_Context – the object passed to every callback (support_email_id, product_id, subscriber, matching_product, matching_products[], all_products[], settings, is_preview, name, rule; helpers is_product_mode(), setting(), owner_meta(), owns(), bought_within(), owned_row(), wc_customer()).

Ownership rows carry owner_id and meta – whatever any plugin attached to that purchase with SSE_Product_Owners::set_owner_meta() – batch-loaded once per fan-out.

Sending is blocked when a record’s type is not registered, or when the type’s own declared placeholders are still present in the finished subject or body. The reason is reported in the preview.

Repeat behaviour is the per-day guard (sse_email_log.source_reference = support:<record_id>:<product_id>), or the record’s frequency cap when one is set. Changelog behaviour lives in the separate ssp-changelog-emails companion plugin.

AJAX actions: sse_support_email_preview (per-product reach from the unsaved editor form), sse_support_email_preview_content (the rendered email for a real recipient), sse_support_email_send (send one product of a saved record).


CSS Classes

Frontend form classes for custom styling – unchanged from Phase 1.

ClassElement
.sse-formForm wrapper
.sse-form--minimal / --clean / --brandedPreset modifier
.sse-form--loadingApplied during AJAX submission
.sse-form--successApplied after successful submission
.sse-form__innerThe <form> element
.sse-form__fieldsFields container (flexbox)
.sse-form__fieldIndividual field wrapper
.sse-form__inputInput elements
.sse-form__input.sse-has-errorInput with validation error
.sse-form__submitSubmit button
.sse-form__messageError/info message area
.sse-form__successSuccess message (hidden until success)
[data-form-id="X"]Target a specific form by ID

Enqueued Assets

HandleTypeWhen loaded
sse-formsCSS + JSWhen a form is rendered
sse-popupCSS + JSWhen a popup form matches the current page
sse-adminCSS + JSAdmin 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.

Further reading

the full hook catalogue with signatures
add your own first-class audience filters
the wp sse command group
how the pieces fit together

Comments

▲
▼
1/1