ChurnStop
Reference · 10 min read

API and hooks reference

This page documents every surface through which ChurnStop exposes cancellation-event data: the REST endpoints served by the plugin itself, the WordPress action / filter hooks, and the payload shapes. All surfaces are stable and versioned; breaking changes require a major-version bump of the plugin.

The API that lives at api.churnstop.org is for the paid-tier license and benchmark backend and is documented separately. This page covers the in-WordPress surface.

REST endpoints

All endpoints are namespaced under /wp-json/churnstop/v1/. All endpoints require manage_woocommerce capability. All endpoints return JSON.

GET /stats/summary

Month-to-date aggregate. The default admin dashboard calls this on page load.

{
  "platform": "woocommerce",
  "period_start": "2026-04-01 00:00:00",
  "period_end": "2026-04-30 23:59:59",
  "total_attempts": 47,
  "saved": 15,
  "save_rate": 31.9,
  "mrr_preserved_cents": 37400
}

The platform field is always "woocommerce" in Phase 1. Future phases reserve "stripe", "paddle", "recurly", "chargebee", "lemonsqueezy", "custom".

GET /flows

List of flows configured on this site. Includes the active default flow plus any variants used for A/B testing.

[
  {
    "id": 1,
    "name": "Default save flow",
    "trigger_event": "wc_subs_cancel_clicked",
    "is_active": 1,
    "created_at": "2026-04-15 10:12:33"
  }
]

GET /events

List of cancellation events, newest first. Paginated via limit query param (default 25, max 100).

[
  {
    "id": 412,
    "user_id": 88,
    "subscription_id": 2041,
    "platform": "woocommerce",
    "external_subscription_id": "2041",
    "flow_id": 1,
    "cancel_reason": "too_expensive",
    "offer_shown": "discount",
    "offer_accepted": 1,
    "final_status": "saved",
    "monthly_value_cents": 4900,
    "created_at": "2026-04-17 09:22:11",
    "resolved_at": "2026-04-17 09:22:47"
  }
]

Use this for custom dashboards or for exporting to an external analytics pipeline.

GET /settings and POST /settings

Read and write ChurnStop's settings object. POST payloads go through the ClickToCancel compliance validator before save; non-compliant configurations return HTTP 422 with a violations array.

{
  "settings": {
    "default_discount_percent": 20,
    "default_discount_cycles": 3,
    "default_pause_days": 30
  },
  "defaults": { ... }
}

WordPress action hooks

Actions fire when specific events happen. Use these to write to your own analytics or trigger downstream automation.

churnstop_cancellation_started

Fires when a customer clicks Cancel and the save flow starts.

do_action(
    'churnstop_cancellation_started',
    int $event_id,
    int $subscription_id,
    array $context  // [ 'user_id', 'monthly_value_cents' ]
);

churnstop_survey_submitted

Fires when the customer answers the cancellation reason question.

do_action(
    'churnstop_survey_submitted',
    int $event_id,
    string $reason,           // e.g. 'too_expensive'
    string $free_text_answer  // optional comment the customer typed
);

churnstop_offer_accepted

Fires when the customer clicks to accept a save offer.

do_action(
    'churnstop_offer_accepted',
    int $event_id,
    array $offer  // [ 'type', 'value_percent' | 'duration_days' | ..., 'duration_billing_cycles' ]
);

churnstop_cancellation_resolved

Terminal hook. Fires when a cancellation is either saved (offer accepted) or completed (customer cancelled). This is the hook to bind for pushing events into external analytics; it gives you the full lifecycle record.

do_action(
    'churnstop_cancellation_resolved',
    int $event_id,
    array $event  // full event row: reason, offer_shown, offer_accepted, final_status, monthly_value_cents, created_at, resolved_at
);

Example - push every resolved event to Segment:

add_action( 'churnstop_cancellation_resolved', function( $event_id, $event ) {
    Segment::track([
        'userId'     => (string) $event['user_id'],
        'event'      => $event['final_status'] === 'saved' ? 'Subscription Saved' : 'Subscription Cancelled',
        'properties' => [
            'subscription_id'       => $event['external_subscription_id'],
            'reason'                => $event['cancel_reason'],
            'offer_shown'           => $event['offer_shown'],
            'offer_accepted'        => (bool) $event['offer_accepted'],
            'monthly_value_cents'   => $event['monthly_value_cents'],
            'platform'              => $event['platform'],
        ],
    ]);
}, 10, 2 );

WordPress filter hooks

Filters let you modify behaviour rather than just observing it.

churnstop_default_flow

Override the default flow configuration at activation or on settings save. See the Offer types reference for the full example.

churnstop_offer_for_reason

Swap the offer that gets shown for a specific reason, per subscriber or per request. Useful for running custom routing logic outside ChurnStop's own A/B engine.

add_filter( 'churnstop_offer_for_reason', function( $offer, $reason, $context ) {
    // Offer a 40% discount instead of 20% for VIP customers
    if ( $reason === 'too_expensive' && is_user_vip( $context['user_id'] ) ) {
        $offer['value_percent'] = 40;
    }
    return $offer;
}, 10, 3 );

churnstop_compliance_validators

Add custom compliance checks on top of the built-in ones. Your check receives the proposed flow configuration and returns an array of violations (empty array = pass).

add_filter( 'churnstop_compliance_validators', function( $validators ) {
    $validators['require_cancel_confirmation_receipt'] = function( $config ) {
        // Ensure the store's custom legal requirement is met
        if ( empty( $config['send_confirmation_email'] ) ) {
            return [ 'Compliance: a cancellation confirmation email must be sent.' ];
        }
        return [];
    };
    return $validators;
} );

The built-in validators cannot be removed via this filter; they can only be supplemented.

Event payload schema

All events stored in wp_churnstop_cancellation_events have this shape. Fields new in a minor version are additive only.

interface CancellationEvent {
  id: number;
  user_id: number;
  subscription_id: number;              // WC-scoped integer (Phase 1 only)
  platform: 'woocommerce';              // Phase 2 reserves stripe | paddle | recurly | ...
  external_subscription_id: string;     // always string at the boundary; WC ID cast to string
  flow_id: number;
  cancel_reason: string | null;         // e.g. "too_expensive"; null until survey submitted
  survey_response_json: string | null;  // raw JSON of survey answers
  offer_shown: string | null;           // one of: discount | pause | skip_renewal | tier_down | extend_trial | product_swap
  offer_accepted: 0 | 1;
  final_status: 'pending' | 'in_progress' | 'saved' | 'cancelled';
  monthly_value_cents: number;          // integer cents
  created_at: string;                   // MySQL datetime
  resolved_at: string | null;           // MySQL datetime; null until final
}

Versioning

  • Plugin major version (1.x -> 2.x) reserves the right to break this API.
  • Plugin minor version (1.0 -> 1.1) is additive only: new endpoints, new hooks, new payload fields.
  • Plugin patch version (1.0.0 -> 1.0.1) is bug fixes only.

Breaking changes to the payload shape always trigger a major version bump. We do not retroactively add required fields; new fields are always optional with a documented default.

What's next