Stripe can tell your application when a payment or Checkout flow changes state, but it does not natively turn those events into branded lifecycle email. This guide shows how to send email from Stripe with Volanea by receiving a Stripe webhook, verifying it, mapping its event data, and calling the Volanea Email API from server-side code.
There is an important architectural detail up front: Volanea does not provide a native Stripe Marketplace app or one-click Stripe plugin. Stripe can send outbound webhook events to an HTTPS endpoint, but a Stripe webhook cannot be pointed straight at an email-sending API and expected to work. Stripe sends a fixed event envelope and signs it with its own signature; it does not transform that payload into a Volanea email request or attach a Volanea authorization header.
The reliable pattern is therefore:
- A Stripe event occurs, such as a completed Checkout Session.
- Stripe sends a signed webhook request to your serverless function or application endpoint.
- Your endpoint verifies the Stripe signature against the unmodified request body.
- Your code extracts the recipient and business data, checks that the event has not already been processed, and sends a Volanea transactional email.
- Your endpoint returns a successful response to Stripe promptly.
That extra relay is not needless plumbing. It is where you protect credentials, apply business rules, handle missing customer data, and stop Stripe’s delivery retries from creating duplicate receipts.
What Stripe can trigger, and what it cannot do directly
Stripe’s native outbound HTTP mechanism is a webhook endpoint. You configure an HTTPS URL and select which event types it should receive. When an event happens, Stripe delivers a JSON event object to that endpoint. For a post-purchase message, checkout.session.completed is often the most useful trigger because it represents successful completion of a Stripe Checkout Session.
A Stripe event has an outer envelope and an inner resource object. In this case, the inner resource is a Checkout Session at data.object. The event ID belongs to the envelope, while the Checkout Session ID and customer details belong to the nested object.
A simplified checkout.session.completed delivery looks like this:
{
"id": "evt_1QexampleABC123",
"object": "event",
"api_version": "2025-02-24.acacia",
"type": "checkout.session.completed",
"created": 1735689600,
"data": {
"object": {
"id": "cs_test_a1b2c3d4",
"object": "checkout.session",
"mode": "payment",
"payment_status": "paid",
"customer_email": null,
"customer_details": {
"email": "ada@example.com",
"name": "Ada Lovelace"
},
"amount_total": 4900,
"currency": "usd",
"metadata": {
"order_id": "ORD-1042"
}
}
}
}
The exact optional fields in a real event vary. customer_details can be absent or contain null values, depending on how Checkout was configured and what information was collected. Metadata is only present when your integration set it. A robust integration treats those fields as optional rather than assuming every payment has a name, email address, order number, or total.
Stripe webhooks are delivery notifications, not a general-purpose workflow mapper. Stripe does not offer a setting on a webhook endpoint that converts data.object.customer_details.email into a to field for another vendor, creates a custom HTML message, or adds your Volanea API key as an outbound authorization header. That is why an application endpoint, serverless handler, or automation middleware is required.
Choose the Stripe event for the message you mean to send
“Payment confirmation” can describe several different points in a Stripe flow. Choosing the wrong event is a common source of premature emails, missing emails, and confusing customer communication.
Checkout purchase confirmation
For a one-time purchase through Stripe Checkout, start with checkout.session.completed. It is emitted after the customer completes the Checkout flow. If you only accept immediate payment methods, this can be the event that starts an order-received or purchase-confirmation message.
Do not blindly interpret it as “funds are settled for every possible payment method.” Checkout Sessions can use delayed-notification payment methods. In those flows, a session may complete before the payment reaches a successful final state.
Asynchronous payment success
If your Checkout configuration supports payment methods that confirm later, listen for checkout.session.async_payment_succeeded as well. Send the “payment received” message from that event, or use separate language for a session-complete message and a payment-success message.
A useful decision table is:
| Customer message | Recommended Stripe event | Important condition |
|---|---|---|
| We received your order | checkout.session.completed | Suitable when order completion is the intended milestone |
| Your payment is confirmed | checkout.session.async_payment_succeeded | Important for delayed payment methods |
| Your subscription is active | invoice.paid or a subscription-specific workflow | Match the message to your billing model |
| Your payment failed | invoice.payment_failed | Avoid exposing sensitive billing details in email |
| Your refund was issued | charge.refunded | Confirm the exact object and amount before sending |
For a subscription business, checkout.session.completed may be appropriate for a welcome message, but it is not necessarily the right recurring-invoice receipt event. Stripe’s invoice events contain invoice-specific context. Model the email around the event that represents the business fact you want the customer to hear about.
Keep fulfillment separate from notification
An email is not fulfillment. A webhook handler should not use a successful Volanea API response as evidence that an order has been fulfilled, inventory has been reserved, or access has been granted. Persist the business state independently, then send the notification as a separate, idempotent side effect.
This separation matters when email is temporarily unavailable, when a customer’s address bounces, or when Stripe retries the same webhook. The order can remain correctly recorded even if the notification needs a later retry.
The architecture for sending email from Stripe securely
The recommended implementation has two secrets and two distinct trust boundaries:
- Stripe webhook signing secret: used only by your server to verify that a delivery genuinely came from Stripe.
- Volanea API key: used only by your server to authorize email sending with Volanea.
Your public webhook URL is not a secret. Stripe must be able to reach it on the public internet. The endpoint’s safety comes from signature verification, HTTPS, application-level validation, and idempotent processing—not from an obscure URL path.
The Volanea key does not live in Stripe’s client-side settings, Checkout page, success URL, browser JavaScript, or mobile app bundle. There is no direct Stripe-to-Volanea connection in which Stripe should hold that key. Store it in the secret manager or encrypted environment-variable facility used by the serverless function or application that receives Stripe’s webhook.
For example, a production deployment might use these server-only environment variables:
STRIPE_WEBHOOK_SECRET=whsec_...
VOLANEA_API_KEY=...
VOLANEA_FROM_EMAIL=receipts@example.com
VOLANEA_FROM_NAME=Example Store
Never prefix the Volanea key with a convention that makes it available to browser code, such as a public environment-variable prefix used by your frontend framework. A key exposed to a browser can be copied by any visitor and used to send mail under your account, damage sender reputation, and create unexpected sending costs.
If you use a platform secret manager, give the webhook service identity access only to the secrets it needs. Rotate the Volanea key if it is ever exposed, and rotate webhook endpoint secrets when changing the Stripe endpoint configuration. This is a practical reason to use a server-side relay even for a seemingly simple receipt message.
Build the webhook relay: Stripe payload to Volanea email
The following Node.js example uses Express, the Stripe Node library, a raw request body for signature verification, and fetch for the Volanea API call. It handles a checkout.session.completed event, maps Checkout fields into an email request, and uses the Stripe event ID as an idempotency key in an application database.
Before using it, install the Stripe library and configure the environment variables shown above. Consult the Volanea API reference and setup guides for the current sender verification requirements and email API details for your account.
import express from "express";
import Stripe from "stripe";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Replace these with your database implementation. The key must be unique.
async function claimEventForProcessing(eventId) {
// Example SQL pattern:
// INSERT INTO processed_stripe_events (stripe_event_id)
// VALUES ($1) ON CONFLICT DO NOTHING RETURNING stripe_event_id;
// Return true only if this is the first successful claim.
return true;
}
async function sendVolaneaEmail({ to, name, orderId, amountTotal, currency }) {
const money = new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase()
}).format((amountTotal ?? 0) / 100);
const safeName = name || "there";
const orderReference = orderId || "your order";
const response = await fetch("https://api.volanea.com/v1/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.VOLANEA_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
from: {
email: process.env.VOLANEA_FROM_EMAIL,
name: process.env.VOLANEA_FROM_NAME
},
to: [{
email: to,
name: name || undefined
}],
subject: `Thanks for your purchase — ${orderReference}`,
text: `Hi ${safeName},\n\nWe received ${orderReference}. Total: ${money}.\n\nThank you.`,
html: `<p>Hi ${escapeHtml(safeName)},</p><p>We received <strong>${escapeHtml(orderReference)}</strong>.</p><p>Total: <strong>${escapeHtml(money)}</strong></p><p>Thank you.</p>`
})
});
if (!response.ok) {
const details = await response.text();
throw new Error(`Volanea send failed: ${response.status} ${details}`);
}
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
// This route must come before app.use(express.json()). Stripe signature
// verification requires the exact, unparsed request body.
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Invalid Stripe webhook signature", err.message);
return res.status(400).send("Webhook signature verification failed");
}
// Acknowledge event types this endpoint intentionally does not handle.
if (event.type !== "checkout.session.completed") {
return res.status(200).json({ received: true });
}
const session = event.data.object;
const email = session.customer_details?.email || session.customer_email;
const name = session.customer_details?.name || null;
const orderId = session.metadata?.order_id || session.id;
// Do not send an email without a valid destination.
if (!email) {
console.warn("Checkout Session has no email", {
eventId: event.id,
sessionId: session.id
});
return res.status(200).json({ received: true, skipped: "no_email" });
}
try {
const firstDelivery = await claimEventForProcessing(event.id);
if (!firstDelivery) {
return res.status(200).json({ received: true, duplicate: true });
}
await sendVolaneaEmail({
to: email,
name,
orderId,
amountTotal: session.amount_total,
currency: session.currency || "usd"
});
return res.status(200).json({ received: true });
} catch (err) {
console.error("Stripe-to-Volanea processing failed", {
eventId: event.id,
message: err.message
});
// A non-2xx response tells Stripe that delivery was not completed,
// so Stripe can retry according to its webhook retry policy.
return res.status(500).send("Temporary processing failure");
}
});
app.listen(3000);
The field mapping in the example is deliberate:
| Stripe Checkout Session field | Volanea email field | Why it is used |
|---|---|---|
customer_details.email | to[0].email | Primary customer email collected by Checkout |
customer_email | fallback for to[0].email | Useful where the customer-details email is unavailable |
customer_details.name | to[0].name and greeting | Optional personalisation only |
metadata.order_id | subject and message content | Your stable internal reference, if set |
id | fallback order reference | Always identifies the Checkout Session |
amount_total and currency | message total | Amounts are represented in the currency’s minor unit for most currencies |
Do not put unescaped metadata directly into HTML. Metadata may originate in your own application, but escaping dynamic text is still a sound default. The example escapes the name, reference, and formatted amount before inserting them into the HTML body.
Configure Checkout data for useful messages
A technically delivered email can still be a poor receipt if your Stripe Session does not contain the data your customer needs. Plan the data contract before writing the template.
When your application creates a Checkout Session, put an internal order reference in metadata if you need to reference it later. Do not put passwords, bank details, government identifiers, raw addresses, or other sensitive values in Stripe metadata simply to make them available to an email template. Metadata is not a substitute for a secure order database.
For richer receipts, the webhook handler can retrieve additional data from your own order system using metadata.order_id. This has advantages over treating Stripe as the only source of truth:
- Your application can determine which goods were actually ordered and how they should be described.
- You can apply tax, fulfillment, localization, and entitlement logic consistently.
- You can avoid exposing internal product metadata in an outbound email.
- You can preserve a record of exactly which receipt version was sent for an order.
Stripe Checkout Sessions can also have line items, but event payloads should not be treated as a guarantee that every expansion you wish to use is present. If a receipt requires detailed line items, retrieve the relevant Stripe resource server-side or use your own order record. Keep that additional API work within the webhook’s reliability budget, or put it on a durable queue.
Verify the webhook before reading or trusting it
Stripe signs webhook deliveries in the Stripe-Signature header. Your handler must verify that signature using the webhook endpoint’s signing secret and the exact raw HTTP request body. Parsing JSON first, reserializing the body, or routing the request through middleware that changes bytes can cause legitimate verification to fail.
This is why the example applies express.raw({ type: "application/json" }) specifically to the webhook route and places it before a JSON body parser. Other routes in the same application can still use express.json() normally.
Signature verification is not optional even if the endpoint URL is difficult to guess. Without it, an attacker could post a hand-written checkout.session.completed object to your endpoint and cause your service to send arbitrary emails. The same attacker could potentially probe your order behavior through response differences.
After verification, validate the assumptions your email logic makes. At minimum, check the event type, verify that an email address exists, and use your application’s order record for authorization-sensitive decisions. A valid Stripe event says Stripe emitted it; it does not automatically mean every optional field has the form your email template expects.
When this breaks: retries, timeouts, and incomplete payloads
A Stripe-to-email integration should be designed around failure rather than assuming every request runs exactly once. Webhook delivery is at least once: Stripe can retry an event when your endpoint does not return a successful response, when a network interruption occurs, or when Stripe cannot determine whether your endpoint completed processing.
Stripe retries can create duplicate sends
Imagine this sequence: your handler calls Volanea, Volanea accepts the email, and then your server crashes before returning HTTP 200 to Stripe. Stripe sees a failed delivery and retries the event. Without deduplication, the retry sends the same receipt again.
Use event.id as the primary deduplication key because it identifies the Stripe event delivery work you are processing. Enforce uniqueness in a durable database, not an in-memory variable. For business-level safety, you may also retain a unique notification record such as order_id + receipt_type, especially if different Stripe event types could represent the same customer-facing milestone.
Be precise about when you mark an event processed. If you permanently mark it complete before Volanea accepts the message, a temporary email API outage can suppress the receipt forever. A stronger pattern is an outbox table with states such as pending, sending, sent, and failed, plus a worker that retries safely.
Webhook timeouts are not a background-job system
A webhook endpoint should verify, persist, and acknowledge quickly. Long-running database calls, PDF generation, multiple external API requests, or slow email-template rendering increase the chance that Stripe treats delivery as unsuccessful and retries.
For high-volume or business-critical sending, use this flow:
- Verify the Stripe signature.
- Write the event ID and required message job data transactionally to durable storage.
- Return a 2xx response promptly.
- Let a queue worker send through Volanea and record the provider response.
- Retry the email job under your own controlled policy, with idempotency protection.
This design separates Stripe delivery reliability from email-provider availability. It also gives operations staff a way to inspect failed jobs and resend intentionally rather than hoping a webhook retry happens at the right time.
Some expected fields will be missing
customer_details.email is not a universal guarantee. Its availability depends on the Checkout configuration and customer flow. customer_email can be null. A customer name may not be collected. Metadata may be absent because it was never set. amount_total may be inappropriate for the specific statement you are sending, particularly in subscriptions, refunds, discounts, or nonstandard currency scenarios.
Treat missing fields as normal branches:
- If there is no email, acknowledge the webhook, record the skipped notification, and resolve the data issue through your order process.
- If there is no name, use a neutral greeting rather than emitting “Hi null”.
- If there is no metadata order ID, use the Checkout Session ID for traceability or query your own system with another stable reference.
- If delayed payment methods are enabled, avoid sending language that claims payment success until the relevant success event arrives.
Address quality and deliverability failures are separate concerns
Stripe can provide an email address collected during checkout, but collection is not the same as deliverability verification. An address may be syntactically valid yet undeliverable, disposable, or owned by a customer who entered it incorrectly. For forms outside Stripe Checkout, validate addresses before committing to a notification workflow; Volanea provides a free address verification tool for that purpose.
Volanea acceptance also does not guarantee inbox placement. Use a verified sending domain, publish the required authentication records, keep receipt content relevant to the purchase, and separate transactional traffic from promotional campaigns when your program and policy require it.
Use Zapier or Make only when middleware fits the risk
If you do not want to maintain a webhook endpoint, a no-code automation service can receive Stripe events and make an HTTP request to Volanea. Stripe has integrations with automation platforms, and tools such as Make can use a Stripe trigger plus an HTTP request module. This can be practical for low-volume internal notifications or an early prototype.
However, do not assume an automation platform is equivalent to the server-side pattern above. Confirm all of the following before using one for customer receipts:
- It can trigger on the specific Stripe event or object state you need.
- It can map nested fields such as
data.object.customer_details.emailcorrectly. - It can store the Volanea API key as a protected connection or secret rather than exposing it in a client-visible field.
- It has a reliable deduplication strategy based on the Stripe event ID.
- It can surface and replay failed runs without silently dropping them.
- It handles webhook signature verification or receives events through its supported Stripe connection.
For an HTTP module, the Volanea key should be saved in that platform’s secure credential store or connection configuration, never embedded in a public form, Stripe metadata, or client-side script. The automation platform becomes part of your trusted backend and should be assessed accordingly.
A custom webhook relay remains the better default when message timing, receipts, customer data, regulatory retention, or duplicate prevention matter. It gives you explicit control over the raw Stripe event, verification, database write, retry queue, and Volanea request.
Test the integration before enabling live events
Stripe offers test mode and tooling for forwarding events to a local endpoint during development. Use these capabilities to exercise the actual webhook handler rather than only posting a hand-created JSON sample. A manually created sample often misses headers, signature behavior, optional fields, and the exact nesting of the event you will receive.
Your test plan should include more than the happy path:
- Complete a test Checkout Session with an email address and confirm one Volanea message is created.
- Deliver the same event twice and confirm the second delivery does not create another message.
- Test a Session with no name and verify the fallback greeting.
- Test a Session with no email and verify the event is recorded as skipped rather than causing an uncontrolled retry loop.
- Make the Volanea call fail temporarily and verify that your queue or Stripe retry strategy behaves as intended.
- Use an invalid Stripe signature and confirm the endpoint returns a 400 response without sending any mail.
- Test delayed-payment behavior if those payment methods are enabled.
Log Stripe event IDs, Checkout Session IDs, your internal order IDs, and Volanea message identifiers where available. Do not log full API keys, full payment details, or unnecessary customer data. Correlation IDs make it much easier to answer a support request such as “I paid but did not receive my receipt” without searching blindly across systems.
Operational choices that improve the customer experience
A good Stripe-triggered email is timely, factual, and easy to recognize. Send from a domain customers associate with the purchase, use a concise subject, and include an order reference that customer support can locate. Give the customer the next relevant action: access instructions, a download link, shipping expectations, or a support route.
Avoid using a transaction event to inject unrelated marketing. A payment confirmation is expected transactional communication; a promotional campaign has different consent, frequency, and deliverability considerations. Keeping those streams distinct improves relevance and makes reporting more meaningful.
Monitor the whole chain, not just whether Stripe displays a delivered webhook. The useful signals are:
- Stripe webhook delivery success and retry activity.
- The count of events your handler rejects, skips, deduplicates, queues, and completes.
- Volanea API response failures and message-delivery outcomes.
- The time from Stripe event creation to accepted email send.
- The difference between completed Checkout Sessions and receipt jobs over the same period.
That last comparison catches quiet data-contract failures, such as a Checkout change that stops collecting email addresses or an application deployment that changes metadata names. Monitoring the integration as a pipeline is more valuable than treating email as an isolated final API call.
Conclusion
To send email from Stripe with Volanea, use Stripe’s real outbound mechanism—webhooks—and place a secure server-side relay between Stripe and the email API. Start with an event such as checkout.session.completed, verify its signature from the raw request body, map only the data your template needs, keep the Volanea API key in server-side secrets, and deduplicate with the Stripe event ID.
The relay is also where reliable systems handle the details that a direct connector cannot: delayed payment methods, absent customer fields, provider errors, Stripe retries, and durable records of what was sent. Once that foundation is in place, the same pattern can support receipts, subscription notices, refund confirmations, access emails, and internal operational alerts without exposing email credentials or sacrificing control.
FAQ
Can Stripe send directly to the Volanea API?
No. Stripe webhooks send Stripe’s signed event payload to an HTTPS endpoint; they do not transform it into a Volanea email request or attach a Volanea API authorization header. Use your own webhook handler or a suitable automation middleware to make the Volanea call.
Which Stripe event should send a Checkout confirmation email?
For a standard Checkout completion message, use checkout.session.completed. If you accept delayed-notification payment methods and the message specifically says payment succeeded, also handle checkout.session.async_payment_succeeded and align the wording with the actual payment state.
Where should I store the Volanea API key?
Store it in the server-side secret manager or encrypted environment configuration for the webhook relay. Do not put it in Stripe metadata, a Checkout success page, browser JavaScript, a mobile app, or any other client-visible configuration.
How do I stop duplicate receipt emails from Stripe retries?
Persist and uniquely enforce the Stripe event.id before processing the send. For stronger business protection, also record a notification type against your internal order ID. Use a durable database or queue rather than in-memory deduplication.
Why does Stripe sometimes not provide the customer email I expect?
Customer fields depend on the Checkout configuration and the individual flow. Treat customer_details.email, customer_email, names, and metadata as optional, define a fallback or skip policy, and test the exact Checkout modes and payment methods your business enables.