Sending transactional email directly from an ERP workflow sounds simple until you account for authentication, retries, customer data quality, and duplicate notifications. This guide shows how to send email from Odoo with Volanea using Odoo Studio’s real outbound webhook action and a small server-side middleware endpoint.
Volanea does not provide a native Odoo app, Odoo Marketplace listing, or one-click connector. The dependable integration is event-driven: an Odoo automation rule watches a record change, Odoo sends a POST payload to middleware you control, and that middleware calls Volanea’s REST API with a server-side secret key.
The integration architecture
The recommended production route has three hops:
- An Odoo record changes in a meaningful way, such as a sales order moving from quotation to confirmed.
- An Odoo Studio automation rule uses Send Webhook Notification to POST selected record fields to a middleware URL.
- Middleware validates the Odoo payload, maps it to Volanea’s
POST /v1/sendrequest, attaches the Volanea API key and an idempotency key, then returns a fast HTTP response to Odoo.
This is not an arbitrary workaround. Odoo Studio automation rules support an outbound Send Webhook Notification action that sends a POST request containing the values of selected fields to a URL. Odoo also presents a Sample Payload preview in the action configuration. (odoo.com)
The key design decision is the middleware layer. Volanea uses a secret API key for authenticated REST requests, while an Odoo Studio webhook action is designed around a destination URL and selected record fields. Keeping the provider credential in middleware means it is never embedded in a webhook URL, a browser-visible JavaScript setting, an Odoo form field, or an automation payload.
Why not send email through Odoo’s normal outgoing mail server?
Odoo can send its own application mail through configured outgoing mail servers, but that is a different integration model. This page is for cases where a business event in Odoo should trigger Volanea’s transactional API: for example, an order-confirmation email with data from sale.order, an account notice after a CRM stage change, or a custom notification after a Studio form submission.
Using the API route gives your middleware one place to add validation, recipient rules, event IDs, logging, and retry protection. It also avoids putting a long-lived Volanea secret into an Odoo Studio configuration that may be viewed or edited by more administrators than should have access to sending credentials.
Volanea’s single-message endpoint is POST /v1/send; it accepts a secret key and supports the Idempotency-Key header for safe retries. (volanea.com)
What Odoo actually provides for outbound HTTP
Odoo Studio includes automation rules that run when a record event occurs. Depending on the model, available triggers include value updates, timing conditions, custom triggers, and more. In kanban workflows, creating an automation from a stage sets the trigger to Stage is set to by default. (odoo.com)
For this integration, use the automation action called Send Webhook Notification. It sends a POST API request to the configured URL with the values of the fields you select. This is outbound from Odoo; it is separate from Odoo’s inbound webhook feature, where an outside system calls an Odoo-generated URL. Odoo’s documentation explicitly notes that an automated action can also send data to an external webhook when something changes inside the Odoo database. (odoo.com)
The concrete trigger: a sales order becomes confirmed
This guide uses the Sales Order model, whose technical model name is sale.order. The business event is a quotation becoming a confirmed sales order:
- Model: Sales Order (
sale.order) - Trigger category: Values Updated
- Watched field:
state - Before Update Domain: quotation state, such as
draftorsent - Apply On Domain: confirmed order state,
sale - Action: Send Webhook Notification
That combination means the email is initiated when the order enters the confirmed state, not every time someone later edits a note, changes a salesperson, or updates a delivery date. Odoo’s automation rules support both before-update and apply-on domain conditions, which is what makes this transition-specific setup possible. (odoo.com)
This trigger is only an example. The same pattern works for a CRM opportunity moving to a stage, an invoice becoming posted, a helpdesk ticket being created, or a custom Studio model record being created. The important rule is to identify one business transition that deserves one email.
Why the trigger should be narrow
A broad trigger is the most common cause of unwanted transactional email. If you configure an automation to run whenever a Sales Order is updated, every ordinary update could send a message: address corrections, price adjustments, internal note changes, or automated recalculations.
Use a state transition plus domains whenever possible. The goal is not merely “send when the record matches this state.” The goal is “send exactly when the record crosses into this state.”
For a confirmation email, this distinction matters:
- A record already in
saleshould not send another confirmation because its invoice address was corrected. - A canceled order should not use the same event key as a re-confirmed order unless that is a deliberate new business event.
- A manually retried webhook must use the same idempotency key as the original confirmation event.
Configure the Odoo Studio webhook action
Open a Sales Order in Odoo, open Studio, and create an automation for the Sales Order model. Set the automation rule to react to the state update described above. Then add an action with the type Send Webhook Notification.
In the action, configure the following values:
- URL: your middleware endpoint, for example
https://integrations.example.com/hooks/odoo/order-confirmed/<random-secret>. - Fields: select the fields middleware needs to create the message.
- Sample Payload: inspect it before enabling the rule in production.
The URL should point to your middleware, not directly to Volanea. The middleware is the component that holds the Volanea key and converts Odoo’s record-oriented JSON into an email-oriented API request.
Add purpose-built fields for a predictable payload
Odoo’s outbound action includes base record information and sends the selected field values. The exact keys in the Sample Payload depend on the model and fields you choose. To avoid fragile logic around relational-field display values, create simple custom text fields on sale.order for this one integration.
For example, add these fields in Studio:
| Odoo field | Suggested technical name | Purpose |
|---|---|---|
| Recipient email | x_volanea_to | Email address to receive the confirmation |
| Email subject | x_volanea_subject | Subject line for the message |
| Email HTML | x_volanea_html | Rendered or prepared HTML body |
| Event key | x_volanea_event_key | Stable unique key for the business event |
| Sender name | x_volanea_from_name | Optional display name for the sender |
Use a formula, automation, custom module, or operational process to populate those fields before the order reaches sale. This makes the outbound webhook contract explicit: Odoo sends strings that are ready for middleware to validate, rather than middleware having to infer email content from nested partner, invoice, and line-item relationships.
The payload Odoo sends
Odoo’s Send Webhook Notification action POSTs selected values and includes record identity fields. With the fields above selected, a representative payload is:
{
"name": "S00042",
"model": "sale.order",
"id": 42,
"x_volanea_to": "alex@example.com",
"x_volanea_subject": "Your order S00042 is confirmed",
"x_volanea_html": "<p>Hi Alex,</p><p>Thanks for your order <strong>S00042</strong>.</p>",
"x_volanea_event_key": "sale-order-42-confirmed-v1",
"x_volanea_from_name": "Example Store"
}
name, model, and id identify the Odoo record; the remaining properties are the selected fields. Your database’s exact field names and values are shown in Odoo’s Sample Payload preview, so treat that preview as the source of truth before deploying. Odoo documents the Sample Payload specifically as a preview of the data in the outbound request, populated from a random record or dummy data when no record is available. (odoo.com)
Do not assume a relational field will arrive as the plain email address you want. A partner field can be represented differently from a scalar text field, and the serialization can vary with the selected model field. Sending dedicated scalar fields eliminates that ambiguity.
Map the Odoo payload to Volanea’s send endpoint
The middleware below uses Node.js with Express. It receives the Odoo JSON payload, verifies required fields, maps the fields into the Volanea format, and calls https://api.volanea.com/v1/send.
The code intentionally uses a deterministic idempotency key based on the Odoo business event. If Odoo retries the same webhook because of a network timeout, the middleware sends the same Idempotency-Key, allowing Volanea to recognize the retry as the same logical email request rather than a new message.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.use(express.json({ limit: "256kb" }));
const {
ODOO_WEBHOOK_SECRET,
VOLANEA_API_KEY,
VOLANEA_FROM_EMAIL
} = process.env;
if (!ODOO_WEBHOOK_SECRET || !VOLANEA_API_KEY || !VOLANEA_FROM_EMAIL) {
throw new Error("Missing ODOO_WEBHOOK_SECRET, VOLANEA_API_KEY, or VOLANEA_FROM_EMAIL");
}
app.post("/hooks/odoo/order-confirmed/:secret", async (req, res) => {
// Keep the Odoo endpoint private. Do not put the Volanea key in this URL.
if (!crypto.timingSafeEqual(
Buffer.from(req.params.secret),
Buffer.from(ODOO_WEBHOOK_SECRET)
)) {
return res.status(404).json({ error: "Not found" });
}
const {
model,
id,
name,
x_volanea_to: to,
x_volanea_subject: subject,
x_volanea_html: html,
x_volanea_event_key: eventKey,
x_volanea_from_name: fromName
} = req.body;
// Validate the Odoo event contract before any provider call.
if (model !== "sale.order") {
return res.status(422).json({ error: "Unexpected Odoo model" });
}
if (!Number.isInteger(id) || !name || !to || !subject || !html || !eventKey) {
return res.status(422).json({
error: "Missing required Odoo fields",
required: [
"id",
"name",
"x_volanea_to",
"x_volanea_subject",
"x_volanea_html",
"x_volanea_event_key"
]
});
}
const volaneaPayload = {
from: {
email: VOLANEA_FROM_EMAIL,
name: fromName || "Example Store"
},
to,
subject,
html,
text: `Your order ${name} is confirmed.`
};
const upstream = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${VOLANEA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `odoo:${model}:${id}:${eventKey}`
},
body: JSON.stringify(volaneaPayload)
});
const responseBody = await upstream.text();
if (!upstream.ok) {
console.error("Volanea send failed", {
status: upstream.status,
odooRecord: `${model}:${id}`,
responseBody
});
// Return a non-2xx status only when a retry could be useful.
return res.status(upstream.status >= 500 ? 502 : 422).json({
error: "Volanea rejected or could not accept the email",
providerStatus: upstream.status
});
}
console.info("Volanea email accepted", {
odooRecord: `${model}:${id}`,
eventKey
});
return res.status(202).json({ accepted: true });
});
app.listen(3000, () => {
console.log("Odoo-to-Volanea middleware listening on port 3000");
});
The field mapping is deliberate:
| Odoo webhook value | Volanea request value | Why it exists |
|---|---|---|
x_volanea_to | to | Recipient address |
x_volanea_subject | subject | Transactional subject line |
x_volanea_html | html | Rich message content |
x_volanea_from_name | from.name | Optional sender display name |
VOLANEA_FROM_EMAIL | from.email | A verified sending address controlled by deployment configuration |
model, id, x_volanea_event_key | Idempotency-Key | Stable identity for safe retry handling |
Volanea supports sending to one address or up to 50 recipients through this endpoint, and its send pipeline includes suppression checks, contact handling, template rendering where applicable, tracking instrumentation, and dispatch. (volanea.com)
For current request options, template fields, attachment support, and response details, use the email API reference and setup guides rather than copying an integration snippet unchanged into a different message type.
Store the Volanea API key safely
The Volanea API key is a server credential. It must never be stored in:
- A JavaScript bundle delivered to an Odoo website visitor.
- A public Odoo controller response.
- An email template variable.
- The outbound webhook URL query string.
- A record field that ordinary users can export.
- A client-visible configuration page.
In the Studio webhook architecture
With the standard Odoo Studio webhook route, the Volanea key belongs in the middleware deployment’s secret store as VOLANEA_API_KEY. That can be a platform secret, container secret, encrypted deployment variable, or equivalent server-only configuration mechanism.
In this design, Odoo stores only the middleware URL. The middleware stores the Volanea key and the Odoo webhook path secret. This separation is useful because rotating the Volanea credential does not require editing an Odoo automation rule, and an Odoo administrator does not need access to the provider key to maintain the trigger.
If you build a custom Odoo module instead
A self-hosted Odoo or Odoo.sh team may choose a custom module that sends the Volanea request from Python inside Odoo’s server process. In that case, inject VOLANEA_API_KEY into the Odoo service environment or a deployment secret manager and read it only in server-side code. Do not hard-code it in the module, commit it to source control, or expose it through a settings form.
A custom module can give you more direct control over headers, signing, database transactions, queueing, and delivery-state recording. But it also creates code that must be maintained across Odoo upgrades. For many teams, the Studio webhook plus small middleware endpoint is the lighter and safer operational boundary.
Verify the sending domain before testing
Use a From address under a domain you have verified in Volanea. An unverified sending domain can cause the API to reject the send request rather than accept it for delivery. Volanea’s integration guidance also calls out verified domains as a requirement for sending. (community.strapi.io)
Choose a stable sender such as orders@yourdomain.com or notifications@yourdomain.com, not a customer’s address. If customers should reply to the message, configure a supported reply-to value in your API request or build a separate inbound handling process.
Before sending real order confirmations:
- Verify the sending domain in Volanea and publish the required DNS records.
- Send a direct API test from middleware with a controlled recipient.
- Trigger the Odoo automation against a test Sales Order.
- Inspect the middleware log for the received Odoo payload and the provider response status.
- Confirm the message appears in Volanea’s email activity records.
Use a dedicated test recipient and a non-production Odoo database while developing. Odoo recommends configuring and testing webhooks using a duplicate database before implementing them in a live database. (odoo.com)
Design the email content for an ERP event
The email copy should reflect what Odoo has actually committed. For an order confirmation, include the order reference and only information that is stable when the order enters the confirmed state.
A simple body might contain:
- Customer name.
- Odoo order reference, such as
S00042. - Confirmation status.
- Order total and currency if those fields are finalized.
- A link to the customer portal, if available.
- Support contact details.
Avoid including every order line in a webhook-built HTML field if the email can become large or if line data changes after confirmation. A better long-term approach is to have middleware fetch or receive a carefully prepared order summary, then render a versioned template. The business event should remain stable even if the presentation changes.
Inline HTML versus stored templates
The example sends html directly because it is easy to understand and maps cleanly from an Odoo field. For a single notification, that can be appropriate.
For a growing email program, use a stored template and pass variables from middleware instead. That separates delivery logic from copy changes, lets marketing or operations review a template without modifying an Odoo automation rule, and makes it easier to reuse the same layout for order confirmations, invoice notices, and shipment updates.
Whichever path you choose, do not let an unrestricted user-entered HTML field become your production email template without review and sanitization. Internal ERP data is still content that can be malformed, unexpectedly large, or unsafe to inject into HTML.
When this breaks: failures specific to the Odoo webhook hop
A production integration must be designed around the fact that Odoo, middleware, and Volanea are separate systems. A successful email API design is not just a successful POST request in a test tool.
Odoo retries after a timeout and sends duplicates
The most dangerous failure looks like this: middleware sends the email successfully, but the HTTP response does not reach Odoo before a timeout. Odoo may retry the webhook even though the first email was accepted.
Without idempotency, the recipient may receive two order confirmations. The fix is to derive the Idempotency-Key from the logical business event, not from the individual HTTP attempt. In the example, odoo:sale.order:42:sale-order-42-confirmed-v1 stays identical for all retries of that one confirmation.
Do not generate a random UUID inside the request handler on every attempt. That would convert every retry into a new send operation and defeat deduplication. Volanea documents idempotency keys specifically as a way to recognize retries and avoid repeating side effects. (volanea.com)
The webhook waits too long for middleware
Odoo’s automation is waiting for the webhook response. If middleware performs slow template rendering, multiple external lookups, PDF generation, or a long queue operation before replying, the Odoo side can time out or classify the call as failed.
Keep the synchronous path small:
- Authenticate the webhook request.
- Validate the JSON fields.
- Build the Volanea request.
- Call Volanea, or enqueue a durable job with the same event key.
- Return a concise response.
For higher-volume workflows, write the event to a durable queue first and return an acknowledgement only after the queue write succeeds. The queue worker can then call Volanea with the persisted idempotency key. That pattern increases resilience, but it also requires monitoring the queue and handling permanent provider errors.
Fields are absent or have unexpected shapes
Some Odoo fields may be empty on some records, hidden from the chosen context, unavailable because a module is not installed, or represented differently when they are relational fields. This is why the middleware returns 422 when required values are missing.
Do not substitute an empty recipient with a generic fallback address in production. That can leak customer order data. Reject the event, log only the record identity and missing field names, then correct the Odoo data or automation field selection.
This is also why the Odoo Sample Payload matters. Odoo identifies sale.order as the technical model name for Sales Order examples, and developer mode helps expose the technical names of models and fields needed for payload configuration. (odoo.com)
A bad record transition causes a valid but wrong email
An automation rule can be technically correct while being business-incorrect. For example, a condition based only on state = sale could send when an already confirmed order is edited. The request will be valid, Volanea will accept it, and the customer will still receive an unnecessary email.
Use both the before-update and after-update domains. Then test these cases deliberately:
- A quotation moves from draft to confirmed: one email expected.
- A confirmed order is edited: no email expected.
- A quotation is canceled: no email expected.
- A record is manually retried: no duplicate email expected.
- Required custom fields are blank: no provider request expected.
Volanea rejects the request
A 4xx response usually means retrying unchanged data will not fix the issue. Common categories include a missing required email field, malformed recipient address, authentication problem, or an unverified sender domain.
Log the status code, Odoo model, record ID, and safe provider error information. Do not log the API key, complete HTML body, or unnecessary customer data. Alert an operator for permanent failures rather than allowing repeated retries to create noise.
A 5xx response or network error may be transient. Retry it with the same idempotency key and bounded exponential backoff. The retry must remain tied to the exact same event key and payload.
Operational checklist before launch
Use this checklist before enabling the automation on live Sales Orders:
- Confirm the Odoo automation uses
sale.order, astate-change trigger, and transition-specific domains. - Confirm the action type is Send Webhook Notification.
- Compare your real Odoo Sample Payload with middleware field names.
- Use scalar custom fields for email destination, subject, HTML, and event key where practical.
- Put
VOLANEA_API_KEYin middleware or Odoo server deployment secrets, never in client-visible settings. - Use a verified Volanea sender domain and a controlled From address.
- Validate recipient, subject, HTML, model, record ID, and event key before calling Volanea.
- Send a deterministic
Idempotency-Keyfor every logical Odoo email event. - Return quickly from middleware and log safe correlation identifiers.
- Test the transition, edit, timeout/retry, missing-field, and provider-error cases.
Email volume is also worth planning for. A single confirmation per order is straightforward; an automation that triggers a notification for many records can become a sending-cost and queue-management concern. Review transactional email pricing when forecasting volume, environments, and operational limits.
Conclusion
The cleanest way to send email from Odoo with Volanea is not a marketplace plugin because there is no native Volanea Odoo connector to install. Use Odoo Studio’s outbound Send Webhook Notification action to react to a specific record transition, send a compact record payload to middleware, and let middleware make the authenticated Volanea API call.
That boundary solves the practical problems that cause integrations to fail later: it keeps the API key server-side, creates a stable mapping between Odoo fields and email fields, validates incomplete records, and uses idempotency to prevent duplicates when a webhook retry follows a timeout. Start with one narrow business event—such as a Sales Order moving into confirmed status—then expand the same pattern to the rest of your transactional workflows.
FAQ
Does Volanea have a native Odoo app or Odoo Marketplace plugin?
No. This integration uses Odoo Studio automation and an outbound webhook to middleware you control. Middleware then calls Volanea’s REST API.
Can Odoo send a webhook when a Sales Order is confirmed?
Yes. Create an automation rule on the Sales Order model, watch the state field under a Values Updated trigger, restrict it with before-and-after domains, and add the Send Webhook Notification action.
Should the Volanea API key go in the Odoo webhook URL?
No. Never put an API key in a URL, record field, browser configuration, or Odoo Studio payload. Store it as a server-side middleware secret, or as a deployment-injected environment variable if using a custom Odoo module.
How do I stop duplicate email when Odoo retries a webhook?
Generate a stable Idempotency-Key from the Odoo model, record ID, and the specific business event. Reuse that exact key for every retry of the same event.
What should middleware do when Odoo sends incomplete data?
Validate required fields before calling Volanea. Return a clear non-2xx response, log the Odoo record identity and missing field names safely, then fix the record data or Odoo action field selection rather than sending a partial message.