message.comDevelopers

Sync Stripe customers to message.com

Keep plan, MRR, and trial status fresh on every visitor record. When a Stripe subscription changes, push the new state to message.com. Agents see who is on what plan; AI knows which visitors are paying customers and which are not.

1. Build the webhook handler

The handler verifies the Stripe signature using the official SDK, branches on event type, and upserts the visitor in message.com.

server.ts
// Node / Express handler for Stripe webhooks
import express from "express";
import Stripe from "stripe";

const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const sig = req.get("stripe-signature")!;
    let evt: Stripe.Event;
    try {
      evt = stripe.webhooks.constructEvent(
        req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!
      );
    } catch {
      return res.status(401).end();
    }

    if (
      evt.type === "customer.subscription.created" ||
      evt.type === "customer.subscription.updated" ||
      evt.type === "customer.subscription.deleted"
    ) {
      const sub = evt.data.object as Stripe.Subscription;
      const customer = await stripe.customers.retrieve(sub.customer as string);
      if (customer.deleted) return res.status(200).end();

      await fetch("https://app.message.com/api/v1/visitors/upsert", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.MESSAGE_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": evt.id,
        },
        body: JSON.stringify({
          email: customer.email,
          externalId: `stripe:${customer.id}`,
          attributes: {
            plan: sub.items.data[0].price.nickname,
            mrr: (sub.items.data[0].price.unit_amount ?? 0) / 100,
            status: sub.status, // active | trialing | past_due | canceled
            currentPeriodEnd: sub.current_period_end,
          },
        }),
      });
    }

    res.status(200).end();
  }
);

Stripe verifies on raw bytes. The Express raw middleware must run before any JSON parser for this route, otherwise constructEvent rejects every request.

2. Register the endpoint in Stripe

  1. Open the Stripe dashboard.
  2. Go to Developers → Webhooks.
  3. Click Add endpoint.
  4. Endpoint URL: your HTTPS handler.
  5. Events to send: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted. Add customer.created and customer.updated if you want non-subscription customers too.
  6. Copy the Signing secret into STRIPE_WEBHOOK_SECRET.

3. Test with the Stripe CLI

bash
# Set up a tunnel to test locally
stripe listen --forward-to localhost:3000/webhooks/stripe

# Fire a test event
stripe trigger customer.subscription.updated

See the Test webhooks locally tutorial for the ngrok alternative.

4. Verify

  1. Create a real test subscription for a customer whose email matches a known visitor in message.com.
  2. Open the visitor in app.message.com. Confirm plan, mrr, and status appear in the attributes panel.
  3. Cancel the subscription. Confirm status flips to canceled.

Useful events to listen for

  • invoice.payment_failed. flag the visitor as paymentIssue: true. Agents see it immediately.
  • customer.subscription.trial_will_end. surface a trial-ending banner in the inbox.
  • checkout.session.completed. tie a chat session to a paid conversion (see Track chat-to-paid conversion).

Common pitfalls

  • Missing Idempotency-Key. Stripe retries on 5xx and timeouts. Use evt.id as the idempotency key on the message.com upsert.
  • Customer email missing. Stripe customers can exist without an email. Upsert by external ID (stripe:cus_xxx) and let the visitor record fill in email later.
  • Multiple subscriptions. If a customer has more than one subscription, decide which one to surface as plan. Often the highest MRR.
  • Cross-environment confusion. Stripe test events and live events use different signing secrets. Configure both, route to different attribute sets, or skip test events in production.

Next steps