Shopify can trigger events when an order, fulfillment, customer, or inventory record changes—but to send transactional email from Shopify through Volanea today, use an HTTP workflow rather than a native app installation. This guide shows the honest implementation: Shopify Flow sends a JSON request to your secure endpoint, and that endpoint calls Volanea’s REST API.
What this Shopify-to-Volanea setup does
This is not a native Volanea app or one-click Shopify connector. There is no Volanea app-install flow to enable, no embedded Shopify configuration page, and no claim that Shopify’s built-in notification system has been replaced.
Instead, this is an event-driven integration with three parts:
- Shopify Flow watches for a business event, such as an order being paid or a fulfillment being created.
- Flow’s Send HTTP request action posts selected Shopify data as JSON to an endpoint you control.
- Your endpoint validates the request, builds the email, and sends it through Volanea’s
POST /v1/sendREST endpoint.
This pattern is useful when the message is outside Shopify’s standard notification templates. Examples include a backorder explanation, a manual-review notice, a digital-download follow-up, a wholesale approval email, a delayed-shipment update, or a staff alert that must include order context.
Shopify Flow supports HTTP requests with a URL, headers, and body. It also supports stored secrets, so a shared webhook secret does not have to appear in workflow text or execution logs. Shopify Flow waits up to 30 seconds for a response and can retry failed HTTP actions according to the error behavior you choose. The HTTP request action is currently available on Grow, Advanced, and Plus plans.
The key design decision is simple: do not place a long-lived Volanea API key in browser code, storefront JavaScript, or a public endpoint. Keep the key in a server-side environment variable or a serverless secret store.
Why use a webhook endpoint instead of sending directly from Flow?
In principle, Shopify Flow can send an HTTP request directly to an external API. In production, a thin receiver is usually the safer and more flexible option.
A receiver gives you a boundary between Shopify’s event model and your email-delivery model. It can authenticate the incoming request, normalize optional fields, escape customer data before it becomes HTML, suppress invalid events, create deterministic idempotency keys, log a correlation ID, and return the right status code to Flow.
It also prevents a workflow editor from becoming the place where business-critical email markup and API behavior live. A short Flow body is easy to understand. A version-controlled service is much easier to test, review, deploy, and roll back.
The recommended delivery path
Use this route for a production workflow:
Shopify event
→ Shopify Flow trigger
→ Send HTTP request action
→ your HTTPS webhook endpoint
→ Volanea POST /v1/send
→ recipient inbox
For example, an Order paid workflow can send an operational message after payment is captured. A Fulfillment created workflow can send a tailored shipping expectation. A customer-related trigger can notify an internal team that a high-value account was created.
The receiver does not need to be a large application. It can be a Node.js route, a serverless function, a Cloudflare Worker, a small Express service, or an endpoint in an existing backend.
What Shopify Flow actually sends
There is no single, immutable “Send Email From Shopify webhook payload.” Shopify Flow’s Send HTTP request action lets you define the request body yourself using Flow variables and Liquid. That means the exact payload received by your endpoint is the payload you configure in the action.
That is an advantage: send only the fields necessary to compose the message. Avoid forwarding an entire order object, full address data, or payment-related fields when the email only requires an order number, recipient address, customer name, and order total.
The next section provides a concrete payload that you can paste into a Flow action for an order-paid email.
Prerequisites before you send
Before building the workflow, prepare the sending side and the receiving side.
1. Verify a sending domain in Volanea
Use a sender address on a domain you control and have verified in Volanea. Domain authentication matters because recipient providers evaluate alignment and authentication when deciding whether to accept, junk, or reject a message.
A practical sender for commerce operations might be orders@notify.example.com or support@example.com. Many teams use a transactional subdomain to keep operational-mail reputation distinct from campaign traffic, while still making the sender recognizable to customers.
Set up the DNS records Volanea provides for the sending domain before turning on the Flow. Do not guess record names or copy records from another email vendor: open the domain setup in your Volanea account and publish the exact SPF, DKIM, tracking, return-path, and DMARC-related values shown for your domain.
2. Create a Volanea API key
Create a secret key for server-to-server use and store it as VOLANEA_API_KEY in your deployment platform’s secret manager. Do not put it in a committed .env file, a Shopify note, a Flow body, or a client-side app bundle.
The Volanea single-message endpoint is POST https://api.volanea.com/v1/send. It supports an Idempotency-Key header, which is particularly valuable in webhook workflows because HTTP senders can retry after timeouts or transient failures.
For details on request fields, templates, and response handling, refer to the email API reference and setup guides.
3. Deploy a public HTTPS endpoint
Your endpoint must be reachable over HTTPS from Shopify Flow. For this guide, assume the receiver is available at:
https://email-worker.example.com/webhooks/shopify/order-paid
For local development, use a secure tunneling tool only for testing. Before production, deploy to a stable domain with TLS and a monitored runtime.
4. Create a Flow secret
In Shopify Flow settings, create a secret with a handle such as shopify_to_volanea_webhook_token. Give it a long random value. Your Flow action will include it in a request header; your receiver will compare it to SHOPIFY_FLOW_WEBHOOK_TOKEN stored in its own environment.
This is not a substitute for good operational controls, but it prevents arbitrary callers from triggering your email endpoint merely by discovering its URL.
Build the Shopify Flow workflow
This example uses an order-paid event because it is common and demonstrates customer data, order data, and a unique event identifier. The same structure works with fulfillment, customer, draft-order, inventory, and other Flow-supported triggers.
Choose the correct trigger
Open Shopify admin, go to Apps > Flow, and create a workflow. Select an order event appropriate to the message you want to send.
For a payment-confirmation or follow-up email, choose an order-paid trigger if the email should happen after payment is recorded. Do not use an order-created trigger if the message promises payment success and your checkout flow can produce unpaid orders.
For a fulfillment-related message, use a fulfillment event instead. The event should match the business fact that the recipient needs to know.
A few examples:
- Order paid: payment was captured; useful for post-purchase instructions or high-value-order handling.
- Fulfillment created: fulfillment processing began; useful for warehouse or delivery-related messages.
- Customer created: a customer record exists; useful for B2B onboarding or internal approval review.
- Product status updated: a product changed state; useful for internal inventory or merchandising alerts.
Add conditions before the HTTP action
Use Flow conditions to avoid sending unnecessary or inappropriate mail. Conditions are cheaper and clearer in Flow than in a downstream email function when the condition relies on Shopify data already available in the workflow.
For example, before sending a delayed-order guidance email, you might require:
- The order is paid.
- The customer has a usable email address.
- The order contains a particular product tag or shipping method.
- The order is not a test order.
- The order has not already been given a tag indicating that this message was sent.
Be careful with repeating triggers. An order can change several times during its lifecycle. If the email must send once, combine a stable event ID with idempotency and, where appropriate, add a Shopify tag only after a successful send.
Add the Send HTTP request action
Add Shopify Flow’s Send HTTP request action after the trigger and any conditions.
Configure it as follows:
| Flow field | Value |
|---|---|
| HTTP method | POST |
| URL | https://email-worker.example.com/webhooks/shopify/order-paid |
| Header | Content-Type: application/json |
| Header | X-Webhook-Token: {{secrets.shopify_to_volanea_webhook_token}} |
| Body | Use the JSON body below |
The shared secret should be inserted using Flow’s secret feature rather than typed as plain text. Shopify documents that Flow secrets are encrypted and obfuscated in the interface and execution logs.
Use this Shopify Flow payload shape
Paste the following body into the Send HTTP request action. It is intentionally compact: it passes only what the receiver needs for an order-paid email.
{
"event": "order.paid",
"event_id": "shopify-order-paid-{{order.id}}",
"shop": "{{shop.myshopifyDomain}}",
"order": {
"id": "{{order.id}}",
"name": "{{order.name}}",
"total_price": "{{order.totalPriceSet.shopMoney.amount}}",
"currency": "{{order.totalPriceSet.shopMoney.currencyCode}}",
"status_url": "{{order.statusPageUrl}}"
},
"customer": {
"email": "{{order.customer.email}}",
"first_name": "{{order.customer.firstName | default: ''}}",
"last_name": "{{order.customer.lastName | default: ''}}"
}
}
This is the exact JSON shape your receiver should expect when you configure the Flow body as above. It is not a universal Shopify webhook schema; it is a deliberate, controlled contract between your workflow and your endpoint.
Why each field is present
event is a readable event label. It lets one endpoint support several workflows later without relying only on the URL path.
event_id is a stable identifier for this logical notification. It is derived from the Shopify order ID and becomes the basis for the Volanea idempotency key. If Flow retries the same request, the receiver can make the same Volanea request with the same key instead of generating a second email.
shop identifies the originating store. This becomes important if the endpoint will support development, staging, multiple storefronts, or multiple merchant accounts.
order.name is generally the customer-facing order label, such as #1042. It is more useful in email copy than an opaque GraphQL ID.
total_price and currency let the message show a concise confirmation. If you do not need totals, remove them. Passing less data reduces exposure and makes payloads easier to inspect.
status_url allows the email to link the customer to Shopify’s order-status page when the field is available to the workflow. Only use it if that URL is appropriate for the recipient and your message’s purpose.
customer.email is the address to which Volanea will send. The receiver must validate that it is present and syntactically plausible before calling the email API.
Create the secure webhook receiver
The following Node.js example uses Express. It accepts the Flow payload, verifies the shared token in a timing-safe comparison, validates the minimum required data, HTML-escapes dynamic values, and sends a message through Volanea.
Install Express:
npm install express
Set these environment variables in your runtime:
VOLANEA_API_KEY=replace_with_your_volanea_secret_key
SHOPIFY_FLOW_WEBHOOK_TOKEN=replace_with_the_same_flow_secret_value
EMAIL_FROM=orders@notify.example.com
EMAIL_FROM_NAME="Example Store"
Then create server.mjs:
import crypto from "node:crypto";
import express from "express";
const app = express();
app.use(express.json({ limit: "100kb" }));
const {
VOLANEA_API_KEY,
SHOPIFY_FLOW_WEBHOOK_TOKEN,
EMAIL_FROM,
EMAIL_FROM_NAME = "Store team"
} = process.env;
for (const [name, value] of Object.entries({
VOLANEA_API_KEY,
SHOPIFY_FLOW_WEBHOOK_TOKEN,
EMAIL_FROM
})) {
if (!value) throw new Error(`Missing required environment variable: ${name}`);
}
function safeEqual(a = "", b = "") {
const left = Buffer.from(a);
const right = Buffer.from(b);
return left.length === right.length && crypto.timingSafeEqual(left, right);
}
function escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function looksLikeEmail(value) {
return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
app.post("/webhooks/shopify/order-paid", async (req, res) => {
const receivedToken = req.get("X-Webhook-Token") || "";
if (!safeEqual(receivedToken, SHOPIFY_FLOW_WEBHOOK_TOKEN)) {
return res.status(401).json({ error: "Unauthorized" });
}
const { event, event_id: eventId, order, customer } = req.body ?? {};
if (event !== "order.paid") {
return res.status(400).json({ error: "Unexpected event" });
}
if (!eventId || !order?.name || !looksLikeEmail(customer?.email)) {
return res.status(400).json({ error: "Invalid event payload" });
}
const firstName = escapeHtml(customer.first_name || "there");
const orderName = escapeHtml(order.name);
const amount = escapeHtml(order.total_price || "");
const currency = escapeHtml(order.currency || "");
const statusUrl = typeof order.status_url === "string" ? order.status_url : "";
const text = [
`Hi ${customer.first_name || "there"},`,
"",
`We received payment for order ${order.name}.`,
amount && currency ? `Order total: ${amount} ${currency}` : "",
statusUrl ? `View your order: ${statusUrl}` : "",
"",
"Thank you,",
EMAIL_FROM_NAME
].filter(Boolean).join("\n");
const safeStatusLink = statusUrl
? `<p><a href="${escapeHtml(statusUrl)}">View your order status</a></p>`
: "";
const html = `
<p>Hi ${firstName},</p>
<p>We received payment for order <strong>${orderName}</strong>.</p>
${amount && currency ? `<p>Order total: <strong>${amount} ${currency}</strong></p>` : ""}
${safeStatusLink}
<p>Thank you,<br>${escapeHtml(EMAIL_FROM_NAME)}</p>
`;
const volaneaResponse = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${VOLANEA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": eventId
},
body: JSON.stringify({
from: {
email: EMAIL_FROM,
name: EMAIL_FROM_NAME
},
to: [
{
email: customer.email,
name: [customer.first_name, customer.last_name].filter(Boolean).join(" ")
}
],
subject: `Payment received for order ${order.name}`,
html,
text
})
});
const responseBody = await volaneaResponse.text();
if (!volaneaResponse.ok) {
console.error("Volanea send failed", {
status: volaneaResponse.status,
eventId,
responseBody
});
return res.status(502).json({
error: "Email provider request failed"
});
}
return res.status(200).json({
ok: true,
event_id: eventId
});
});
app.listen(process.env.PORT || 3000, () => {
console.log("Webhook receiver is listening");
});
What makes this code production-oriented
The endpoint does more than relay a request. It creates safeguards around a customer-facing action.
First, it requires a shared secret. Without that check, anyone who finds the endpoint could submit a payload and cause outbound email.
Second, it returns 400 for malformed input and 401 for invalid credentials. Those statuses tell Flow that the configuration or request is wrong, rather than creating a false success.
Third, it uses Idempotency-Key: eventId. This matters because Flow may resend a request after a timeout or configured retry condition. A retry should represent the same logical email, not a second message in the customer’s inbox.
Fourth, it escapes dynamic HTML. Customer names, order names, and similar fields are data—not trusted markup. Treating them as HTML without encoding can produce malformed messages or potentially introduce unwanted content.
Fifth, it sends both html and text. A plain-text alternative improves compatibility and gives recipients a usable version when HTML rendering is unavailable or undesirable.
Test the request before enabling the workflow
Do not start by sending to real customers. Test the endpoint and the Volanea message independently, then test the complete Flow.
Test the receiver with curl
Run the receiver locally or deploy it to a staging endpoint. Then send a representative request:
curl -X POST https://email-worker.example.com/webhooks/shopify/order-paid \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: replace_with_your_shared_secret" \
--data '{
"event":"order.paid",
"event_id":"shopify-order-paid-gid-123456",
"shop":"example-store.myshopify.com",
"order":{
"id":"gid://shopify/Order/123456",
"name":"#1042",
"total_price":"48.00",
"currency":"USD",
"status_url":"https://example-store.myshopify.com/orders/status/example"
},
"customer":{
"email":"you@example.com",
"first_name":"Alex",
"last_name":"Morgan"
}
}'
A 200 response means the receiver accepted the payload and Volanea accepted the send request. Then check the Volanea send activity and your test inbox.
Test the same payload twice with the same event_id. The expected result is one logical email send, not two. This is the simplest practical test of your idempotency design.
Test in Shopify Flow
Once the endpoint works, use a test order or a safe test condition in Shopify Flow. Confirm all of the following:
- The trigger fires only after the intended business event.
- Flow’s execution log shows a successful HTTP action.
- The endpoint receives the expected
event_idand order number. - Volanea records the message as accepted for sending.
- The message arrives at the intended test address.
- The sender domain and visible From name are correct.
- The order link works and does not expose inappropriate data.
If an email arrives but contains blank fields, inspect the Flow execution and verify the variable paths available for the trigger you selected. Shopify Flow exposes variables based on workflow context, so an order-paid trigger and a fulfillment trigger do not necessarily provide identical paths.
Make retries and duplicates safe
Transactional email can be harmful when it duplicates. A customer may tolerate one receipt-like update; two or three copies often create confusion and support tickets.
Shopify Flow can wait for a response and retry HTTP actions when errors or timeouts occur. A network timeout has an important ambiguity: your endpoint may have successfully called Volanea, but Flow may not have received the endpoint’s 200 response before the connection closed.
That is exactly why the idempotency key must be stable across attempts.
Use an event-specific key
For the order-paid example, shopify-order-paid-{{order.id}} is appropriate if the business rule is one email per order-paid event. Do not use a random UUID generated on every request, because each retry would look like a new send.
For another use case, choose an identifier that represents the business action:
fulfillment-shipped-{{fulfillment.id}}for one email per fulfillment.customer-review-needed-{{customer.id}}-{{date}}for a controlled scheduled reminder.backorder-notice-{{order.id}}-{{line_item.id}}for one notice per affected line item.
The point is not the string format. The point is that the key must remain identical when the same event is retried and must change when a genuinely distinct email should be sent.
Return status codes deliberately
Your endpoint’s status code affects what Flow does next.
Return a 2xx response only after Volanea has accepted the send request. If the provider request fails with a temporary problem, return a 5xx response so the Flow action can follow its retry configuration.
Return a 4xx response for permanently invalid input, such as a missing recipient email or an unexpected event label. Retrying invalid JSON will not repair it.
Avoid returning 200 merely because your server received the request. That acknowledges the event before the actual email action is complete, making silent delivery failures more likely.
Use templates when the email becomes more complex
The inline HTML example is intentionally direct. It is useful for a first implementation or a message with only a few variables. As the message becomes more branded, localized, or frequently edited, move presentation into an email template.
Volanea templates support placeholders resolved from variables supplied at send time. A template can hold the reusable subject, preheader, sender settings, and email body while your webhook sends customer- and order-specific values.
This changes the responsibility split:
- Shopify Flow decides when the event should happen.
- Your receiver validates the event and assembles safe variables.
- The Volanea template controls reusable email presentation.
That division is especially helpful when a marketing or operations team needs to update wording without asking an engineer to edit JavaScript. It also reduces the chance that a minor style edit changes webhook behavior.
Do not confuse a transactional template with a marketing campaign. A message triggered by a paid order, fulfillment, password event, or service status should be narrowly tied to that event. Promotional content and optional offers need separate consent and compliance consideration.
Deliverability and data-handling considerations
A working API request is not the same as a reliable customer-email program. Transactional messages deserve the same care as any other customer-facing system.
Keep the purpose obvious
The subject should state the operational reason for the email. For this example, Payment received for order #1042 is clearer than a vague branded subject such as Good news from Example Store.
The sender identity should be stable. If your store uses orders@notify.example.com, do not alternate between unrelated From addresses unless there is a clear operational reason. Consistent identity helps customers recognize genuine communication and makes support investigations easier.
Do not mix promotional content into critical messages by default
An order update is usually expected communication. Adding a large promotional block, unrelated product pitch, or campaign-style tracking can change the recipient’s experience and potentially affect how the message is classified under applicable marketing rules.
Keep the transactional core useful even if you include a modest recommendation or support link. The email should still make sense to someone who did not consent to promotional mail.
Send the minimum data necessary
The Flow payload in this guide avoids full shipping addresses, item-level SKU details, payment identifiers, and customer metadata. That is deliberate.
Every additional field creates another place where sensitive information can appear: Flow logs, application logs, error reports, traces, queues, or provider-side metadata. Add fields only when the email requires them.
Verify recipient addresses upstream when appropriate
Checkout-collected addresses are not always error-free. For workflows involving manual entry, B2B onboarding, imports, or non-customer contacts, validate addresses before initiating an important email sequence. You can use the free email address verification tool during data cleanup or integrate verification into your own intake process.
Validation cannot guarantee inbox placement, but it can catch obvious syntax and domain problems before an address becomes part of automated sending.
Common implementation mistakes
Most failed integrations are not caused by the HTTP request itself. They come from an unclear event definition, weak secret handling, or retry behavior that was never tested.
Sending on the wrong Shopify event
If an order-created trigger sends a message saying payment was received, customers can receive inaccurate information for pending-payment or manual-payment orders. Match content to the trigger’s confirmed state.
Treating the Flow payload as a fixed Shopify schema
The Send HTTP request body is customizable. If your code expects customer.email but the Flow body sends email, the endpoint will reject the request. Treat the JSON body as an explicit versioned contract you own.
When changing the body, update the receiver and workflow together. For a high-volume store, consider supporting both a v1 and v2 route during migration.
Exposing the Volanea key in Flow or frontend code
A Volanea API key authorizes email sending. Keep it in server-side secrets. Shopify Flow’s secret is appropriate for authenticating Flow to your receiver; it does not remove the value of keeping provider credentials in your own controlled environment.
Ignoring duplicate sends
Retries are normal in distributed systems. A random idempotency key, or no idempotency key at all, turns transient errors into duplicate customer messages. Use a deterministic key and test it.
Trusting dynamic fields as HTML
Names, product titles, notes, and order labels should be escaped before interpolation into HTML. If you need rich product descriptions or custom notes, sanitize them with an allowlist rather than inserting raw content.
Returning too slowly
Shopify Flow waits a maximum of 30 seconds for an HTTP response. Keep the endpoint focused. If your workflow needs slow database work, extensive enrichment, or batch processing, acknowledge a safely queued job only after it is durably stored, then let a worker send the email with the same idempotency key.
When a direct Flow-to-API call is reasonable
For a narrowly scoped internal alert, you might send a direct HTTP request from Flow to an API without an intermediate service. That can be acceptable when the payload is simple, security requirements are understood, the response is quick, and the workflow is easy to audit.
The receiver approach is stronger when any of the following are true:
- You email customers rather than only internal staff.
- You need input validation or HTML escaping.
- You need stable idempotency across retries.
- You need to enrich Shopify data with data from another system.
- You want reusable templates or localization.
- You need structured logging, monitoring, and alerting.
- Multiple Shopify workflows will send through the same Volanea account.
In other words, direct calls optimize for fewer moving pieces. A receiver optimizes for control. Customer-facing transactional mail normally justifies that control.
Operational monitoring after launch
Treat this workflow as production infrastructure. Watch the full path, not just one dashboard.
At minimum, monitor:
- Shopify Flow failures: a failed action means the event may not have reached your receiver.
- Webhook authentication failures: unexpected
401responses can indicate a secret mismatch or unwanted traffic. - Validation failures: repeated
400responses often mean a Flow variable changed or a workflow is firing on records without email addresses. - Volanea API failures:
5xxresponses, rate-related responses, or authentication errors should alert an owner. - Delivery events: accepted API requests are not identical to inbox placement. Review bounces, complaints, suppressions, and delivery outcomes.
Include the event ID in logs at every step. If support asks whether order #1042 received an email, a single event identifier makes it possible to trace the Flow execution, receiver request, API request, and provider result.
Avoid logging complete payloads indefinitely. Redact email addresses where practical, set retention limits, and restrict log access. Transactional email systems often contain personally identifiable information even when they do not store payment data.
Conclusion
To send transactional email from Shopify using Volanea, do not look for a native app connection that does not exist. Use Shopify Flow’s HTTP request action to send a deliberately small JSON event to a secure endpoint, then let that endpoint call Volanea’s REST API with server-side credentials.
The result is a flexible integration that respects the strengths of each system: Shopify detects commerce events, Flow orchestrates the trigger, your endpoint applies business and security rules, and Volanea handles transactional email sending. Start with one low-risk workflow, use a deterministic idempotency key, test retries, and expand only after you can trace every send end to end.
FAQ
Does Volanea have a native Shopify app integration?
No. This guide uses an HTTP integration pattern: Shopify Flow sends an event to your endpoint, and your endpoint sends the email through Volanea’s REST API.
Can Shopify Flow send the request directly to Volanea?
It can make HTTP requests, but a small server-side receiver is generally safer for customer-facing messages because it protects provider credentials, validates data, escapes HTML, and handles idempotency consistently.
What payload does Shopify Flow send to the webhook?
The Send HTTP request action sends the body you configure. The JSON payload in this guide is a recommended contract for an order-paid workflow, not a fixed universal Shopify webhook payload.
How do I stop duplicate transactional emails?
Create a stable event ID in Flow, such as shopify-order-paid-{{order.id}}, and pass it as Volanea’s Idempotency-Key header. Reuse the same key when a webhook is retried.
Which Shopify plans can use Send HTTP request in Flow?
Shopify documents the Send HTTP request action as available on Grow, Advanced, and Plus plans. Check your current Shopify plan and Flow action availability before designing the workflow.