Docs
Sign up

Webhooks

Churn Solution sends real-time notifications when key events occur in your account. This guide covers how events are generated, the structure of each payload, and how to verify that every request came from Churn Solution.

View as Markdown
On this page
  1. Overview
  2. Endpoint requirements
  3. Request format
  4. Request headers
  5. Event types
  6. session — Cancellation-flow session
  7. The Session object
  8. status values
  9. Customer
  10. Subscription
  11. Subscription status values
  12. Plan
  13. Cancellation flow
  14. Session steps
  15. Offer
  16. Coupon
  17. Security
  18. Verifying signatures
  19. How the signature is computed
  20. Verification steps
  21. Delivery & retries
  22. Best practices
  23. Quick reference

Overview

When a relevant event occurs — such as a customer completing a cancellation flow — Churn Solution sends an HTTP POST request to your registered endpoint. The body is JSON, and a signature in the request headers lets you confirm the sender.

Property Value
HTTP method POST
Content-Type application/json
Signature header Churnsolution-Signature
Field naming snake_case keys
Timestamps UTC Unix seconds (not milliseconds)

Each registered endpoint receives its own signing secret. Keep it private — it is what proves a payload is authentic.

Endpoint requirements

  1. Serve over HTTPS

    At a stable, public URL.

  2. Accept a JSON POST body

    The request is sent as application/json.

  3. Return a 2xx status

    Once the event is stored. Any 4xx or 5xx counts as a failed delivery.

  4. Respond quickly

    Acknowledge receipt first and process heavy work asynchronously.

Always verify the signature before trusting any data in the payload.

Request format

Every webhook uses the same top-level envelope: an event id, your app_id, a data object whose object field names the payload type, created_at and attempt_number.

{
  "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 event ID. Use it for de-duplication.
app_id string Your app identifier on the billing gateway.
data object The event payload, with an object discriminator.
data.object string Object type, e.g. "session".
created_at integer UTC Unix seconds. Used in the signature.
attempt_number integer Delivery attempt count, starting at 1.

Request headers

Header Value
Churnsolution-Signature Hex-encoded HMAC-SHA256 signature
Content-Type application/json

Event types

Churn Solution currently sends one event type.

session — Cancellation-flow session

Sent when a customer completes a cancellation-flow session. data.object is "session", and data.session holds the Session object described below. This is the primary business event.

The Session object

The data.session object describes a customer's path through a cancellation flow and how it ended: the final status, whether the customer was saved, and nested customer, subscription, plan, flow, coupon and step details.

{
  "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 Session identifier.
status enum string Final session status. See status values.
is_saved boolean true if the customer was retained, false if they churned.
created_at integer UTC Unix seconds.
customer object See Customer.
subscription object See Subscription.
plan object See Plan.
cancellation_flow object See Cancellation flow.
uncancel_coupon object See Coupon. null when not applicable.
steps array 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 Gateway customer identifier.
name string Customer name.
email string Customer email.
created_at integer UTC Unix seconds.

Subscription

Field Type Description
id string Gateway subscription identifier.
status enum string Subscription status. See values below.
start_date integer Subscription start.
current_period_start integer Current billing period start.
current_period_end integer Current billing period end.
trial_start integer null if no trial.
trial_end integer null if no trial.

Subscription 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.
recurring object Billing cadence, e.g. { "interval": "month", "interval_count": 1 }.
recurring.interval string day, week, month or year.
recurring.interval_count integer Number of intervals between billings.
currency string ISO code, e.g. usd.
unit_amount integer Amount in the smallest currency unit.
unit_amount_decimal number Amount in major units.
quantity integer Plan quantity.
plan_summary string Human-readable summary.

Cancellation flow

Field Type Description
id integer Flow identifier.
name string Flow name the customer went through, such as an A/B variant or audience.
created_at integer UTC Unix seconds.
updated_at integer UTC Unix seconds.

Session steps

session.steps is an ordered array. Each step has a type; fields that do not apply to a step's type are null.

{
  "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 enum FORM, SURVEY or OFFER.
order integer 1-based position in the flow.
contains_offer boolean Whether an offer was shown in this step.
survey_answer string SURVEY only. The selected option.
followup_question string SURVEY only. The follow-up question, if any.
followup_question_answer string SURVEY only. Free-text answer.
form_answer string FORM only. Free-text answer.
offer object Present when contains_offer is true. See Offer.

Offer

Exactly one of coupon, pause or trial_extension is populated.

Field Type Description
accepted boolean Whether the customer accepted the offer.
coupon object / null See Coupon.
pause object e.g. { "number_of_months": 2 }
trial_extension object e.g. { "number_of_days": 14 }

Coupon

Field Type Description
name string Coupon name.
code string Coupon code.
currency string ISO code, for fixed-amount coupons.
amount_off integer Smallest currency unit. null for percentage coupons.
amount_off_decimal number 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 Months, when duration is repeating.
number_of_billing_cycles integer Billing cycles, if defined.
currency_symbol string Display symbol, e.g. $.
summary string Human-readable discount summary.

Security

Verifying signatures

Each request carries a hex-encoded HMAC-SHA256 signature in the Churnsolution-Signature header, keyed with your endpoint's signing secret. Verify it with a constant-time comparison and reject mismatches with a 401.

How the signature is computed

signed_payload = "<created_at>;<raw_request_body>"
signature      = lowercase_hex( HMAC_SHA256( signing_secret, signed_payload ) )

Use the exact bytes you received as the raw body. Do not re-serialize the parsed JSON — key order or whitespace changes will break the signature.

Verification steps

  1. Read the raw request body

    Before any parsing.

  2. Extract created_at

    From the body.

  3. Compute the HMAC-SHA256

    Over created_at, a semicolon, and the raw body, then hex-encode the result.

  4. Compare

    Against the Churnsolution-Signature header using a constant-time comparison.

  5. Reject mismatches

    Respond with 401.

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
  }
);

Delivery & retries

  • Any 2xx or 3xx response counts as a successful delivery. 4xx / 5xx responses, connection failures and timeouts count as failures.
  • attempt_number tells you which delivery attempt this is.
  • The same event may be delivered more than once. De-duplicate using the top-level id.
  • Events may arrive out of order. Use created_at to sort them if ordering matters.

Best practices

  • Verify the signature before acting on any payload.
  • Respond quickly; process asynchronously.
  • Ignore unknown fields — new ones may be added over time.
  • Treat optional fields as possibly null or absent.
  • Store the signing secret in a secrets manager or environment variable, never in client-side code.
  • Use HTTPS.

Quick reference

Property Value
HTTP method POST
Body format JSON, snake_case keys
Signature header Churnsolution-Signature
Algorithm HMAC-SHA256, lowercase hex
Signed payload <created_at>;<raw_body>
Signing key Per-endpoint signing secret
Expected response 2xx
Timestamps UTC Unix seconds
Idempotency key Top-level id
Esc
↑↓ navigate↵ openEsc close