Webhooks enable Chargebee Retention to call a script on your server when an event has occurred in the Chargebee Retention cancel experience. Webhook events can be used to trigger an action in your billing system or activate another internal workflow. This article contains the following sections:
Chargebee Retention's webhook events will not redirect if your endpoint returns a 3xx HTTP redirect code. To ensure your endpoint's URL is correct you should perform the following test in your terminal. curl -v [https://example.com/endpoint](https://example.com/endpoint) -X POST
e.g. https://example.com/endpoint redirecting to https://www.example.com/endpoint would be recorded as a failure. Chargebee Retention events would need to be pointed to https://www.example.com/endpoint
in this example.
Chargebee Retention events are sent as POST requests to any static endpoint. Events can be distributed to multiple endpoints or directed to a single URL.
In order to use our webhook notification system, you will need to create a subscription in your Chargebee Retention dashboard. Head over to Settings > Alerts & Webhooks, scroll to the bottom and click Add. You will be prompted with a menu item that allows you to enter the URL and choose your triggers.
Chargebee Retention will provide a shared secret that will be used to sign all events prior to transmission using the HMAC sha1 algorithm. The signature is generated automatically at the time the subscription is created. The signature will be placed in the request header: "X-Hub-Signature" and should be used to verify whether or not the event payload is authentic.
You'll be able to view the secret by viewing the webhook details by clicking on the pencil icon.
For reference, our webhooks are signed using the following code:
public static String signHmacSha1(String message, String key) {
try {
// Get an hmac_sha1 key from the raw key bytes
byte[] keyBytes = key.getBytes(StandardCharsets.UTF-8);
SecretKeySpec signingkey = new SecretKeySpec(keyBytes, "HmacSHA1");
// Get an hmac_sha1 Mac instance and initialize with the signing key
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(signingkey);
// Compute the hmac on input data bytes
byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF-8));
// Convert raw bytes to Hex
byte[] hexBytes = new Hex().encode(rawHmac);
// Convert array of Hex bytes to a String
return new String(hexBytes, Standard Charsets.UTF_8);
} catch (Exception e) {
throw new IllegalStateException("Unable to sign message", e);
}
}
There are twelve event types that can be individually enabled to trigger a POST request for any Chargebee Retention app. Check out our offer creation help doc for a review of offer types and actions.
Trigger | Description |
Page loaded | Any visit to a cancel experience including page refreshes. |
Modal Opened | Viewed a modal offer |
Deflect | Left the page |
Cancel | Clicked cancel |
Send email | Accepted an offer on the send message modal |
Link | Left the page via a URL |
Intercom chat | Initiated an intercom chat |
Accepted Offer | Accepted an offer |
Save | Previously deflected user did not cancel (30 days by default) |
Nevermind | Clicked nevermind |
Accepted LA Offer | Accepted a Loss Aversion Offer |
Accepted Modal Offer | Accepted a Modal Offer |
"url": "https://www.example.com/endpoint",
"delivery_attempts": 1,
"created_at": "1970-01-01T00:00:00Z",
"subscription_id": "1234567890",
"first_sent_at": "1970-01-01T00:00:00Z",
"id": "a7467d71-92c4-4a2d-92bb-ec315bbbb082",
"type": "event"
Nested within the data object is the type of event that was triggered and several useful parameters as well as the survey, offer, browser context and fields from the JS snippet.
"data": {
"type": "link",
"id": "2955d62b-3b2a-46ce-ad83-204c9b99d689",
"app_id": "1234567890",
"session_id": "abcde12345",
"name": "10_off.sixty_forty_column.217648e7",
"timestamp": "1970-01-01T00:00:00Z",
"survey": {...
},
"offer": {...
},
"fields": {...
},
"context": {...
},
},
The survey key holds answers to survey questions if they were selected at the time of accepting an offer or during cancel.
"survey": {
"reason_for_leaving": [
{
"name": "reason_for_leaving",
"value": "customer_service_was_unsatisfactory.1928377447",
"tier": "tier0", //legacy
"lives_on": "tier0" //legacy
}
],
"competition": "None",
"sentiment": 10,
"feedback": "Chargebee Retention is great!",
"confirmation": true,
"selected_reason": "customer_service_was_unsatisfactory.1928377447",
//unique reason key
"display_reason": "Customer service was unsatisfactory",
//reason shown to canceller
"chargebee_retention_reason": "bad_customer_service"
//Chargebee Retention internal reason key
},
// additionally a custom reason key can be added to align with your internal tracking
The offer key holds information relative to the modal that was displayed if the CTA in an offer is clicked.
"offer": {
"name": "10_off.1731427124",
//unique name of the offer established when the offer was created
"display_name": "$10 Off",
//mutable name of the offer in the experience manager
"type": "$",
//offer type chosen during offer creation
"category": "Discounts"
//category of offer chosen during offer creation
},
The context key contains the browser context information from the client.
"context": {
"ip": "1.1.1.1",
"locale": "en-US",
"timezone": "America/Los_Angeles",
"user_agent": "Mozilla/5.0 ...",
"url": "https://cancel.example.com/example/cancel/abcde12345",
},
The fields key contains fields passed through the snippet including custom as well as standard fields that have been mapped in.
The following code block shows a small sample of a typical payload nested in the fields object.
"fields": {
"standard.Owner Email": "jane@chargebee.com",
"standard.Owner First Name": "Jane",
"standard.Owner Last Name": "Brighteyes",
//Chargebee Retention standard fields if populated and mapped.
"cancel.save_return_url": "https://www.example.com/return",
"cancel.app_id": "1234567890",
"cancel.account.created_at": "1970-01-01T00:00:00Z",
"cancel.account.plan": "Premium",
"cancel.account.plan_term": "monthly",
"cancel.account.internal_id": "abcd123",
//original payload from the JS snippet
"cancel.custom.activity": "none",
"cancel.custom.emails": "500",
"cancel.custom.offer_eligible": true,
//custom fields from the JS snippet
"cancel.context.locale": "en-US",
"cancel.context.timezone": "America/Los_Angeles",
"cancel.context.user_agent": "Mozilla/5.0 ...",
//browser context for cancel session
}
By default we consider responses in the 2xx's to be a success.
3xx, We do not redirect but we will retry.
4xx, 5xx (not including 410, and 429): Are treated as failed and will be retried.
We try the endpoint twice on network failures or 4xx or 5xx (excluding 410). After the second failure, we begin health-checks with OPTIONS on the endpoint looking for a successful (2xx) response with HTTP. We continue doing these health checks with exponential backoff for up to 24 hours. When we receive a successful response to a health check, we will retry the original post. This cycle continues for up to 24 hours total.
410/Gone: We will delete your webhook and no longer retry.
429/Throttling: Future placeholder for throttling (not implemented yet)
In the event you receive an event and do not wish to process it, we ask that you respond with a 2xx response, so as not to trigger our retry policy.
If you are using the DRF (Django Rest Framework) to parse Chargebee Retention webhooks, please be aware that DRF will use a JSON parser to parse the webhook. This will modifies the webhook payload in various ways and if you try to generate a signature based on the output of the JSON parser DRF uses the signature will not match the signature Chargebee Retention sends because the payload used to generate the signature is no longer the same as the payload Chargebee Retention sends.
Please refer to: https://www.django-rest-framework.org/api-guide/parsers/
Normally the parser is determined based on the content type, so it would normally use JSONParser, but this link describes how you can overwrite the parser that is used to parse the request. "You can also set the parsers used for an individual view, or viewset, using the APIView class-based views."
If you set it to use FileUploadParser then DRF should keep the content that Chargebee Retention sends intact. You would end up with a dictionary with a single 'file' key and the value would be the raw content Chargebee Retention sends.
See the "Basic usage example:" code snippet
These lines in particular are relevant:
parser_classes = [FileUploadParser]
file_obj = request.data['file']
this file_obj
should contain exactly the content Chargebee Retention sends and DRF should not have reformatted it. file_obj
is what you will want to use to compute the signature for verification purposes.
Note that if needed, the text can be converted back to Json using json.loads(request.data['file'])