Skip to main content
All posts
Automation7 min read

How to Send Chargebee Cancellation Alerts to a Shared Team Phone

Authenticate one Chargebee cancellation webhook, retrieve the event again, discard billing details, and send fixed copy to a private PingRoom Room.

Two PingRoom avatars guide a sealed cancellation token through a wooden verification gate while Pingo watches beside an empty queue tray.

To send a shared phone alert when a Chargebee subscription is actually cancelled, protect one webhook with Basic Authentication, retrieve the event again from the configured Chargebee site, accept only subscription_cancelled, and queue fixed copy for a private PingRoom Room. Chargebee keeps the billing record. The Room receives only Chargebee subscription cancelled and Open Chargebee to review the cancellation.

This is a webhook recipe, not a native Chargebee integration. It does not forward a customer name, email address, subscription ID, invoice, cancellation reason, or account value to PingRoom.

Choose the smallest route that works

Chargebee already offers team and workflow destinations. Use them when they match the job.

NeedBetter starting point
The team already works from SlackChargebee's native Slack integration
A no-code workflow should route cancellation data into a business systemChargebee's Zapier connection
A small group needs one fixed, privacy-minimal phone signalThe authenticated relay and PingRoom workflow below
The event must assign work, update retention records, or decide outreachKeep that logic in Chargebee, a CRM, or an owned workflow system

The direct relay is useful when the alert should be deliberately sparse. It also addresses an important Chargebee boundary: current Chargebee webhooks support Basic Authentication, but do not provide HMAC signing. Basic Authentication checks the caller's credentials; it does not prove that the request body was unchanged. The relay therefore uses the incoming body only to find the event ID, then retrieves that event again from Chargebee before queueing anything.

What you need

  • One Chargebee site and permission to configure a webhook.
  • A Chargebee API key stored only on the relay server.
  • A separate strong username and password for Chargebee's webhook Basic Authentication.
  • A server-side HTTPS relay with durable unique storage and a queue.
  • A private PingRoom Room whose owner has PingRoom Pro. Incoming webhooks are Pro connections.
  • A controlled test subscription or Chargebee test event with invented data.

Keep the Chargebee API key, webhook password, replay secret, and PingRoom webhook URL separate. Do not put them in browser code, screenshots, analytics, logs, or a public repository.

Prepare the PingRoom destination

Open your Rooms in the PingRoom browser app, choose the destination Room, then open Settings → Connections → From another app.

  1. Add an incoming webhook and name it after the Chargebee site and cancellation event.
  2. Keep Quick Ping on Custom message.
  3. Save and test it once.
  4. Store the complete PingRoom webhook URL in the relay's protected server configuration.

The URL contains a secret. The relay is the only component that should know both the Chargebee credentials and the PingRoom destination. The incoming-webhook reference documents PingRoom's current request fields, limits, cooldown behavior, idempotency, and response shape.

Configure one authenticated Chargebee webhook

In Chargebee, open Settings → Configure Chargebee → Webhooks. Add the relay's public HTTPS URL, enable My webhook URL is protected by basic authentication, and enter the relay-specific username and password.

Chargebee sends events asynchronously. Its current documentation says a non-2XX response can trigger retries, and the same event may arrive more than once. Chargebee's event ID is the duplicate boundary. Store it durably before returning success; a process-local Map is not enough for production.

This recipe listens for the actual subscription_cancelled event. It deliberately ignores a subscription that is merely scheduled to cancel. If your team needs advance retention work, start with Chargebee's scheduled-cancellation notifications or Zapier trigger instead of relabeling a future state as a completed cancellation.

Authenticate, retrieve, then discard the details

The relay performs five checks in order:

  1. compare the Basic Authentication header in constant time;
  2. parse the body only after authentication;
  3. require subscription_cancelled and a bounded event ID;
  4. retrieve that event from the configured Chargebee site's v2 API;
  5. queue fixed PingRoom copy with an opaque replay key.

It never reads content.subscription, content.customer, content.invoice, or cancel_reason to build the Ping. This is the exact dependency-free core used by the controlled checks for this article:

import { createHmac, timingSafeEqual } from "node:crypto";

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const WEBHOOK_USERNAME = requiredEnv("CHARGEBEE_WEBHOOK_USERNAME");
const WEBHOOK_PASSWORD = requiredEnv("CHARGEBEE_WEBHOOK_PASSWORD");
const CHARGEBEE_SITE = requiredEnv("CHARGEBEE_SITE");
const CHARGEBEE_API_KEY = requiredEnv("CHARGEBEE_API_KEY");
const REPLAY_SECRET = requiredEnv("CHARGEBEE_REPLAY_SECRET");
const EXPECTED_API_VERSION = "v2";

if (!/^[a-z0-9-]+$/.test(CHARGEBEE_SITE)) {
  throw new Error("Invalid CHARGEBEE_SITE");
}

function sameText(received, expected) {
  const left = Buffer.from(received ?? "", "utf8");
  const right = Buffer.from(expected, "utf8");
  return left.length === right.length && timingSafeEqual(left, right);
}

function validBasicAuthorization(received) {
  const expected = `Basic ${Buffer.from(
    `${WEBHOOK_USERNAME}:${WEBHOOK_PASSWORD}`,
  ).toString("base64")}`;
  return sameText(received, expected);
}

export async function retrieveChargebeeEvent(eventId, fetchImpl = fetch) {
  const url = `https://${CHARGEBEE_SITE}.chargebee.com/api/v2/events/${encodeURIComponent(eventId)}`;
  const authorization = `Basic ${Buffer.from(`${CHARGEBEE_API_KEY}:`).toString("base64")}`;
  const response = await fetchImpl(url, {
    headers: {
      accept: "application/json",
      authorization,
    },
  });
  if (!response.ok) throw new Error(`Chargebee returned ${response.status}`);
  const payload = await response.json();
  return payload.event;
}

export async function receiveChargebee({
  headers,
  rawBody,
  retrieveEvent = retrieveChargebeeEvent,
  enqueueOnce,
}) {
  if (!validBasicAuthorization(headers.authorization)) {
    return { status: 401, outcome: "invalid_authorization" };
  }

  let incoming;
  try {
    incoming = JSON.parse(rawBody);
  } catch {
    return { status: 400, outcome: "invalid_json" };
  }

  if (incoming?.event_type !== "subscription_cancelled") {
    return { status: 200, outcome: "ignored" };
  }

  const eventId = incoming?.id;
  if (!/^[A-Za-z0-9_-]{1,40}$/.test(eventId ?? "")) {
    return { status: 400, outcome: "invalid_event_id" };
  }

  let authoritative;
  try {
    authoritative = await retrieveEvent(eventId);
  } catch {
    return { status: 503, outcome: "chargebee_unavailable" };
  }

  if (
    authoritative?.id !== eventId ||
    authoritative?.api_version !== EXPECTED_API_VERSION
  ) {
    return { status: 502, outcome: "invalid_authoritative_event" };
  }

  if (authoritative.event_type !== "subscription_cancelled") {
    return { status: 200, outcome: "ignored" };
  }

  const digest = createHmac("sha256", REPLAY_SECRET)
    .update(eventId)
    .digest("hex");
  const replayKey = `chargebee-cancel-${digest}`;
  const pingRoomBody = {
    action: null,
    title: "Chargebee subscription cancelled",
    message: "Open Chargebee to review the cancellation.",
    correlation_id: replayKey,
  };

  try {
    const inserted = await enqueueOnce(replayKey, {
      headers: {
        "content-type": "application/json",
        "idempotency-key": replayKey,
      },
      body: JSON.stringify(pingRoomBody),
    });
    return { status: 200, outcome: inserted ? "queued" : "replay" };
  } catch {
    return { status: 503, outcome: "queue_unavailable" };
  }
}

Wrap this core in your server framework without weakening three boundaries:

  1. retain only a durable unique record for the opaque replay key and the fixed queued request;
  2. return HTTP 200 only after the queue has stored the item or confirmed the same replay;
  3. let the queue consumer retry the identical PingRoom body with the identical Idempotency-Key.

Chargebee's API documentation says its webhook retries can continue for more than three days. Keep the duplicate record long enough to cover that retry window and any manual resend policy your team permits.

Retrieving the event again verifies the event identity and payload through Chargebee's authenticated API. It does not prove the subscription is still cancelled when a person reads the Ping. Chargebee notes that event snapshots can become stale and arrive out of order. Open Chargebee before taking billing, access, or customer-contact action.

Rehearse the whole boundary

Use a Chargebee test site or controlled test event with invented values:

  1. Send one subscription_cancelled event and confirm the relay authenticates before parsing.
  2. Confirm the relay retrieves the same event ID from the configured Chargebee site.
  3. Inspect the durable queue item. It should contain only the fixed title, fixed message, correlation ID, and idempotency header.
  4. Confirm the Room shows Chargebee subscription cancelled and Open Chargebee to review the cancellation.
  5. Replay the same event and confirm the queue reports a replay without adding another item.
  6. Change customer or subscription fields while keeping the same event ID. The Room should still receive no new Ping and no changed data.
  7. Send another event type, malformed JSON, invalid credentials, an invalid event ID, and a failed authoritative lookup. None may create a Ping.
  8. Disable the PingRoom webhook and confirm the consumer keeps its retry record instead of pretending delivery succeeded.

The controlled fixture for this article covers those boundaries with Chargebee-shaped requests, a mocked authoritative API lookup, a queue stand-in, and a local PingRoom contract mock. It uses no live Chargebee site, API key, subscription, customer data, PingRoom Room, phone, or delivery receipt.

Two PingRoom avatars separate one accepted cancellation token from an identical replay at a wooden verification gate while Pingo watches.

Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer, is watching the duplicate remain behind the gate. He has not joined the private Room, inspected a subscription, or chosen a retention response. The human team owns the billing workflow and every customer decision.

Read the signal correctly

A successful Ping means the configured Chargebee webhook passed the relay's authentication, the same event was retrieved from Chargebee, the fixed request was queued, and PingRoom accepted it. It does not prove:

  • why the subscription was cancelled;
  • that the current subscription state has not changed again;
  • that a customer should be contacted;
  • that access, invoices, or downstream systems were updated;
  • that somebody reviewed the cancellation;
  • or that every phone displayed a notification.

Chargebee remains authoritative for the event, subscription, customer, invoice, and later state. PingRoom records the short shared signal and sends it through its normal delivery path. Connectivity, operating-system policy, notification permissions, Focus settings, Room mutes, and device state can still affect what appears.

If that boundary fits your team, create or choose the private Room, add its incoming webhook, and rehearse one invented Chargebee cancellation before enabling the relay for production events.

Illustrations are AI-generated editorial scenes featuring Pingo the Pigeon and approved PingRoom avatars. The people and situation are fictional examples, not customers or product screens. The human team is responsible for product claims and decisions.

PingRoom

Short-form communication.

View all posts