Docs / Integrations / Payments

Payment Integrations

Send receipts, invoices, subscription alerts, and trial reminders automatically when payment events fire — verified via HMAC on every request.

Machine translation available — choose your language in the top bar.

Each integration ships as a self-contained Cloudflare Worker in the integrations/ directory. Clone the repo, drop in your secrets, and run wrangler deploy. Every handler verifies the incoming signature before touching Mailgrid so spoofed webhooks are rejected at the door.

Stripe

Located at integrations/stripe/. The Worker validates every inbound request against Stripe's Stripe-Signature header using HMAC-SHA256 before dispatching an email.

Supported events

Webhook payload → email mapping

integrations/stripe/index.ts
// Verify Stripe signature
const sig   = request.headers.get("stripe-signature");
const valid = await verifyStripeSignature(body, sig, env.STRIPE_WEBHOOK_SECRET);
if (!valid) return new Response("Unauthorized", { status: 401 });

const event = JSON.parse(body);

// Map event type → email template + recipient
const handlers: Record<string, (e: any) => EmailPayload> = {
  "payment_intent.succeeded": (e) => ({
    to:      e.data.object.receipt_email,
    subject: `Receipt — ${formatAmount(e.data.object.amount, e.data.object.currency)}`,
    template: "stripe-receipt",
    vars: { amount: formatAmount(e.data.object.amount, e.data.object.currency) },
  }),
  "invoice.payment_failed": (e) => ({
    to:      e.data.object.customer_email,
    subject: "Action required: payment failed",
    template: "stripe-payment-failed",
    vars: { invoice_url: e.data.object.hosted_invoice_url },
  }),
  "customer.subscription.trial_will_end": (e) => ({
    to:      e.data.object.customer_email,
    subject: "Your trial ends in 3 days",
    template: "stripe-trial-ending",
    vars: { trial_end: new Date(e.data.object.trial_end * 1000).toLocaleDateString() },
  }),
};

const handler = handlers[event.type];
if (!handler) return new Response("OK", { status: 200 }); // unhandled event

const payload = handler(event);
await sendViaMaailgrid(payload, env.MAILGRID_API_KEY, env.FROM_EMAIL);

Environment variables

VariableDescription
STRIPE_WEBHOOK_SECRETWebhook signing secret from the Stripe Dashboard (starts whsec_)
STRIPE_SECRET_KEYStripe secret key for server-side API calls (sk_live_…)
MAILGRID_API_KEYYour Mailgrid API key (mb_live_…)
FROM_EMAILVerified sender address, e.g. billing@yourapp.com

Deploy

terminal
# Deploy the Worker
wrangler deploy --config integrations/stripe/wrangler.toml

# Add secrets (prompts for value, never stored in wrangler.toml)
wrangler secret put STRIPE_WEBHOOK_SECRET
wrangler secret put MAILGRID_API_KEY

# Local dev — forward Stripe events to your Worker
stripe listen --forward-to localhost:8787
Stripe CLI tip

Run stripe listen --print-json to inspect raw event payloads during development before they reach your Worker.

Paddle

Located at integrations/paddle/. Paddle signs requests with a Paddle-Signature header in the format ts=…;h1=…. The Worker extracts the timestamp, recomputes HMAC-SHA256 over ts:body, and compares against h1.

Supported events

Signature verification

integrations/paddle/verify.ts
export async function verifyPaddleSignature(
  body: string,
  header: string,
  secret: string,
): Promise<boolean> {
  const parts = Object.fromEntries(
    header.split(";").map((p) => p.split("="))
  );
  const ts   = parts["ts"];
  const h1   = parts["h1"];
  const key  = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const sig = await crypto.subtle.sign(
    "HMAC", key, new TextEncoder().encode(`${ts}:${body}`),
  );
  return toHex(sig) === h1;
}

Deploy

terminal
wrangler deploy --config integrations/paddle/wrangler.toml
wrangler secret put PADDLE_WEBHOOK_SECRET
wrangler secret put MAILGRID_API_KEY

Lemon Squeezy

Located at integrations/lemonsqueezy/. Lemon Squeezy sends an X-Signature header containing the HMAC-SHA256 hex digest of the raw request body, signed with your webhook signing secret.

Supported events

Signature verification

integrations/lemonsqueezy/verify.ts
export async function verifyLemonSqueezySignature(
  body: string,
  xSignature: string,
  secret: string,
): Promise<boolean> {
  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const sig = await crypto.subtle.sign(
    "HMAC", key, new TextEncoder().encode(body),
  );
  return toHex(sig) === xSignature;
}

Deploy

terminal
wrangler deploy --config integrations/lemonsqueezy/wrangler.toml
wrangler secret put LEMONSQUEEZY_SIGNING_SECRET
wrangler secret put MAILGRID_API_KEY

RevenueCat

Located at integrations/revenuecat/. RevenueCat webhooks use Bearer token authentication — set a shared secret in the RevenueCat dashboard and the Worker validates the Authorization: Bearer <token> header on each request. Ideal for mobile app subscriptions on iOS and Android.

Supported events

Handler

integrations/revenuecat/index.ts
// Bearer token auth
const auth = request.headers.get("authorization") ?? "";
if (auth !== `Bearer ${env.REVENUECAT_WEBHOOK_SECRET}`) {
  return new Response("Unauthorized", { status: 401 });
}

const { event } = await request.json();

// event.type is one of INITIAL_PURCHASE, RENEWAL, CANCELLATION, etc.
if (event.type === "BILLING_ISSUE") {
  await sendViaMaailgrid({
    to:       event.app_user_id,   // map to customer email via your DB
    subject:  "Payment issue with your subscription",
    template: "revenuecat-billing-issue",
    vars: { store: event.store, product_id: event.product_id },
  }, env.MAILGRID_API_KEY, env.FROM_EMAIL);
}
Mapping app_user_id to email

RevenueCat's app_user_id is typically your internal user ID, not an email address. Query your database (or a KV store) to resolve it to a customer email before calling Mailgrid.

Deploy

terminal
wrangler deploy --config integrations/revenuecat/wrangler.toml
wrangler secret put REVENUECAT_WEBHOOK_SECRET
wrangler secret put MAILGRID_API_KEY

Chargebee

Located at integrations/chargebee/. Chargebee authenticates webhooks via HTTP Basic Auth — the username is your Chargebee site name and the password is a shared webhook password you configure in the Chargebee console.

Supported events

Basic Auth verification

integrations/chargebee/verify.ts
export function verifyBasicAuth(
  request: Request,
  expectedUsername: string,
  expectedPassword: string,
): boolean {
  const authHeader = request.headers.get("authorization") ?? "";
  if (!authHeader.startsWith("Basic ")) return false;
  const decoded  = atob(authHeader.slice(6));
  const [user, pass] = decoded.split(":");
  return user === expectedUsername && pass === expectedPassword;
}

Deploy

terminal
wrangler deploy --config integrations/chargebee/wrangler.toml
wrangler secret put CHARGEBEE_WEBHOOK_USERNAME   # your site name
wrangler secret put CHARGEBEE_WEBHOOK_PASSWORD
wrangler secret put MAILGRID_API_KEY

Webhooks quick reference

At a glance — auth method, the header to inspect, and the deploy command for each integration.

Provider Auth method Signature header Deploy command
Stripe HMAC-SHA256 Stripe-Signature wrangler deploy --config integrations/stripe/wrangler.toml
Paddle HMAC-SHA256 (ts=…;h1=…) Paddle-Signature wrangler deploy --config integrations/paddle/wrangler.toml
Lemon Squeezy HMAC-SHA256 X-Signature wrangler deploy --config integrations/lemonsqueezy/wrangler.toml
RevenueCat Bearer token Authorization wrangler deploy --config integrations/revenuecat/wrangler.toml
Chargebee HTTP Basic Auth Authorization wrangler deploy --config integrations/chargebee/wrangler.toml
Never log raw webhook bodies in production

Webhook payloads can contain PII (email addresses, billing details). Use structured logging that redacts sensitive fields, or strip them before forwarding to any log aggregator.