Docs
For developers
Filter and action hooks the free plugin exposes. Companion plugins (including the pro plugin) extend the free plugin entirely through these hooks — no core file modifications.
Subscriber lifecycle
do_action( 'otts_subscriber_created', int $id, string $email, string $first_name )— fires after a successful new subscription. Used internally by the welcome email and the lead-magnet handler. Hook this to push the subscriber to an external CRM.do_action( 'otts_subscriber_unsubscribed', int $id, string $email )— fires when someone clicks unsubscribe.
Send pipeline
apply_filters( 'otts_send_recipients', array $subscribers, WP_Post $post )— filter the recipient list before a send. The pro plugin's Stripe paywall uses this to droppast_due/cancelledpaid subscribers from paid newsletter sends.apply_filters( 'otts_render_for_subscriber', string $html, array $subscriber, WP_Post $post )— per-recipient HTML filter. The paywall returns a teaser + Subscribe CTA for free subscribers receiving paid issues; the open-rate tracker injects a 1×1 pixel at priority 99.
Email providers
apply_filters( 'otts_provider_choices', array $choices )— add labels to the Email Provider dropdown.apply_filters( 'otts_make_provider', ?OTTS_Provider_Base $provider, string $slug, array $config )— return a provider instance for your custom slug. SubclassOTTS_Provider_Baseand implementslug(),label(),config_fields(),test_connection(),send_campaign().
Forms
apply_filters( 'otts_form_types', array $types )— add slugs (e.g.popup,slidein) to the supported form-type list. Render via CSS classes.otts-form-{type}.
Editor & audit
apply_filters( 'otts_editor_action', ?string $result, string $action, string $text, string $tone )— register a custom AI editor action. Return the rewritten text ornullto defer.apply_filters( 'otts_audit_checks', array $checks, int $newsletter_id, WP_Post $post )— append or transform pre-send audit checks. Each check is['id' => ..., 'label' => ..., 'status' => 'pass|warn|fail', 'note' => ...].
Settings extension
apply_filters( 'otts_settings_tabs', array $tabs )— add tabs to Newsletter → Settings.do_action( 'otts_render_settings_tab_{tab}' )— render the body of your custom tab. Companion plugin's callback echoes form fields directly inside the active form element.do_action( 'otts_save_settings_{tab}' )— handle the POST when your tab is saved. Nonce verification is already done by the free plugin.
REST API
POST /wp-json/otts/v1/subscribe— public, takesemail, optionalfirst_name, honeypotwebsite.GET /wp-json/otts/v1/unsubscribe— public, takesemail+tokenquery params.GET /wp-json/otts/v1/lead-magnet/download— public, takese(email) +x(expires) +s(HMAC).POST /wp-json/otts/v1/generate— admin-only, triggers a generation.POST /wp-json/otts/v1/audit/{id}— admin-only, runs the audit on a newsletter.POST /wp-json/otts/v1/editor/{action}— admin-only, runs an editor action.POST /wp-json/otts/v1/send/{id}— admin-only, sends a newsletter immediately.
Newsletter templates (pro plugin)
Subclass OTTS_Pro_Template_Base and register your template at plugins_loaded via OTTS_Pro_Templates::register( $instance ). The four required methods:
slug()— unique key stored in_otts_templatepost metalabel()— name shown in the Newsletter → Templates dropdowndescription()— one-paragraph explanation rendered next to the radiobuild_prompt( array $args )— return the full Claude prompt for your structure. Receivestone,length,topic. The first line of Claude’s expected response must beSubject: ...followed by a blank line, then the HTML body.render_email( string $content_html, WP_Post $post, array $subscriber )— wrap the stored HTML in your styled email shell. Hooks the free plugin’sotts_render_for_subscriberfilter at priority 5 (before paywall and open-rate).
See includes/templates/class-template-newsroom.php in the pro plugin for a full reference implementation.
Other filters
apply_filters( 'otts_claude_model', string $model )— override the Claude model used for generation and editor actions. Defaultclaude-sonnet-4-5.
Source & license
One Two Three Send is GPLv2-or-later. The full source ships with the plugin — read it, fork it, contribute back. Bug reports and feature requests go to our contact page.