Webhooks
Webhooks enable Churnkey to push real-time event notifications to your application. Once you’ve registered an endpoint URL, Churnkey will send a secure HTTPS POST request to that URL every time one of your customers completes a Churnkey session. The JSON payload will include session information like their survey response, any freeform feedback they left, and the outcome of the session e.g. subscription pause, cancellation, discount, or if they abandoned the cancellation flow.
Webhooks can be managed from your Churnkey account
Use Cases
Clients use Churnkey webhooks to:
- Update customer profile information in CRMs such as Hubspot or Intercom
- Example: marking every customer who cited “technical difficulties” in their survey response for a tutorial email campaign
- Create a customized Slack thread where customer success teams can discuss next steps for customer retention
- Send “secret plan” offers to customers who already declined offers on Churnkey Please note that for business logic related directly to customer billing and subscriptions, we recommend you use Stripe webhooks. This accounts for the possibility that subscriptions may be modified outside of the Churnkey flow.
Webhook Event Data
As soon as a webhook is enabled, Churnkey will start to stream new event notifications to that URL. These will be HTTPS POST requests with a JSON payload of the below format:
type SegmentFilter = {
attribute: string;
operand: string;
value: string[];
type: string;
}
type Segment = {
name: string;
filter: SegmentFilter[];
}
type ABTest = {
name: string;
id: string;
}
type DiscountConfig = {
couponId: string;
customDuration: string;
}
type PauseConfig = {
maxPauseLength: number;
pauseInterval: string;
datePicker: boolean;
}
type TrialExtensionConfig = {
trialExtensionDays: number;
}
type PlanChangeConfig = {
options: string[];
}
type RedirectConfig = {
redirectUrl: string;
redirectLabel: string;
}
type PresentedOffer = {
guid: string;
offerType: 'DISCOUNT' | 'PAUSE' | 'PLAN_CHANGE' | 'CONTACT' | 'TRIAL_EXTENSION' | 'REDIRECT';
discountConfig?: DiscountConfig;
pauseConfig?: PauseConfig;
trialExtensionConfig?: TrialExtensionConfig;
planChangeConfig?: PlanChangeConfig;
redirectConfig?: RedirectConfig;
}
type AcceptedOffer = {
guid: string;
offerType: 'discount' | 'pause' | 'plan change' | 'contact' | 'trial extension' | 'redirect';
pauseEndDate?: Date;
pauseInterval?: 'MONTH' | 'WEEK';
pauseDuration?: number;
pauseEndDate?: Date; // if specific date chosen
newPlanId?: string;
newPlanPrice?: number;
redirectUrl?: string;
couponId?: string;
couponType?: 'PERCENT' | 'AMOUNT';
couponAmount?: number;
couponDuration?: number;
trialExtensionDays?: number;
}
type Session = {
result: 'abort' | 'cancel' | 'pause' | 'discount' | 'plan_change' | 'contact' | 'trial_extension' | 'redirect';
presentedOffers: PresentedOffer[];
acceptedOffer?: AcceptedOffer;
mode: 'TEST' | 'LIVE';
segment?: Segment;
abTest?: ABTest;
subscriptionId: string;
feedback?: string; // freeform feedback
surveyResponse?: string;
followupQuestion?: string; // from survey selection
followupResponse?: string; // from survey selection
}
type SessionData = {
session: Session;
// customer from payment provider with expanded subscription(s)
customer: StripeCustomer | ChargebeeCustomer | ...
}
export type SessionWebhookPayload = {
event: 'session' | 'dunning';
data: SessionData;
}
Example Session Webhook Payload
{
"event": "session",
"data": {
"customer": <UP TO DATE CUSTOMER>,
"session": {
"result": "pause", // pause, discount, cancel, abort
"feedback": "Freeform feedback from customer",
"surveyResponse": "Too Expensive",
"mode": "LIVE", // billing mode, LIVE or TEST
"followupQuestion": "From survey question",
"followupResponse": "Customer response to survey follow-up question",
"acceptedOffer": {
// details below
"offerType": "PAUSE",
"pauseDuration": 2,
},
// Segment information will be sent if the customer belonged to a segment defined in Churnkey
"segment": {
"name": "My annual customers",
"filter": [{
"attribute": "BILLING_INTERVAL",
"operand": "INCLUDES",
"value": ["YEAR"]
}]
},
"abTest": {
"name": "A/B test for discount percentage",
"id": "Unique ID for AB Test"
}
}
}
}
If a customer accepts an offer through Churnkey, the data.session
object will include an acceptedOffer
property with information about the offer they accepted. Depending on the offer type, this object will look different.
Webhook Signature Verification
For enhanced security and data integrity, every webhook sent by our service is accompanied by a signature. This allows you to verify that the webhook was genuinely sent from our servers.
How We Sign Outgoing Webhooks
- Payload Stringification: We first convert the webhook payload to a string by using JSON stringify.
- HMAC Computation: Using the SHA256 algorithm, an HMAC (Hash-based Message Authentication Code) is computed on the stringified payload. The secret key for this HMAC computation is the
webhookSecret
associated with your organization. - Header Attachment: The resulting hash from the HMAC computation is then attached to the outgoing webhook as a header named
ck-signature
.
How to Verify the Webhook Signature
To verify the webhook signature on your end:
- Retrieve the Secret: Get your
webhookSecret
from our service. This is your org secret key which can be found on Churnkey | Settings | Account. - Capture the Payload and Signature: When you receive a webhook, capture the payload (body) and the value of the
ck-signature
header. - Compute the HMAC: Use the SHA256 algorithm to compute an HMAC on the payload, using your
webhookSecret
as the secret key. - Match the Signatures: Compare the computed HMAC value (from the previous step) with the
ck-signature
value you captured. If the two match, then the webhook is verified and was genuinely sent by our service.
Node.js Code Example
const crypto = require('crypto');
// find your account secret under Cancel Flow API Key: https://app.churnkey.co/settings/account
function verifyWebhookSignature(payload, receivedSignature, secret) {
// Compute the HMAC
const computedHmac = crypto.createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex');
// Compare the computed HMAC with the received signature
return computedHmac === receivedSignature;
}
// Usage:
const payload = req.body; // assuming you are using a body-parser middleware in Express.js
const receivedSignature = req.headers['ck-signature'];
const secret = 'YOUR_WEBHOOK_SECRET'; // Retrieve this from our service or your configuration
if (verifyWebhookSignature(payload, receivedSignature, secret)) {
console.log('Webhook signature is valid!');
} else {
console.log('Webhook signature is invalid or has been tampered with.');
}
For more code examples, please check the server side HMAC signing on Installing Churnkey
Pause offer accepted
{
"offerType": "PAUSE",
"pauseDuration": 2 // pause duration in months
}
Discount offer accepted
{
"offerType": "DISCOUNT",
"couponId": "my_coupon_id", // coupon Id in Stripe/Chargebee, discount ID in Braintree
"couponType": "PERCENT", // PERCENT or AMOUNT
"couponAmount": 30,
"couponDuration": 2 // coupon duration in months - null if forever coupon
}
Dunning Webhook Data
If you have webhooks enabled and use dunning, Churnkey will stream email event notifications to your webhook URL. There are multiple events for each email based on the recipient’s actions. Potential action
values are DELIVERY
, BOUNCE
, OPEN
, CLICK
.
{
"event": "dunning",
"data": {
"email": {
"emailTo": "[email protected]",
"subject": "Need help with your subscription?",
"from": "[email protected]",
"action": "DELIVERY", // DELIVERY, BOUNCE, OPEN, or CLICK
"emailCount": 2, // This emails order in the campaign
"emailsRemaining": 3 // Emails remaining in campaign
},
"customer": "cus_XXXXXXXXXXXX",
"invoice": "inv_XXXXXXXXXXXX"
}
}