Success! Link copied successfully
Back

Webhooks

Receive real-time notifications when key events happen in Churn Solution. This guide explains how the events are generated, the structure of the payload we send you, and how to securely verify every request.

Overview

When a relevant event happens in Churn Solution — for example, a customer completes a cancellation flow — we send an HTTP POST request to the endpoint URL you have registered with us. The body is JSON, and a cryptographic signature is included in a header so you can verify the payload genuinely came from Churn Solution.

HTTP method POST
Content-Type application/json
Signature header Churnsolution-Signature
Field naming All JSON keys are snake_case
Timestamps UTC Unix timestamps in seconds (not milliseconds)

You will be provided with a signing secret for each registered endpoint. Keep it secret — it is the only thing that lets you prove a payload is authentic.

Endpoint requirements

Your endpoint must:

  1. Be reachable over HTTPS at a stable, publicly accessible URL.
  2. Accept a JSON POST body.
  3. Respond with a 2xx status code once the event has been received and stored. Any 4xx/5xx response is treated as a failed delivery.
  4. Respond quickly. Acknowledge receipt first, then do heavy processing asynchronously.
Important: always verify the signature before trusting or processing any data in the payload.

Request format

Every webhook is delivered with this top-level envelope:


{
  "id": 90217,
  "app_id": "your-app-identifier",
  "data": {
    "object": "session",
    "session": { /* see The Session object below */ }
  },
  "created_at": 1718971200,
  "attempt_number": 1
}
            
Field Type Description
id integer Unique identifier of this webhook event. Use it for idempotency / de-duplication.
app_id string Your application identifier on the originating billing gateway.
data object The event payload. Always contains an object discriminator.
data.object string The type of object contained in data (e.g. "session").
created_at integer UTC Unix timestamp (seconds) when the event was created. Used in the signature.
attempt_number integer Which delivery attempt this is (starts at 1).

Request headers

Header Description
Churnsolution-Signature Hex-encoded HMAC-SHA256 signature of the payload. See Verifying signatures.
Content-Type application/json

Event types

session — Cancellation-flow session

Sent when a customer completes a cancellation-flow session. The data.object is "session" and the data.session object is populated. This is currently the primary business event.

The Session object

data.session describes a customer's journey through a cancellation flow and its outcome.


{
  "id": 12345,
  "status": "CANCELLED",
  "is_saved": false,
  "created_at": 1718971200,
  "customer": {
    "id": "cus_abc123",
    "name": "Jane Doe",
    "email": "[email protected]",
    "created_at": 1700000000
  },
  "subscription": {
    "id": "sub_abc123",
    "status": "ACTIVE",
    "start_date": 1700000000,
    "current_period_start": 1717200000,
    "current_period_end": 1719792000,
    "trial_start": null,
    "trial_end": null
  },
  "plan": {
    "id": "price_abc123",
    "recurring": { "interval": "month", "interval_count": 1 },
    "currency": "usd",
    "unit_amount": 2000,
    "unit_amount_decimal": 20.0,
    "quantity": 1,
    "plan_summary": "$20.00 / month"
  },
  "cancellation_flow": {
    "id": 55,
    "name": "Default Cancel Flow",
    "created_at": 1700000000,
    "updated_at": 1716000000
  },
  "uncancel_coupon": null,
  "steps": [ /* see Session steps below */ ]
}
            
Field Type Description
id integer Unique session identifier.
status string (enum) Final session status. See values below.
is_saved boolean true if the customer was retained (saved); false if they churned.
created_at integer UTC Unix seconds when the session was created.
customer object The customer. See Customer.
subscription object The subscription in question. See Subscription.
plan object The plan / price on the subscription. See Plan.
cancellation_flow object The flow (or A/B variant) the customer went through. See Cancellation flow.
uncancel_coupon object Coupon applied if the customer un-cancelled; null when not applicable. See Coupon.
steps array Ordered steps the customer went through. See Session steps.

status values

ABANDONED, SEND_TO_PAGE, CONTACT, TRIAL_EXTENSION, PAUSE, SWITCH_PLAN, COUPON, CANCELLED, PENDING_CANCEL, UNCANCELLED, UNPAUSED, REACTIVATED, SWITCH_PLAN_WITH_COUPON.

Customer

Field Type Description
id string Customer identifier on the billing gateway.
name string Customer name.
email string Customer email.
created_at integer UTC Unix seconds when the customer was created.

Subscription

Field Type Description
id string Subscription identifier on the billing gateway.
status string (enum) Subscription status. See values below.
start_date integer UTC Unix seconds — subscription start.
current_period_start integer UTC Unix seconds — current billing period start.
current_period_end integer UTC Unix seconds — current billing period end.
trial_start integer UTC Unix seconds — trial start (null if no trial).
trial_end integer UTC Unix seconds — trial end (null if no trial).

status values: ACTIVE, TRIALING, PAST_DUE, PAUSED, INCOMPLETE, INCOMPLETE_EXPIRED, CANCELED, UNPAID, EXPIRED, PENDING, HALTED, ON_HOLD.

Plan

Field Type Description
id string Plan / price identifier on the billing gateway.
recurring object Billing cadence: { "interval": "month", "interval_count": 1 }.
recurring.interval string Billing interval, e.g. day, week, month, year.
recurring.interval_count integer Number of intervals between billings.
currency string ISO currency code, e.g. usd.
unit_amount integer Amount in the smallest currency unit (e.g. cents).
unit_amount_decimal number Amount in major units, e.g. 20.0.
quantity integer Quantity of the plan.
plan_summary string Human-readable summary of the plan.

Cancellation flow

Field Type Description
id integer Cancellation flow identifier.
name string Name of the flow shown to the customer (A/B variant or audience).
created_at integer UTC Unix seconds — flow creation.
updated_at integer UTC Unix seconds — last flow update.

Session steps

session.steps is an array describing each step the customer encountered, in display order. Each step has a type, and the fields relevant to that type are populated.


{
  "id": 9001,
  "type": "SURVEY",
  "order": 1,
  "contains_offer": true,
  "survey_answer": "Too expensive",
  "followup_question": "What price would work for you?",
  "followup_question_answer": "Around $10",
  "form_answer": null,
  "offer": {
    "accepted": true,
    "coupon": {
      "name": "Win-back 50%",
      "code": "STAY50",
      "currency": "usd",
      "amount_off": null,
      "amount_off_decimal": null,
      "percent_off": 50,
      "duration": "repeating",
      "duration_in_months": 3,
      "number_of_billing_cycles": 3,
      "currency_symbol": "$",
      "summary": "50% for 3 billing cycles"
    },
    "pause": null,
    "trial_extension": null
  }
}
            
Field Type Description
id integer Step identifier.
type string (enum) One of FORM, SURVEY, OFFER.
order integer 1-based position of this step within the session.
contains_offer boolean true if an offer was presented at this step.
survey_answer string (SURVEY) The survey option the customer selected.
followup_question string (SURVEY) Follow-up question shown for the selected option, if any.
followup_question_answer string (SURVEY) The customer's free-text follow-up answer.
form_answer string (FORM) The customer's free-text form answer.
offer object The offer presented at this step. Present when contains_offer is true. See Offer.
Fields not relevant to a step's type are null. For example, a FORM step has form_answer populated and survey_answer null.

Offer

Exactly one of coupon, pause, or trial_extension is populated, indicating the kind of offer made.

Field Type Description
accepted boolean true if the customer accepted the offer.
coupon object A discount coupon. null if not a coupon offer. See Coupon.
pause object A subscription pause: { "number_of_months": 2 }.
trial_extension object A trial extension: { "number_of_days": 14 }.

Coupon

Field Type Description
name string Coupon name.
code string Coupon code.
currency string ISO currency code (for fixed-amount coupons).
amount_off integer Fixed discount in the smallest currency unit (e.g. cents). null for percentage coupons.
amount_off_decimal number Fixed discount in major units. null for percentage coupons.
percent_off integer Percentage discount. null for fixed-amount coupons.
duration string once, repeating, or forever.
duration_in_months integer Number of months the coupon repeats (when duration is repeating).
number_of_billing_cycles integer Number of billing cycles the coupon applies to, if defined.
currency_symbol string Display symbol for currency, e.g. $.
summary string Human-readable summary of the discount.

Security

Verifying signatures

Every request includes a Churnsolution-Signature header. It is a hex-encoded HMAC-SHA256 of the payload, keyed with your endpoint's signing secret.

How the signature is computed


signed_payload = "<created_at>;<raw_request_body>"
signature      = lowercase_hex( HMAC_SHA256( signing_secret, signed_payload ) )
            
  • <created_at> is the integer value of the top-level created_at field in the body.
  • <raw_request_body> is the exact, unmodified raw bytes of the request body. Do not re-serialize the parsed JSON — whitespace and key ordering must match exactly.
  • The two parts are joined by a single semicolon (;).

Verification steps

  1. Read the raw request body as received (before any JSON parsing or reformatting).
  2. Read the created_at value from the body.
  3. Compute HMAC_SHA256(signing_secret, created_at + ";" + raw_body) and hex-encode it.
  4. Compare your result to the Churnsolution-Signature header using a constant-time comparison.
  5. Reject the request (401) if they do not match.

Node.js (Express)


const crypto = require('crypto');
const express = require('express');
const app = express();

const SIGNING_SECRET = process.env.CHURNSOLUTION_SIGNING_SECRET;

// Capture the raw body — required for signature verification.
app.post(
  '/webhooks/churnsolution',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body; // Buffer
    const signature = req.get('Churnsolution-Signature');

    const createdAt = JSON.parse(rawBody.toString()).created_at;
    const signedPayload = `${createdAt};${rawBody.toString()}`;

    const expected = crypto
      .createHmac('sha256', SIGNING_SECRET)
      .update(signedPayload)
      .digest('hex');

    const valid =
      signature &&
      expected.length === signature.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

    if (!valid) return res.status(401).send('Invalid signature');

    const event = JSON.parse(rawBody.toString());
    res.status(200).send('ok');   // acknowledge immediately
    handleEvent(event);           // process asynchronously
  }
);
            

Python (Flask)


import hmac, hashlib, json, os
from flask import Flask, request, abort

app = Flask(__name__)
SIGNING_SECRET = os.environ["CHURNSOLUTION_SIGNING_SECRET"]


@app.post("/webhooks/churnsolution")
def churnsolution_webhook():
    raw_body = request.get_data()  # raw bytes
    signature = request.headers.get("Churnsolution-Signature", "")

    created_at = json.loads(raw_body)["created_at"]
    signed_payload = f"{created_at};{raw_body.decode()}".encode()

    expected = hmac.new(
        SIGNING_SECRET.encode(), signed_payload, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        abort(401)

    event = json.loads(raw_body)
    return "ok", 200
            

Delivery & retries

  • Success is any non-error HTTP response (2xx/3xx). A 4xx/5xx, a connection error, or a timeout is recorded as a failed delivery.
  • The attempt_number field indicates the delivery attempt for this event.
  • Idempotency: the same logical event may be delivered more than once. De-duplicate using the top-level id so you process each event exactly once.
  • Ordering: do not assume webhooks arrive in the order events occurred. Use created_at to order events if needed.

Best practices

  • Always verify the signature before acting on a payload.
  • Respond fast (2xx) and process asynchronously.
  • Be tolerant of new fields. We may add fields over time; ignore unknown fields rather than failing.
  • Treat fields as nullable. Optional fields may be null or absent depending on the scenario.
  • Keep your signing secret secret. Store it in a secrets manager or environment variable, never in client-side code.
  • Use HTTPS so payloads are encrypted in transit.

Quick reference

HTTP method POST
Body format JSON, snake_case keys
Signature header Churnsolution-Signature
Signature algorithm HMAC-SHA256, hex-encoded (lowercase)
Signed payload "<created_at>;<raw_body>"
Signing key Per-endpoint signing secret (provided to you)
Expected response 2xx
Timestamps UTC Unix seconds
Idempotency key Top-level id