Registering Custom Audience Filters (target any audience you like)

Dave Hilditch

On the roadmap. The Audience Filter Registry described here is a planned Super Speedy Emails feature. We’re publishing the developer API ahead of release so you — and the tools/AI assistants you build on top of the plugin — can design your integrations now. The shape below is stable in intent; small details may change before it ships.

Super Speedy Emails’ audience builder lets you stack simple include/exclude filters to reach exactly the right subscribers. The built-in filters cover lists, product ownership, purchase recency and so on — but your site has data of its own: membership tiers in usermeta, warranty dates in order-item meta, an ACF field on products, a licence renewal date. The Audience Filter Registry lets you add those as first-class filters: you register one filter, and it appears in the builder’s dropdown with the right inputs and a live recipient count, and it’s honoured identically in the count, the preview, the test send and the real send.

This guide is for developers (and the AI assistants helping them). It’s a copy-paste cookbook.

The idea in one sentence

You call sse_register_audience_filter( $key, $def ) with a friendly label, a set of typed parameters, and a callback that returns the subscriber IDs that match — and the plugin does the rest (dropdown entry, input rendering, live count, evaluation).

The one function

sse_register_audience_filter( 'my_filter_key', array(
    'label'       => 'Human name shown in the dropdown',
    'description' => 'One line explaining what it targets.',
    'group'       => 'both',      // 'include' | 'exclude' | 'both' (default 'both')
    'params'      => array( /* typed inputs — see below */ ),
    'callback'    => function ( array $params ) {
        // $params is already validated + coerced to the declared types.
        // Return an array of matching SUBSCRIBER IDs (ints).
        return array( /* subscriber ids */ );
    },
) );
  • $key — a unique slug (namespace it, e.g. acme_plan_tier). Lowercase, letters/numbers/underscores.
  • Register on the init hook (after checking the function exists), or via the sse_audience_filters filter for lazy registration.

Declaring parameters (the inputs shown in the builder)

Each parameter you declare becomes an input in the builder. The registry sanitises the value to the declared type before your callback runs, so your callback never re-sanitises GUI input.

typeBuilder controlYour callback receives
productsearchable product pickerproduct id (int); blank = “any” if required:false
numbernumber inputint/float (honours min, max, step, default)
texttext inputsanitised string
selectdropdownone of your options values
checkboxcheckboxbool
listdropdown of your SSE listslist id (int)
termsearchable term pickerterm id (int); set taxonomy
datedate inputY-m-d string

Every parameter also supports label, help, required, and default.

The callback contract (read this)

  • Return a set of subscriber IDs, not a yes/no per person. The audience engine resolves the whole audience at once and combines filters with set algebra — a per-subscriber check would be far too slow for the live count that refreshes as you type. Compute the matching set with one (or a few) queries and return the ids.
  • Use $wpdb->prepare() for any SQL. Your parameters are already type-coerced, but prepare anyway.
  • If your filter’s provider data is missing (e.g. a column that doesn’t exist), return an empty array — never error.

To make the common shapes one-liners, use the SSE_Audience helpers (they do the fiddly WooCommerce/WordPress → subscriber-id mapping for you):

SSE_Audience::all_active(): int[]
SSE_Audience::owning_product( int $product_id, bool $include_bundles = true ): int[]
SSE_Audience::from_emails( array $emails ): int[]                 // WC billing/account emails → subscriber ids
SSE_Audience::from_user_ids( array $user_ids ): int[]             // wp_user_id → subscriber ids
SSE_Audience::where_user_meta( string $key, string $compare, $value ): int[]
SSE_Audience::owners_where_product_meta( string $key, $value = null ): int[]                    // product postmeta
SSE_Audience::owners_where_owner_meta( string $key, string $compare, $value, int $product_id = 0 ): int[]      // per-ownership meta (see below)
SSE_Audience::owners_where_owner_meta_date_within( string $key, string $from, string $to, int $product_id = 0 ): int[]
SSE_Audience::run_segment( array $query ): int[]                 // compose the built-in primitives directly

Worked examples

1. Membership tier from usermeta (e.g. an ACF/members field)

“Email everyone on the Gold plan.”

add_action( 'init', function () {
    if ( ! function_exists( 'sse_register_audience_filter' ) ) return;

    sse_register_audience_filter( 'acme_plan_tier', array(
        'label'       => 'Membership plan',
        'description' => 'Subscribers whose account has this membership plan.',
        'params'      => array(
            'plan' => array(
                'type'    => 'select',
                'label'   => 'Plan',
                'options' => array(
                    array( 'value' => 'gold',   'label' => 'Gold' ),
                    array( 'value' => 'silver', 'label' => 'Silver' ),
                ),
            ),
        ),
        'callback'    => fn( $p ) => SSE_Audience::where_user_meta( 'membership_plan', '=', $p['plan'] ),
    ) );
} );

2. Renewal / warranty window (per-ownership meta from the order)

“Email people whose warranty expires in the next 30 days.”

For data that lives on the order line (a warranty date, a renewal date, a licence tier…), store it once as per-ownership meta when the purchase is ingested, then have your filter query it. Super Speedy Emails fires sse_product_owner_upserted after each purchase’s ownership row is written, and gives you SSE_Product_Owners::set_owner_meta():

// (a) Ingest: attach your data to the ownership row when it's created/refreshed.
add_action( 'sse_product_owner_upserted', function ( $owner_id, $ctx ) {
    if ( $ctx['item'] instanceof WC_Order_Item_Product ) {
        $expiry = (string) $ctx['item']->get_meta( 'warranty_expiry' ); // Y-m-d, or ''
        SSE_Product_Owners::set_owner_meta( $owner_id, 'warranty_expiry', $expiry ?: null );
    }
}, 10, 2 );

// (b) Target: a filter that reads that owner meta.
sse_register_audience_filter( 'acme_warranty_expiring', array(
    'label'       => 'Warranty expiring within N days',
    'description' => 'Buyers whose warranty_expiry date falls within N days of today.',
    'params'      => array(
        'days' => array( 'type' => 'number', 'label' => 'Within days', 'default' => 30, 'min' => 1 ),
    ),
    'callback'    => function ( $p ) {
        return SSE_Audience::owners_where_owner_meta_date_within(
            'warranty_expiry',
            gmdate( 'Y-m-d' ),
            gmdate( 'Y-m-d', time() + (int) $p['days'] * DAY_IN_SECONDS )
        );
    },
) );

To backfill the meta onto existing customers, re-drive the owner rebuild (SSE_Product_Owners::rebuild_all()), which re-fires the hook for every past order. This ingest-then-filter split is exactly how the bundled ssp-product-renewals companion implements “Licence tier” and “Licence expiry window”.

3. A flag on the product (product postmeta)

“Email owners of any product marked as a beta.”

sse_register_audience_filter( 'acme_owns_beta', array(
    'label'    => 'Owns a beta product',
    'params'   => array(),   // no inputs
    'callback' => fn( $p ) => SSE_Audience::owners_where_product_meta( '_is_beta', 'yes' ),
) );

Once registered, each of these appears in SSE → Emails → audience builder as an include/exclude row with its inputs, updates the live “matched / after opt-outs” count as you change it, and is honoured for real when you send.

Where to put this code

In a small plugin or mu-plugin (recommended, so it’s version-controlled and survives theme changes), or your theme’s functions.php. A dedicated plugin per integration keeps your targeting logic in one auditable place — that’s exactly what Super Speedy Plugins does for its own licence filters (a tiny companion plugin that registers Licence tier and Licence expiry window).

Notes

  • Permissions/security: filters are registered by your (trusted) server-side code; their parameters come only from the admin builder (manage_options). The registry sanitises params by declared type before your callback runs.
  • Deactivating your plugin: any saved campaign that used your filter shows a disabled “filter unavailable — its plugin may be inactive” row instead of silently changing who gets the email. Re-activate to restore it.
  • Advanced JSON: a registered filter compiles to { "type": "filter", "key": "…", "params": { … } }, so you can also hand-write it in the Advanced JSON box.

Related

Building targeted audiences in the Emails screen, Product Audiences & Segments, Developer Reference, Developer Reference: Filters & Actions.

Leave a Reply

Your email address will not be published. Required fields are marked *

×
1/1