Stripe can tell your system that a payment, subscription, refund, or invoice event occurred; Volanea can deliver the resulting message. This guide shows the honest, production-ready way to send transactional email from Stripe with a webhook receiver and Volanea’s REST API—without claiming a native Stripe app or one-click integration exists.

There is no Volanea app to install in Stripe today. Instead, Stripe delivers an event to an HTTPS endpoint you control, your endpoint verifies the event’s signature, maps the relevant Stripe data into an email, and sends that email through Volanea. That small server-side layer is important: it keeps both your Stripe webhook secret and Volanea API key private, gives you a place to apply business rules, and prevents duplicate messages when events are retried.

The architecture: Stripe event to Volanea email

The integration has four steps:

  1. A customer completes Checkout, pays an invoice, receives a refund, or triggers another Stripe event.
  2. Stripe sends an HTTPS POST request containing a Stripe Event object to your webhook endpoint.
  3. Your server verifies the Stripe-Signature header against the unmodified request body, then decides whether the event warrants an email.
  4. Your server calls Volanea’s POST /v1/send endpoint with the recipient, sender, subject, HTML, plain-text fallback, and a stable Idempotency-Key.

This is deliberately different from sending an API key through a browser, a Stripe Dashboard field, or a generic no-code webhook action. A transactional email provider credential can send mail for your domain. It belongs in server-side environment variables, not in front-end code or an exposed automation configuration.

Stripe supports webhook event destinations that send JSON Event objects to an HTTPS endpoint. A single endpoint can receive multiple event types, which makes it practical to centralize your payment-email logic while still keeping each message type explicit in code. Stripe also retries webhook delivery when it does not receive a successful response, so your integration needs duplicate protection rather than assuming each event arrives exactly once.

The basic flow looks like this:

Stripe Checkout / Billing / Payments
            |
            | Stripe Event JSON + Stripe-Signature
            v
Your HTTPS webhook endpoint
            |
            | verify signature, map data, deduplicate
            v
Volanea POST /v1/send
            |
            v
Customer inbox

What “Send Email From Stripe” means in this guide

“Send Email From Stripe” is a useful description of the workflow, but it should not be confused with a native Volanea integration inside Stripe. Stripe has built-in customer email settings for some receipts, refunds, invoices, failed payments, trial-ending reminders, and subscription notices. Those can be useful when the default Stripe message fits your requirements.

Use the webhook-to-API pattern when you need control Stripe’s built-in settings do not provide: a custom sender identity, a branded template, application-specific links, richer personalization, different email copy by plan, internal notifications, or messages that are tied to your own fulfillment state.

For example, a successful Checkout Session may be the point at which you want to send a welcome email containing:

  • A link to create an account or set a password.
  • The plan name selected in your application.
  • An onboarding checklist based on the product purchased.
  • A support contact that reflects the customer’s segment or region.
  • A receipt or billing-management link supplied by Stripe.

The webhook is the trigger, not the final source of truth for every decision. For high-value flows, your webhook should use Stripe’s event ID to find a record in your database, confirm that entitlement provisioning succeeded, and then send the email. That makes the message reflect what your product actually did—not only what a payment system reported.

Choose the right Stripe event before writing email code

A common mistake is treating every successful-looking Stripe event as an order-confirmation trigger. The best event depends on your payment model and what the email is meant to communicate.

Checkout purchase confirmation

For Stripe Checkout, checkout.session.completed is usually the starting point for a customer-facing confirmation or onboarding message. Stripe’s Checkout guidance uses this event for post-payment fulfillment. The event’s object is a Checkout Session, so it can contain useful fields such as the session ID, mode, currency, amount totals, customer_details, customer_email, and a client reference ID if you supplied one.

However, a completed Checkout Session is not always equivalent to immediately settled payment for every payment method. If your business cannot grant access until the payment is confirmed, use the event sequence appropriate to your payment method and fulfillment model. In many systems, it is safer to update your own order record from the webhook and let a separate state transition decide whether to send a “payment confirmed” email.

Subscription and invoice messages

For recurring billing, invoice events are generally more precise than Checkout events after the first purchase. Useful examples include:

  • invoice.payment_succeeded for a successful recurring invoice payment.
  • invoice.payment_failed for a payment-failure or update-payment-method message.
  • customer.subscription.trial_will_end for a trial-ending reminder.
  • customer.subscription.updated for a plan-change confirmation, provided you compare the relevant values rather than emailing on every update.
  • customer.subscription.deleted for a cancellation confirmation or win-back workflow.

Do not automatically send a generic “thank you” for all of these. A payment-recovery email should clearly state the required next action. A downgrade confirmation should state when the new plan takes effect. A cancellation email should distinguish immediate cancellation from cancellation at the end of the current billing period.

Refund confirmation

For refunds, charge.refunded can trigger a branded confirmation. Stripe also offers automatic refund receipts, so decide whether your message supplements or replaces that receipt experience. If you send both, use different purposes: Stripe’s receipt can be the payment record, while your Volanea email can explain product access, shipping, next steps, or support expectations.

The Stripe webhook payload you receive

Stripe sends a JSON Event object in the HTTP request body. The outer wrapper identifies the event and places the related Stripe resource inside data.object. For a checkout.session.completed event, the object nested at data.object is a Checkout Session.

A simplified, representative payload looks like this:

{
  "id": "evt_1RExampleABC123",
  "object": "event",
  "api_version": "2026-07-29.dahlia",
  "created": 1780000000,
  "data": {
    "object": {
      "id": "cs_test_a1ExampleCheckoutSession",
      "object": "checkout.session",
      "amount_subtotal": 4900,
      "amount_total": 4900,
      "currency": "usd",
      "customer": "cus_Example123",
      "customer_details": {
        "email": "ada@example.com",
        "name": "Ada Lovelace"
      },
      "customer_email": "ada@example.com",
      "mode": "subscription",
      "payment_status": "paid",
      "status": "complete",
      "subscription": "sub_Example123",
      "metadata": {
        "app_user_id": "user_12345",
        "plan_name": "Pro"
      }
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "type": "checkout.session.completed"
}

Treat this example as a shape guide rather than a schema contract for every account and event. The fields available in data.object vary by event type, account configuration, API version, and how the original payment or Checkout Session was created. For example, customer_details can be absent, and customer_email can be null. Code defensively and deliberately reject or queue an event when there is no valid recipient.

Two fields matter especially for reliability:

  • id at the top level is the Stripe event ID. Use it as the idempotency identity for the email send.
  • type tells you which workflow should run. Never infer the event type from fields that happen to be present.

The request also includes the Stripe-Signature header. Do not attempt to reproduce or compare this signature yourself with parsed JSON. Verify it using Stripe’s SDK and the exact raw body received over HTTP. Parsing and reserializing JSON before verification can change the byte sequence and cause signature validation to fail.

Set up a secure Stripe webhook endpoint

Before writing the Volanea call, create an HTTPS endpoint in your application. In Stripe, configure an event destination for that URL and subscribe only to the events your application handles. Keeping the event list focused makes the handler easier to test and reduces accidental behavior from unrelated account activity.

For this tutorial, suppose the endpoint is:

https://app.example.com/webhooks/stripe

Your deployment needs these server-side secrets:

STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
VOLANEA_API_KEY=sk_test_...
VOLANEA_FROM_EMAIL="Acme <billing@updates.example.com>"

Use a Stripe test-mode secret and test webhook signing secret while developing. Use separate production secrets after you create the live-mode event destination. A webhook secret is specific to the endpoint and mode, so copying a test secret into production will make verification fail.

Verify the raw request body

The handler must preserve raw bytes for Stripe signature verification. In Express, define the webhook route with express.raw({ type: "application/json" }) before any global JSON middleware that would parse this route. You can use express.json() for the rest of your application after the webhook route is registered.

Do not do these things:

  • Do not accept the event based only on a shared URL or an IP allowlist.
  • Do not call JSON.parse() before stripe.webhooks.constructEvent().
  • Do not use the Checkout success page as your fulfillment or sending trigger. A customer can close the page, revisit it, or manipulate browser state.
  • Do not acknowledge the Stripe request as successful before you have safely recorded or completed the work needed to prevent loss.

A robust production architecture often persists the verified event ID and payload, returns a fast 2xx response, and processes the email in a queue. The direct version below is easier to understand and appropriate for a short-running transactional operation, but it still protects the Volanea send itself with an idempotency key.

Working Node.js code: Stripe webhook to Volanea REST API

Install the Stripe SDK and Express:

npm install express stripe

Create server.mjs with the following code. It listens for checkout.session.completed, verifies the Stripe signature, reads the customer email, builds a plain-text and HTML message, and posts it to Volanea’s REST API.

import "dotenv/config";
import express from "express";
import Stripe from "stripe";

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

function escapeHtml(value = "") {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function formatAmount(amount, currency) {
  if (typeof amount !== "number" || !currency) return null;

  return new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: currency.toUpperCase()
  }).format(amount / 100);
}

async function sendWithVolanea({ to, subject, text, html, eventId }) {
  const response = await fetch("https://api.volanea.com/v1/send", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.VOLANEA_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `stripe-event:${eventId}:checkout-confirmation`
    },
    body: JSON.stringify({
      from: process.env.VOLANEA_FROM_EMAIL,
      to: [to],
      subject,
      text,
      html,
      headers: {
        "X-Stripe-Event-ID": eventId
      }
    })
  });

  const responseText = await response.text();

  if (!response.ok) {
    throw new Error(
      `Volanea send failed with ${response.status}: ${responseText}`
    );
  }

  return responseText ? JSON.parse(responseText) : null;
}

app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["stripe-signature"];

    if (typeof signature !== "string") {
      return res.status(400).send("Missing Stripe-Signature header");
    }

    let event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        signature,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (error) {
      console.error("Stripe signature verification failed:", error.message);
      return res.status(400).send("Invalid webhook signature");
    }

    try {
      if (event.type !== "checkout.session.completed") {
        return res.status(200).json({ received: true, ignored: true });
      }

      const session = event.data.object;

      // For delayed payment methods, choose an event strategy that matches
      // your fulfillment rule. This example only confirms sessions marked paid.
      if (session.payment_status !== "paid") {
        return res.status(200).json({
          received: true,
          ignored: true,
          reason: "Checkout Session is not paid"
        });
      }

      const recipient = session.customer_details?.email ?? session.customer_email;

      if (!recipient) {
        console.error("No customer email on Checkout Session", session.id);
        return res.status(200).json({
          received: true,
          ignored: true,
          reason: "No recipient email"
        });
      }

      const customerName = session.customer_details?.name || "there";
      const safeName = escapeHtml(customerName);
      const amount = formatAmount(session.amount_total, session.currency);
      const planName = session.metadata?.plan_name || "your plan";
      const safePlanName = escapeHtml(planName);
      const safeSessionId = escapeHtml(session.id);

      const subject = "Your purchase is confirmed";
      const text = [
        `Hi ${customerName},`,
        "",
        `Thanks for choosing ${planName}. Your purchase is confirmed${amount ? ` for ${amount}` : ""}.`,
        "",
        "You can now return to the app and continue getting started.",
        "",
        `Reference: ${session.id}`
      ].join("\n");

      const html = `
        <!doctype html>
        <html lang="en">
          <body style="margin:0;padding:24px;background:#f6f7f9;font-family:Arial,sans-serif;color:#1f2937;">
            <main style="max-width:600px;margin:0 auto;background:#ffffff;padding:32px;border-radius:12px;">
              <h1 style="margin-top:0;font-size:24px;">Purchase confirmed</h1>
              <p>Hi ${safeName},</p>
              <p>Thanks for choosing <strong>${safePlanName}</strong>. Your purchase is confirmed${amount ? ` for <strong>${escapeHtml(amount)}</strong>` : ""}.</p>
              <p>You can now return to the app and continue getting started.</p>
              <p style="color:#6b7280;font-size:14px;">Reference: ${safeSessionId}</p>
            </main>
          </body>
        </html>
      `;

      await sendWithVolanea({
        to: recipient,
        subject,
        text,
        html,
        eventId: event.id
      });

      return res.status(200).json({ received: true, emailed: true });
    } catch (error) {
      console.error("Stripe webhook processing failed:", error);
      // A non-2xx response tells Stripe that processing did not complete.
      // The same Idempotency-Key makes a retried Volanea request safe.
      return res.status(500).send("Webhook processing failed");
    }
  }
);

app.use(express.json());

app.get("/health", (_req, res) => {
  res.status(200).json({ ok: true });
});

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});

The Volanea request uses POST https://api.volanea.com/v1/send, Bearer authentication, JSON content, and an Idempotency-Key. The from value should be a sender address on a domain you have authenticated for sending. The to field is an array, even though this example sends one message to one customer.

For the full endpoint reference, authentication details, and sending options, see the Volanea email API documentation.

Why the Idempotency-Key is not optional

Stripe webhooks are at-least-once deliveries, not exactly-once deliveries. A network timeout can occur after Volanea accepted an email but before your application received its response. If your endpoint returns a failure in that situation, Stripe can retry the same event. Without a stable idempotency key, your application may send the customer the same purchase confirmation twice.

This line makes retries for the same Stripe event and email purpose represent the same logical send:

"Idempotency-Key": `stripe-event:${eventId}:checkout-confirmation`

Use the event ID rather than generating a new random UUID for every attempt. A random value defeats deduplication because each retry looks like a new message. Include the email purpose as a suffix so that one event can legitimately generate separate messages when your design requires it—for example, a customer confirmation and an internal sales notification.

Idempotency at the sending API is valuable, but it is not a replacement for your own database-level event tracking. For important commerce flows, record at least:

  • Stripe event ID.
  • Event type and received timestamp.
  • Stripe resource ID, such as the Checkout Session or invoice ID.
  • Your application user or order ID.
  • Email purpose, recipient, and Volanea response identifier if returned.
  • Processing state, error details, and retry count.

This record gives support and engineering teams an answer to “Did we email this customer?” without relying on assumptions from a webhook log alone.

Map Stripe data safely into email content

Payment data is useful for personalization, but it is also externally supplied input. Escape values before interpolating them into HTML. The example’s escapeHtml() helper protects customer names, metadata values, and session identifiers from being interpreted as markup.

Prefer your database for product facts

Stripe metadata is helpful for identifiers, but avoid putting your entire email model into metadata. Metadata can be stale, truncated, manually edited, or absent on historical objects. A better pattern is to include a stable internal ID, such as app_user_id or order_id, then load the current plan name, account URL, fulfillment status, and support context from your database.

For example:

const appUserId = session.metadata?.app_user_id;
const order = await orders.findByStripeCheckoutSessionId(session.id);

if (!order || order.status !== "fulfilled") {
  throw new Error("Order is not ready for confirmation email");
}

This separates billing facts from application facts. Stripe confirms a payment-related event; your application determines exactly what access, delivery, or onboarding step the buyer should receive.

Build both HTML and text versions

Send a text alternative alongside HTML. Plain text improves readability in text-only environments, helps when HTML rendering is restricted, and gives recipients a useful fallback. The content should communicate the same essential facts: what happened, what the customer should do next, and how to get help.

Avoid copying raw Stripe JSON into an email. It can expose unnecessary information, create confusing content, and leak internal IDs. Construct a concise message from only the fields the recipient needs.

Test the integration before enabling live events

Stripe’s CLI is useful for sending test events to a local webhook endpoint. Start your server, then forward test-mode webhook events to it:

stripe listen --forward-to localhost:3000/webhooks/stripe

The CLI prints a webhook signing secret. Use that local secret as STRIPE_WEBHOOK_SECRET while testing through the CLI. Then trigger a sample event:

stripe trigger checkout.session.completed

The built-in fixture may not include every field your production Checkout Session provides, especially custom metadata. Test both the fixture and an actual test-mode Checkout flow created by your application. Confirm all of the following:

  1. A valid signed event gets a 200 response.
  2. An invalid or missing signature gets a 400 response.
  3. The intended recipient is selected when customer_details.email exists.
  4. The handler safely ignores an event without a recipient instead of sending to an unintended address.
  5. The Volanea request has a verified sender and reaches the test inbox.
  6. Replaying the same Stripe event does not create a second logical email send.
  7. An unrelated Stripe event returns a quick 200 response and does not produce an email.

Keep Stripe test and live modes separate. Test events have IDs that begin with test-mode object prefixes, use test keys, and should point to test credentials. Before enabling live events, create a live webhook destination, configure its separate signing secret, authenticate your real sending domain in Volanea, and send a controlled purchase through the full production path.

Handle delayed payments, retries, and failures

Email timing communicates business truth. A message saying “Your payment is confirmed” should not be sent merely because a customer reached a success page or because a session completed before an asynchronous payment method finished processing.

For Checkout, inspect the session’s payment status and select Stripe events that align with the specific payment methods you support. If fulfillment occurs later, wait until your application marks the order fulfilled. If you support subscriptions, make invoice events the source of truth for renewal receipts rather than treating the initial Checkout event as a recurring billing signal.

When Volanea returns an error

If Volanea rejects the send request due to authentication, payload validation, sender setup, or a temporary upstream issue, return a non-2xx response from your webhook handler after logging the failure. Stripe can retry the event. Because the Volanea call uses the same idempotency key, a retry remains safe if the first request actually succeeded but your process lost the response.

For resilient systems, move the post-verification work to a queue:

Verify Stripe signature
        |
        v
Store event ID + enqueue job
        |
        v
Return 200 to Stripe quickly
        |
        v
Worker loads order state and calls Volanea

A queue is especially helpful when your handler must retrieve additional data, generate an attachment, contact several services, or send a sequence of emails. It isolates Stripe’s webhook response time from email-provider latency and gives you controlled retry policies.

Avoid duplicate channels

Before launching, check whether Stripe’s automatic customer emails are enabled for the same event. If Stripe sends a payment receipt and your workflow sends a purchase confirmation, recipients may receive two messages. That can be acceptable when their purposes are distinct, but it is poor customer experience when both repeat the same receipt details.

Make a deliberate decision for each event:

EventStripe built-in emailVolanea workflow
Successful one-time paymentOptional receiptBranded confirmation and onboarding
RefundOptional refund receiptProduct-access or fulfillment explanation
Failed recurring paymentStripe recovery email availableCustom dunning only if it adds useful account context
Trial endingStripe reminder availableProduct-specific conversion or onboarding guidance
Subscription cancellationStripe notification options availableCancellation confirmation with account-specific next steps

Deliverability and sender setup for payment emails

A webhook can run perfectly while an unauthenticated or poorly configured sender causes delivery problems. Configure and authenticate the domain you use in VOLANEA_FROM_EMAIL before treating the integration as production-ready. Your sender should be recognizable, stable, and aligned with the brand and links in the message.

Good payment-email practices include:

  • Send from a monitored address or clearly state that replies are not monitored.
  • Use a consistent sender identity, such as billing@updates.example.com or support@example.com.
  • Keep payment confirmations transactional rather than promotional.
  • Include a direct support path for billing questions.
  • Avoid adding unrelated marketing content to a required receipt, cancellation, or payment-failure message.
  • Keep account-management and payment-update links on trusted HTTPS domains.

For recipient data quality, validate addresses before putting them into your own CRM or creating a customer record. Volanea’s email address verification tool can help catch malformed or risky addresses before they become part of an automated workflow.

Common implementation mistakes

The mechanics of sending an HTTP request are simple. The expensive mistakes are usually about trust boundaries and event semantics.

Calling Volanea directly from Stripe configuration

Do not place a Volanea secret key in a client-side app, public webhook configuration, or a URL parameter. Use a server-side endpoint you control. It is the correct boundary for verifying Stripe, applying logic, and keeping credentials private.

Triggering from the browser return URL

A Checkout success_url is for customer experience, not a reliable back-end trigger. Customers may not return to it, may refresh it, or may open it later. Use webhooks for fulfillment and transactional email.

Assuming every Checkout Session has an email

Check customer_details.email and customer_email defensively. If neither exists, log the event and use a defined recovery process. Never substitute an arbitrary internal email or silently send to a stale address without a clear policy.

Treating every subscription update as an email event

Subscription objects can change for operational reasons that customers do not need to hear about. Compare relevant previous and current values, or create rules that only send for meaningful changes such as price, interval, quantity, cancellation status, or trial status.

Sending HTML with unescaped input

Names, metadata, descriptions, and custom fields may contain characters that alter HTML. Escape dynamic content or use a template system that escapes by default. Keep trusted HTML fragments separate from untrusted strings.

Conclusion: build a dependable Stripe-to-email boundary

To send a transactional email from Stripe using Volanea, do not look for a native installation flow that does not exist. Build the small integration boundary that production systems need: a verified Stripe webhook endpoint, an explicit event-to-message mapping, and a server-side Volanea API call protected by a stable idempotency key.

That approach gives you more than a confirmation email. It gives you a foundation for subscription lifecycle messages, invoice notices, payment recovery, refunds, and application-specific onboarding—while keeping billing events, customer data, credentials, and deliverability responsibilities in the right places.

FAQ

Is there a native Volanea app for Stripe?

No. The supported pattern is to receive Stripe events at an HTTPS endpoint you control and call Volanea’s REST API from your server. This keeps credentials private and lets you verify Stripe signatures before sending email.

Which Stripe event should send an order confirmation?

For a typical Stripe Checkout flow, start with checkout.session.completed. Confirm that the session and payment state match your fulfillment policy before emailing. For recurring payments, use invoice events such as invoice.payment_succeeded for renewal-specific messaging.

Why do I need to verify the Stripe signature?

A public webhook URL can receive requests from anyone. Stripe signature verification proves that the request was signed using your endpoint’s webhook secret and that the raw body was not altered before verification.

How do I prevent a Stripe retry from sending duplicate emails?

Use the Stripe event ID as part of Volanea’s Idempotency-Key, and keep that key identical on retries. For high-value workflows, also store processed Stripe event IDs and email outcomes in your database.

Can I use Stripe’s automatic receipts and Volanea emails together?

Yes, but make their purposes distinct. For example, let Stripe send a payment receipt while Volanea sends onboarding instructions or product-access details. Avoid sending two near-identical payment confirmations for the same event.