Developer Reference: Filters & Actions
Super Speedy Emails exposes a set of WordPress filters and actions so you can extend its behaviour from your own plugin, mu-plugin, or theme without modifying the plugin. This page is the complete reference for those hooks. For the database schema, PHP classes, and the sse_send() API, see the main Developer Reference.
Quick reference
| Hook | Type | Purpose |
|---|---|---|
sse_send() | function | Queue a single email from your own code. |
sse_register_support_email_type() | function | Declare a support-email type: its mode, placeholders, options and callbacks. |
sse_register_support_email_types | action | Where to call the above (fires on init, priority 5). |
sse_register_audience_filter() | function | Add a custom audience filter for marketing campaigns. |
sse_changelog_product_map | filter | (Companion plugin) map product IDs to plugin slugs. |
sse_support_assets_dir | filter | (Companion plugin) where the changelog reader looks for files. |
sse_templates_registry | filter | Add or override email templates. |
sse_confirmation_ttl_days | filter | Double opt-in link lifetime. |
sse_automation_triggers | filter | Register custom automation trigger types. |
The sse_send() function
The public entry point for sending a single email from another plugin. It runs the suppression gate, creates a log entry, and queues the message for delivery.
$result = sse_send( array(
'email' => 'customer@example.com',
'subject' => 'Your licence expires soon',
'body_text' => "Hi {{first_name}},\n\nYour licence expires in 7 days...",
'category' => 'product_marketing', // governs the suppression rules
'unsubscribe_url' => $your_unsubscribe_url, // optional; sets List-Unsubscribe
'source_plugin' => 'my-renewals-plugin',
'source_reference' => 'renewal:order:12345', // your own tag for dedup/audit
) );
if ( is_wp_error( $result ) ) {
// Blocked by a suppression, or an invalid address.
} else {
// $result is the email log row ID.
}
It returns the log row ID on success, or a WP_Error if the address is invalid or suppressed for that category. Full argument documentation is in the Developer Reference. The rest of this page is mostly about the automated support-email system, which is the main extension surface.
Support-email types
These power the Automated Support & Changelog Emails feature. The engine is generic; a type supplies the policy. You declare a type once, in code; admins then create Support Emails of that type in wp-admin, and the engine dispatches only your callbacks for records of your type.
add_action( 'sse_register_support_email_types', function () {
sse_register_support_email_type( 'security-advisory', array(
'label' => 'Security advisory',
'mode' => 'per_product', // or 'per_user'
'placeholders' => array(
'{advisory}' => 'The advisory text for this product',
),
'footer' => "You're receiving this because you own {product_name}.",
'callbacks' => array(
'should_send' => 'my_should_send',
'content' => 'my_content',
'should_send_to_user' => 'my_should_send_to_user',
'on_complete' => 'my_on_complete',
),
) );
} );
Every callback receives the running value plus a single SSE_Support_Context $context – see the Developer Guide for the full object. $context->subscriber is null in the per-product probe and set per recipient at send; $context->is_preview is true during the admin Preview (your callbacks must be side-effect-free then).
The products a support email covers are the admin’s checklist on the record – there is no hook to override them from code.
Performing side effects (writing an option, recording “we sent version X”) inside the decision callbacks. They can run during the admin Preview, when $context->is_preview is true. Do persistent writes only in on_sent or on_complete, which fire on real sends and never during Preview.
Registration arguments
| Argument | Meaning |
|---|---|
label | Shown in the editor’s type picker. |
mode | per_product (one email per product to its owners) or per_user (one email per person, aggregating everything they own). |
stages | 1 for a single body, or N for an escalating reminder sequence the admin writes in the editor. |
stage_reset_days | Restart the sequence after this many quiet days (0 = never). |
placeholders | {token} => description. Shown in the editor’s helper panel, and checked before sending. |
freq_cap | ['max' => N, 'days' => N] – the default cap, overridable per record. |
footer | The “why you’re receiving this” line, editable per record. |
admin_fields | Per-record options – see below. |
callbacks | The five callbacks below. |
should_send – per product
Runs once per product, before owners are loaded. Return true if there’s news worth sending. Default true. Guard any state write with $context->is_preview.
function my_should_send( $should, $context ) {
// $context->product_id set; $context->is_preview true during Preview
return $should; // false → skip this product (no owners loaded)
}
content – per product, then per recipient
Return { body, name }, and optionally subject. The incoming $content holds the editor’s values – return it to keep them. This is where you fill the placeholders your type declared, inside the admin’s body.
function my_content( $content, $context ) {
return array(
'body' => str_replace( '{advisory}', my_advisory_text( $context->product_id ), $content['body'] ),
'name' => get_the_title( $context->product_id ),
);
}
Per-recipient tokens ({first_name}, {unsubscribe_url}, {manage_url}, {product_name}, {email}, {site_name}, {site_url}) are filled by the engine after this runs, so leave them in place. Double-brace {{...}} is also accepted.
Filling your placeholders only when you have something to say. Replace them unconditionally – a literal {advisory} left in the body is treated as a fault, and the engine refuses to send the record at all rather than mail raw tokens to customers. The preview tells you which tokens were left unfilled.
should_send_to_user – per owner
Runs once per owner, after the per-day guard, the suppression gate and the admin’s recency rules. Return false to skip this individual. Default true.
function my_should_send_to_user( $send, $context ) {
// e.g. skip bundle-only owners:
if ( $context->matching_product && ! $context->matching_product->direct ) {
return false;
}
return $send;
}
People excluded here appear under Will NOT receive in the Preview. You don’t check unsubscribes/bounces here – the gate already removed them.
on_sent and on_complete – recording what you sent
on_sent fires once per successfully-queued email; on_complete fires once per product, after that product’s entire fan-out has finished. Both are real sends only, never Preview.
function my_on_complete( $context ) {
update_option( 'my_last_sent_' . $context->product_id, my_current_advisory_id() );
}
Advance a per-product baseline in on_complete, not on_sent. on_sent fires on recipient one of the run, so if the run dies partway through – a PHP timeout while queueing a big audience – everyone it never reached is stranded until the next time your baseline changes, rather than until tomorrow. on_complete only fires when the fan-out actually finished, so a mid-run death simply leaves the baseline alone and the next run re-covers the product.
admin_fields – your own options on the record
A type can declare options that appear in the Support Email editor, save on the record, and come back to your callbacks. Field types are select, text, number and checkbox.
'admin_fields' => array(
'audience_scope' => array(
'type' => 'select',
'label' => 'Send to',
'description' => 'All owners, or only those on an active licence.',
'default' => 'all',
'options' => array(
array( 'value' => 'all', 'label' => 'All owners' ),
array( 'value' => 'active_licence', 'label' => 'Active licence only' ),
),
),
),
Read them with $context->setting( 'audience_scope', 'all' ). The value is always resolved – an option the admin never touched comes back as your declared default.
The engine validates what gets stored against your declaration on every write path (the editor, a config import, an AI assistant over MCP): keys you didn’t declare are discarded, and a value outside your list falls back to your default. Preview honours a change before it is saved, so the admin can see the audience each choice reaches. If your plugin is deactivated, the admin’s stored settings are left alone rather than wiped.
Per-ownership meta
Anything your plugin attached to a customer’s purchase with SSE_Product_Owners::set_owner_meta( $owner_id, $key, $value ) is available on the context, batch-loaded once per run:
$renewal = $context->owner_meta( 'renewal_date' ); // for the product in context
$tier = $context->owner_meta( 'license_tier', $other_id ); // for a specific product
It’s also on each ownership row as ->meta, alongside ->days_since_last, ->direct and the rest.
Repeat behaviour
There’s no version or reference dedup in core. Repeats are governed by the per-day guard (at most one email per support-email + product + customer per calendar day), the record’s frequency cap if one is set, and your should_send callback deciding when there’s news. “Send again tomorrow because the version changed” = your callback returns true again on a new day.
Changelog-reader hooks
These belong to the companion plugin (Super Speedy Plugins – Changelog Emails), the worked-example type described above. They are not part of the core plugin.
sse_changelog_product_map (filter)
Maps a WooCommerce product ID to a plugin slug, so the reader knows which readme.txt / metadata to read for that product. Ships pre-filled with the Super Speedy Plugins catalogue.
add_filter( 'sse_changelog_product_map', function ( $map ) {
$map[12345] = 'my-plugin-slug';
return $map;
} );
sse_support_assets_dir (filter)
Overrides the local directory the changelog reader looks in for each plugin’s readme.txt and metadata file. Useful in development, or if your assets live somewhere non-standard.
add_filter( 'sse_support_assets_dir', function ( $dir ) {
return '/path/to/your/plugin/assets';
} );
Template hooks
sse_templates_registry (filter)
Adds new editable email templates, or overrides the defaults of existing ones. The registry defines each template’s label, default subject/body, and the list of supported {{tokens}}. See Editing Email Templates.
add_filter( 'sse_templates_registry', function ( $registry ) {
$registry['my_template'] = array(
'label' => 'My custom email',
'has_subject' => true,
'subject' => 'Hello {{first_name}}',
'body' => "Hi {{first_name}},\n\n...",
'placeholders' => array(
'{{first_name}}' => "Recipient's first name",
),
);
return $registry;
} );
Render a registered template in your own code with SSE_Templates::subject( $key, $vars ) and SSE_Templates::body( $key, $vars ) – only the tokens you pass are replaced, so unknown tokens are left intact for a later pass.
Other hooks
sse_confirmation_ttl_days (filter)
How long a double opt-in confirmation link stays valid, in days (default 7). See Double Opt-in.
add_filter( 'sse_confirmation_ttl_days', function ( $days ) {
return 14;
} );
sse_automation_triggers (filter)
Registers additional automation trigger types beyond the built-in ones, so your code can start an automation sequence from a custom event. See Automations & Drips.
add_filter( 'sse_automation_triggers', function ( $triggers ) {
$triggers['my_custom_event'] = array(
'label' => 'My custom event',
);
return $triggers;
} );
Where to put this code
Hook code belongs in a small plugin or mu-plugin (recommended, so it’s version-controlled and survives theme changes), or your theme’s functions.php. For a support-email type specifically, a dedicated mu-plugin keeps your sending policy in one auditable place.
Put your support-email policy in a dedicated mu-plugin rather than functions.php. It survives theme switches, is version-controlled, and keeps “who gets what, and when” in one file you can audit – which matters when the answer is “we emailed 4,000 customers”.
Further reading
sse_send() argument listSSE_Support_Context objectsse_templates_registry
Comments