# How to Send a Mailchimp Campaign-Sent Alert to a Shared Room

> Verify one Mailchimp campaign-finished webhook, discard campaign and audience data, and queue fixed, replay-safe copy for a shared PingRoom Room.

- Published: 2026-11-04
- Author: PingRoom
- Category: Automation
- Topics: Mailchimp campaign sent notification, Mailchimp webhook push notification, shared marketing team phone alert, webhook push notifications
- Canonical: https://pingroom.io/blog/mailchimp-campaign-sent-push-notifications
- Cover image: https://pingroom.io/blog/mailchimp-campaign-sent-push-notifications-cover.webp
- Cover description: Two PingRoom avatars move an unlabelled campaign folio from a miniature sending press to a finished tray while Pingo watches beside an hourglass.

**Use Mailchimp's own mobile push when the people who need the update already use the Mailchimp account. When a separate, stable group only needs to know that one campaign finished sending, connect Mailchimp's signed campaign webhook to a small relay and send fixed copy to a private PingRoom Room.**

The workflow in this guide sends **Mailchimp campaign finished — Check Mailchimp for the campaign record.** It does not forward the subject, audience, recipients, contact fields, report data, send counts, or campaign ID.

Mailchimp remains the source of truth. A campaign-finished event does not prove that every recipient received, opened, read, or acted on the email. This is a webhook recipe you configure, not a native Mailchimp integration.

## Start with Mailchimp's own notification

Mailchimp's [mobile push settings](https://mailchimp.com/help/about-push-notifications/) include a **Sent Campaign** notification. That is the smaller choice when the intended recipients are Mailchimp account users.

| Need                                                            | Better starting point                    |
| --------------------------------------------------------------- | ---------------------------------------- |
| An account user needs the normal campaign update                | Mailchimp mobile push                    |
| A stable, opt-in group needs one shared short signal            | Signed Mailchimp webhook → PingRoom Room |
| Someone needs the subject, audience, report, or delivery detail | Open the campaign in Mailchimp           |
| The team needs approvals or a marketing workflow                | Keep the established marketing system    |

Do not create a second channel merely because a webhook exists. Add the Room only when the shared audience and next action are clear.

## Prepare one audience webhook and one private Room

Mailchimp's current [webhook setup guide](https://mailchimp.com/help/about-webhooks/) says webhooks require a Standard or Premium plan. Create an audience webhook, choose **Email campaign sending**, and enable only the source types your team actually uses. The callback needs a public HTTPS URL with a trusted certificate.

When Mailchimp shows the signing secret, save it immediately in server-side secret storage. Mailchimp shows it once. Do not put it in source control, a browser bundle, logs, or the PingRoom request.

In PingRoom, create a private Room for the people who need the shared signal. The Room owner needs **PingRoom Pro** for incoming webhooks. Open **Settings → Connections → From another app**, add an incoming webhook, leave **Quick Ping** on **Custom message**, and store its complete URL as `PINGROOM_WEBHOOK_URL` in the relay.

The [incoming-webhook walkthrough](/blog/webhook-to-push-notification) covers the PingRoom setup. The [webhook reference](/documents/webhooks) carries the current fields, limits, cooldown, idempotency behavior, and responses.

## Verify the raw request before reading it

Mailchimp sends audience webhook bodies as `application/x-www-form-urlencoded`. Its [developer webhook guide](https://mailchimp.com/developer/marketing/api-concepts/webhooks) documents signed deliveries in `X-Mailchimp-Signature`:

1. Read the exact request bytes without decoding the form.
2. Parse the header's timestamp and `v1` signature.
3. Reject timestamps more than five minutes from the server clock.
4. Compute HMAC-SHA256 over `timestamp + "." + rawBody` with the saved signing secret.
5. Compare the expected and supplied signatures with a timing-safe function.
6. Only then decode the form and accept `type=campaign`.

Do not place a body parser in front of signature verification. Re-encoding form fields can change the bytes and make a valid signature impossible to reproduce.

## Queue only a digest, then send fixed copy

This compact Node example leaves HTTP framework plumbing to your deployment. `acceptMailchimpCampaign` receives the untouched body and header, verifies them, and inserts one opaque key into a durable idempotent queue.

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

const MAX_BODY_BYTES = 256_000;
const MAX_AGE_SECONDS = 300;

function parseSignature(header) {
  const fields = new Map(
    String(header)
      .split(",")
      .map((part) => part.trim().split("=", 2)),
  );
  const timestamp = fields.get("t");
  const signature = fields.get("v1");
  if (
    !/^\d{10}$/.test(timestamp ?? "") ||
    !/^[a-f0-9]{64}$/.test(signature ?? "")
  ) {
    throw new Error("Invalid Mailchimp signature header");
  }
  return { timestamp, signature };
}

function verifyMailchimp(rawBody, header, secret, nowSeconds) {
  const { timestamp, signature } = parseSignature(header);
  if (Math.abs(nowSeconds - Number(timestamp)) > MAX_AGE_SECONDS) {
    throw new Error("Expired Mailchimp signature");
  }
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  const supplied = Buffer.from(signature, "hex");
  if (
    expected.length !== supplied.length ||
    !timingSafeEqual(expected, supplied)
  ) {
    throw new Error("Invalid Mailchimp signature");
  }
}

export async function acceptMailchimpCampaign({
  rawBody,
  signature,
  secret,
  nowSeconds,
  queue,
}) {
  if (
    !Buffer.isBuffer(rawBody) ||
    rawBody.length === 0 ||
    rawBody.length > MAX_BODY_BYTES
  ) {
    throw new Error("Invalid Mailchimp body");
  }
  verifyMailchimp(rawBody, signature, secret, nowSeconds);

  const form = new URLSearchParams(rawBody.toString("utf8"));
  if (form.get("type") !== "campaign") return { ignored: true };

  const digest = createHash("sha256").update(rawBody).digest("hex");
  const eventKey = `mailchimp-campaign:${digest}`;
  await queue.insertOnce({ eventKey });
  return { accepted: true, eventKey };
}
```

The queue row contains no decoded Mailchimp values. A worker later uses `eventKey` as the PingRoom `Idempotency-Key` and sends one constant body:

```json
{
  "action": null,
  "title": "Mailchimp campaign finished",
  "message": "Check Mailchimp for the campaign record."
}
```

Hashing the raw body is not encryption and does not make it safe to retain the source payload. The privacy boundary is that the relay does not log or enqueue that payload and forwards only fixed copy.

The digest handles an exact retry with the same body. It does not establish a universal campaign identity across changed deliveries, and this guide does not claim exactly-once delivery. If your observed Mailchimp payload and contract provide a stable campaign identifier you are prepared to retain, document that field and migration separately before changing the key.

![Two PingRoom avatars test one raw event token at a brass signature gate while an exact duplicate waits outside and Pingo watches beside a closed credential box.](/blog/mailchimp-campaign-sent-push-notifications-1.webp)

_These AI-generated editorial illustrations feature approved PingRoom avatars and Pingo the Pigeon, PingRoom's fictional Chief Marketing Officer. They are not Mailchimp or PingRoom screenshots and do not depict customers._

Pingo is supervising the duplicate tray with the patience of someone who has already received the same announcement twice. He cannot read a campaign, enter a private Room, or send a Ping. People choose the webhook, recipients, fixed copy, and operating policy.

## Return quickly and keep failures visible

Mailchimp's developer guide says a callback that is unavailable or takes more than ten seconds can be retried, with attempts spread over 75 minutes. Insert into a durable queue before returning success. Do not wait for PingRoom or a phone to display the alert inside the Mailchimp request.

If queue insertion fails, return a non-success response so the failure remains visible to Mailchimp. If the worker cannot reach PingRoom, retry the same fixed body with the same `eventKey`. Never turn a downstream error into a claim that the campaign failed.

An HTTP success from PingRoom means the Ping was accepted. Connectivity, Room mutes, notification permissions, Focus settings, and operating-system policy still affect what appears on each phone.

## Rehearse six boundaries

Before connecting a real audience, use harmless fixtures and verify:

1. A correctly signed `campaign` form queues one opaque key.
2. The exact same signed body queues no second item.
3. A wrong signature and a timestamp outside five minutes are rejected.
4. A signed non-campaign event is acknowledged as ignored.
5. Logs and the PingRoom request contain none of the form's campaign, audience, subject, contact, or report values.
6. A PingRoom failure keeps the queue item retryable with the same key and fixed body.

For this article, we ran those boundaries with signed Mailchimp-shaped form bodies, an in-memory idempotent queue, and a local PingRoom contract mock. No live Mailchimp account, campaign, public relay, PingRoom Room, phone, or customer data was used.

When the rehearsal passes, [open PingRoom](/app/rooms), create the private destination Room, and enable only the Mailchimp campaign event that deserves shared attention. Keep campaign review and evidence in Mailchimp.

_Mailchimp is a trademark of its owner. PingRoom is not affiliated with or endorsed by Mailchimp._
