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
- Open the Stripe dashboard.
- Go to Developers → Webhooks.
- Click Add endpoint.
- Endpoint URL: your HTTPS handler.
- Events to send:
customer.subscription.created,customer.subscription.updated,customer.subscription.deleted. Addcustomer.createdandcustomer.updatedif you want non-subscription customers too. - 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.updatedSee the Test webhooks locally tutorial for the ngrok alternative.
4. Verify
- Create a real test subscription for a customer whose email matches a known visitor in message.com.
- Open the visitor in
app.message.com. Confirmplan,mrr, andstatusappear in the attributes panel. - Cancel the subscription. Confirm
statusflips tocanceled.
Useful events to listen for
invoice.payment_failed. flag the visitor aspaymentIssue: 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.idas 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.