Webhooks

To receive notifications when cancellation flow sessions are started or completed, ProsperStack can deliver webhooks to your application.

Adding a webhook

Navigate to the webhooks configuration page by clicking Settings in the left navigation, then Manage integrations. Click Configure in the Webhooks section.

Click the Create a webhook link to create a new webhook.

Create a webhook

Enter the URL of your application endpoint that ProsperStack should deliver webhooks to and select which events the endpoint should receive.

  • flow_session_started — Occurs when a cancellation flow is started
  • flow_session_completed — Occurs when a cancellation flow is completed

Webhook payloads

Flow session started

The flow_session_started webhook is delivered whenever a new cancellation session is started. The payload contains information about the canceling subscriber and which cancellation flow they've been routed to.

Example payload:

{
  "event": "flow_session_started",
  "event_id": "evt_1TwEZeOiaN9qTNHO2vuctd2j",
  "data": {
    "id": "sess_99aTIJuf30OWozAbvgu7Kje4",
    "status": "in_progress",
    "subscriber": {
      "id": "subr_dRiytWmkVtSBt9mOpRaC0ca0",
      "payment_provider_id": "cus_Jax42BBWGOWuDp",
      "name": "Jane Doe",
      "email": "jane@example.com",
      "status": "saved",
      "properties": [
        {
          "property": {
            "id": "prop_jrtpyZs1httwFFIl7V80cHwf",
            "key": "number_of_contacts",
            "name": "Number of contacts",
            "entity": "subscriber",
            "type": "number",
            "format": "number"
          },
          "value": 5800,
          "formatted_value": "5,800"
        }
      ],
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    },
    "subscription": {
      "id": "subn_dp79dRR5wIy1kGW4PCO41wit",
      "payment_provider_id": "sub_JE1trB8eUAnh0r",
      "subscriber_id": "subr_dRiytWmkVtSBt9mOpRaC0ca0",
      "mrr": "39.95",
      "status": "active",
      "properties": [
        {
          "property": {
            "id": "prop_S6A17y8S8wgKBBJbveQHpLW6",
            "key": "is_professional",
            "name": "Is professional",
            "entity": "subscription",
            "type": "boolean",
            "format": null
          },
          "value": true,
          "formatted_value": "True"
        }
      ],
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    },
    "flow": {
      "id": "flow_cMJ6T2tH56T2XngZdhBqpgtM",
      "name": "Default",
      "updated_at": "2021-11-04T15:22:13.449Z",
      "created_at": "2021-11-04T15:22:13.424Z"
    },
    "answers": [],
    "offers_presented": [],
    "offer_accepted": null,
    "cancel_reason": null,
    "created_at": "2021-11-04T15:41:58.238Z",
    "started_at": "2021-11-04T15:41:58.238Z",
    "updated_at": "2021-11-04T15:41:58.238Z",
    "completed_at": null
  }
}
Flow session completed

The flow_session.completed webhook is delivered whenever a cancellation session is completed. The payload contains the entire result of a flow session, including the status, survey answers and any offers presented or accepted.

Example payload:

{
  "event": "flow_session_completed",
  "event_id": "evt_ujO4n2g2QbWtGUVg1zJSbC5I",
  "data": {
    "id": "sess_Qxu1whWCpfYlX93hH3IIojdL",
    "status": "saved",
    "subscriber": {
      "id": "subr_dRiytWmkVtSBt9mOpRaC0ca0",
      "payment_provider_id": "cus_Jax42BBWGOWuDp",
      "name": "Jane Doe",
      "email": "jane@example.com",
      "status": "saved",
      "properties": [
        {
          "property": {
            "id": "prop_jrtpyZs1httwFFIl7V80cHwf",
            "key": "number_of_contacts",
            "name": "Number of contacts",
            "entity": "subscriber",
            "type": "number",
            "format": "number"
          },
          "value": 5800,
          "formatted_value": "5,800"
        }
      ],
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    },
    "subscription": {
      "id": "subn_dp79dRR5wIy1kGW4PCO41wit",
      "payment_provider_id": "sub_JE1trB8eUAnh0r",
      "subscriber_id": "subr_dRiytWmkVtSBt9mOpRaC0ca0",
      "mrr": "39.95",
      "status": "active",
      "properties": [
        {
          "property": {
            "id": "prop_S6A17y8S8wgKBBJbveQHpLW6",
            "key": "is_professional",
            "name": "Is professional",
            "entity": "subscription",
            "type": "boolean",
            "format": null
          },
          "value": true,
          "formatted_value": "True"
        }
      ],
      "created_at": "2019-08-24T14:15:22Z",
      "updated_at": "2019-08-24T14:15:22Z"
    },
    "flow": {
      "id": "flow_cMJ6T2tH56T2XngZdhBqpgtM",
      "name": "Default",
      "updated_at": "2021-11-04T15:22:13.449Z",
      "created_at": "2021-11-04T15:22:13.424Z"
    },
    "answers": [
      {
        "question": {
          "id": "ques_e9PvoXmNB1WYYeNQOr6uRq1l",
          "type": "multiple_choice",
          "text": "What is your primary reason for leaving?"
        },
        "value": [
          {
            "id": "qopt_wrT7Rl362RVNi6eE2A4J8DLH",
            "text": "Too expensive"
          }
        ],
        "sentiment": null
      },
      {
        "question": {
          "id": "ques_acYB4paiWRGO5eFYJOAQy7XU",
          "type": "text",
          "text": "How can we improve?"
        },
        "value": "Really love the product, just can't afford it right now!",
        "sentiment": "positive"
      }
    ],
    "offers_presented": [
      {
        "id": "offr_FBMw5k51DYga0K2WswbiFfMU",
        "type": "coupon",
        "name": "40% off for three months",
        "details": {
          "type": "coupon",
          "coupon_type": "percentage",
          "amount_off": "40",
          "duration": "repeating",
          "months": 3,
          "apply_to": "subscription",
          "payment_provider_coupon_id": "40OFF3MONTHS"
        },
        "metadata": {
          "offer_code": "3590757"
        },
        "created_at": "2021-07-08T14:25:12.211Z",
        "updated_at": "2021-07-08T14:25:12.211Z"
      }
    ],
    "offer_accepted": {
      "id": "offr_FBMw5k51DYga0K2WswbiFfMU",
      "type": "coupon",
      "name": "40% off for three months",
      "details": {
        "type": "coupon",
        "coupon_type": "percentage",
        "amount_off": "40",
        "duration": "repeating",
        "months": 3,
        "apply_to": "subscription",
        "payment_provider_coupon_id": "40OFF3MONTHS"
      },
      "metadata": {
        "offer_code": "3590757"
      },
      "created_at": "2021-07-08T14:25:12.211Z",
      "updated_at": "2021-07-08T14:25:12.211Z"
    },
    "cancel_reason": {
      "text": "Too expensive",
      "reason_code": "too_expensive"
    },
    "created_at": "2022-08-19T16:43:23.014092Z",
    "started_at": "2022-08-19T16:43:25.103221Z",
    "updated_at": "2022-08-19T16:45:56.773241Z",
    "completed_at": "2022-08-19T16:45:56.773241Z"
  }
}

Webhook retries

If delivering a webhook fails because your endpoint does not respond or returns an error, ProsperStack will attempt to retry the webhook for up to three days. If delivery continues to fail after three days, the webhook will be disabled and you'll receive an email to let you know that there was a problem completing the webhook delivery.

You can re-enable a disabled webhook from the webhook integrations page once any issues with the endpoint have been resolved.

Webhook limits

You can have up to ten webhooks configured with your ProsperStack account. If you need more, get in touch with us at support@prosperstack.com and we'll be happy to help.

Verifying webhooks

To verify that a webhook request came from ProsperStack, ProsperStack includes a signature in each webhook request's ProsperStack-Signature header.

The ProsperStack-Signature header contains a timestamp and a signature. You can extract these values to verify that the webhook request originated from ProsperStack.

t=1660874139,s=05ba90dc69f562b66a79dc28f40cacff6210388c804ece5094c80c4d8a89af88

Verifying the signature

1. Extract the timestamp and signature from the header

Split the header using the , character as the seprator to a get a list of elements. Then split each element using the = character as the separator to get a prefix and value pair.

The value the the t prefix corresponds to the timestamp and the s prefix corresponds to the signature.

2. Prepare the signature payload string

Create the signature payload string by concatenating:

  • The timestamp (as a string)
  • The . (dot) character
  • The request body (i.e. the JSON-stringified request payload)
3. Compute the expected signature

Compute an HMAC with the SHA256 hash function using the prepared signature payload string from the previous step as the message and your ProsperStack client secret as the key.

Your client secret can be found in the Settings page of your ProsperStack dashboard under the Account section.

4. Compare the signatures

Compare the signature value from the ProsperStack-Signature header and your computed signature from the previous step to make sure they match. To protect against timing attacks, make sure to use a constant-time string comparison function when comparing the signature values.

To prevent replay attacks, compare the timestamp from the ProsperStack-Signature header and the current timestamp to make sure the difference is within your tolerance.

Verification example

Verifying the webhook signature will look different depending on your server language, but the following is an example of what it might look like in Node.js:

import crypto from "crypto";
import { differenceInSeconds } from "date-fns";

const SECRET = "my client secret";
const TOLERANCE_SECONDS = 300;

const body = req.body;
const signatureHeader = req.headers["prosperstack-signature"];

const signatureValues = signatureHeader
  .split(",")
  .map((part) => part.split("="))
  .reduce(
    (acc, [key, value]) => ({
      ...acc,
      [key]: value,
    }),
    {}
  );
const { t: timestamp, s: signature } = signatureValues;

if (
  differenceInSeconds(new Date(), new Date(Number(timestamp) * 1000)) >
  TOLERANCE_SECONDS
) {
  throw new Error("Timestamp is out of tolerance!");
}

const computedSignature = crypto
  .createHmac("sha256", SECRET)
  .update(timestamp + "." + body)
  .digest("hex");

if (
  computedSignature.length !== signature.length ||
  !crypto.timingSafeEqual(
    Buffer.from(computedSignature),
    Buffer.from(signature)
  )
) {
  throw new Error("Signatures do not match!");
}

Don't let customers slip away.