Sending an order email at the right moment should be reliable, not dependent on a storefront browser session. This guide shows how to send email from VTEX with Volanea by using VTEX Order Management System (OMS) webhooks, a secure middleware endpoint, and Volanea’s REST email API.

Volanea does not provide a native VTEX Marketplace app or plug-in. That matters because a VTEX OMS webhook is an event notification, not an email-template editor or a general-purpose payload-mapping tool. The robust integration is therefore: VTEX emits an order-status notification, your server receives it, retrieves authoritative order data from VTEX, applies your business rules, and submits an email request to Volanea.

What the VTEX-to-Volanea architecture looks like

The integration has four deliberate parts:

  1. VTEX OMS detects that an order has reached a selected status, such as payment-approved or invoiced.
  2. An OMS webhook sends a small status-change notification to an HTTPS endpoint you operate.
  3. Your middleware validates the event, fetches the full order from the VTEX Orders API, prevents duplicate processing, and renders the message data.
  4. Volanea accepts the final transactional-email request and handles the sending infrastructure and delivery lifecycle.

This extra service is not needless complexity. It creates a boundary between a commerce event system and an email system. The webhook should say, in effect, “order X changed state.” Your middleware decides whether that event deserves an email, who should receive it, what should appear in it, and whether the event was already handled.

An order-status webhook is a good fit for order confirmations, payment-approved notices, fulfillment updates, invoices, shipment notifications, cancellation messages, and post-delivery review requests. It is not a substitute for the storefront’s immediate checkout experience. For example, do not make a shopper wait for a webhook and an external email call before displaying an on-screen order confirmation.

The concrete VTEX trigger: an OMS order-status change

VTEX OMS supports webhook configurations that filter order notifications by status. The triggering business event is an order status change. Choose the status based on the promise your email makes, rather than choosing the earliest possible event by default.

For a paid-order email, use payment-approved. At that point, payment approval has occurred, but warehouse handling may not have begun. For an operational “we are preparing your items” email, use ready-for-handling. For a shipment-oriented message, an invoiced event is often more appropriate because invoice and tracking-related data can be available then.

Pick one event per customer promise

A practical initial policy might be:

  • payment-approved: send a payment-received or order-confirmed message.
  • ready-for-handling: send a preparation update only if it adds useful information beyond the confirmation.
  • invoiced: send shipping or invoice communication after validating the order’s package and tracking fields.
  • canceled: send a cancellation message, with support information and any relevant refund expectation.

Avoid configuring the same customer-facing email on several intermediate statuses. An order can move through multiple OMS states, and a customer who receives three near-identical confirmations will experience that as a defect even if every API call technically succeeds.

OMS webhook filtering also lets you keep the event stream narrow. Start with one status and one email type. Once idempotency, logging, consent rules, and templates are proven, add another status deliberately.

What VTEX sends to your webhook

An OMS webhook notification is intentionally compact. It identifies the order and the state transition; it is not a complete checkout or order-detail document. A handler should treat the notification as a prompt to retrieve the current order, not as the sole source of truth for the message.

A typical order-status notification has this shape:

{
  "OrderId": "v12345678store-01",
  "State": "payment-approved",
  "LastState": "waiting-for-authorization",
  "LastChange": "2026-10-16T14:24:51.0000000+00:00",
  "LastChangeUtc": "2026-10-16T14:24:51.0000000+00:00",
  "CurrentChange": "2026-10-16T14:24:51.0000000+00:00"
}

Field capitalization is significant: OMS notifications use names such as OrderId and State. Store the received body for troubleshooting, but do not assume that optional order information exists in it. In particular, a webhook notification does not give you a safe basis for building line items, customer name, delivery address, or a checkout link.

After receiving OrderId, call the VTEX Orders API for the order details. The order response contains the richer data your message needs, including the client profile, item list, totals, shipping data, and status. Exact availability can differ according to the order’s origin, privacy controls, and the fields populated at checkout, so validate every field before rendering.

Why an order lookup is essential

The status notification and the order document serve different purposes. The notification tells you that an event happened. The order lookup provides the business record you must validate.

That distinction prevents several errors:

  • A customer email address may be absent or not usable for a particular order flow.
  • Item quantities and prices must come from the authoritative order data, not from values cached in a prior event.
  • A late or repeated webhook can arrive after the order changed again.
  • Shipping data may not be ready at payment approval.

Your handler should compare the fetched order’s current status with the webhook’s State when the message requires that exact state. If the order has moved beyond it, decide explicitly whether to send, skip, or use the newer state. Do not silently assume webhook arrival order is business-event order.

Configure the VTEX OMS webhook

Create the OMS hook using VTEX’s Orders API webhook configuration for the account and environment where the orders are processed. Set the filter to the chosen status and set the hook URL to a public HTTPS endpoint controlled by your integration.

The hook URL should be specific, such as https://email.example.com/webhooks/vtex/oms, rather than a broad application endpoint. Use a separate endpoint or a clear event discriminator if the same service receives other providers’ webhooks. This makes logs, rate limits, monitoring, and incident response much easier to manage.

Your endpoint must return a successful response promptly after it has durably accepted the event. “Durably accepted” generally means you have written an idempotency record or queued a job; it does not mean the email has already been delivered. If you render templates, call multiple APIs, and wait for Volanea synchronously before responding, transient slowness can turn into a retry problem.

Test with an order status that matches the filter

A common setup mistake is testing with an order in a different OMS state and concluding that the hook is broken. Verify the configured status spelling, the account/environment, and whether the order actually reached that state.

Use a non-production endpoint first and log only the event metadata necessary for testing. Order records can contain personally identifiable information. Production logs should redact recipient addresses, postal addresses, API credentials, and payment-related fields.

VTEX’s webhook feature sends an HTTP notification; it does not transform that notification into Volanea’s email-request schema. That is why sending directly from an OMS hook to a mail API is not the recommended design, even if a destination endpoint happens to accept arbitrary HTTP requests.

Build the secure middleware handler

The example below uses Node.js and Express. It accepts the actual OMS notification fields, makes an authenticated order lookup, maps relevant order fields to an email, and sends through Volanea. Replace the placeholders with your account-specific values and use your production secret manager rather than a local .env file in deployment.

import express from 'express'

const app = express()
app.use(express.json({ limit: '256kb' }))

const VTEX_ACCOUNT = process.env.VTEX_ACCOUNT
const VTEX_ENVIRONMENT = process.env.VTEX_ENVIRONMENT || 'vtexcommercestable'
const VTEX_APP_KEY = process.env.VTEX_APP_KEY
const VTEX_APP_TOKEN = process.env.VTEX_APP_TOKEN
const VOLANEA_API_KEY = process.env.VOLANEA_API_KEY
const FROM_EMAIL = 'orders@example.com'
const FROM_NAME = 'Example Store'

// Persist this in Redis, Postgres, or a queue in production.
const processed = new Set()

app.post('/webhooks/vtex/oms', async (req, res) => {
  const { OrderId, State, LastState, LastChangeUtc } = req.body ?? {}

  if (!OrderId || !State) {
    return res.status(400).json({ error: 'Missing OrderId or State' })
  }

  // One event key per order and message-producing state.
  const eventKey = `${OrderId}:${State}`
  if (processed.has(eventKey)) {
    return res.status(204).end()
  }

  // Claim before sending. A database unique constraint is safer than a Set.
  processed.add(eventKey)

  try {
    const orderUrl = `https://${VTEX_ACCOUNT}.${VTEX_ENVIRONMENT}.com.br/api/oms/pvt/orders/${encodeURIComponent(OrderId)}`
    const orderResponse = await fetch(orderUrl, {
      headers: {
        'X-VTEX-API-AppKey': VTEX_APP_KEY,
        'X-VTEX-API-AppToken': VTEX_APP_TOKEN
      }
    })

    if (!orderResponse.ok) {
      throw new Error(`VTEX order lookup failed: ${orderResponse.status}`)
    }

    const order = await orderResponse.json()
    const recipient = order.clientProfileData?.email
    const firstName = order.clientProfileData?.firstName || 'there'

    // Never send a payment-approved message for a different current status
    // without making that business rule explicit.
    if (State !== 'payment-approved' || order.status !== 'payment-approved') {
      return res.status(204).end()
    }
    if (!recipient) {
      console.warn('No recipient email', { OrderId, State, LastChangeUtc })
      return res.status(204).end()
    }

    const items = (order.items || []).map((item) => ({
      name: item.name,
      quantity: item.quantity,
      priceMinor: item.price
    }))

    const itemRows = items.map((item) =>
      `<li>${escapeHtml(item.name)} × ${item.quantity}</li>`
    ).join('')

    const emailPayload = {
      from: { email: FROM_EMAIL, name: FROM_NAME },
      to: [{ email: recipient, name: firstName }],
      subject: `Payment received for order ${order.orderId || OrderId}`,
      html: `<p>Hi ${escapeHtml(firstName)},</p>
        <p>We received payment for order <strong>${escapeHtml(order.orderId || OrderId)}</strong>.</p>
        <ul>${itemRows}</ul>
        <p>Thank you for shopping with us.</p>`
    }

    const sendResponse = await fetch('https://api.volanea.com/v1/emails', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${VOLANEA_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(emailPayload)
    })

    if (!sendResponse.ok) {
      const detail = await sendResponse.text()
      throw new Error(`Volanea send failed: ${sendResponse.status} ${detail}`)
    }

    return res.status(202).json({ accepted: true, orderId: OrderId })
  } catch (error) {
    processed.delete(eventKey) // permit a controlled retry after a failure
    console.error('VTEX OMS email processing failed', { OrderId, State, error: error.message })
    return res.status(500).json({ error: 'Event processing failed' })
  }
})

function escapeHtml(value = '') {
  return String(value).replace(/[&<>'"]/g, (char) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;'
  }[char]))
}

app.listen(3000)

The field mapping is explicit: OrderId selects the order to retrieve; clientProfileData.email becomes the recipient; clientProfileData.firstName becomes a safe greeting fallback; and each object in order.items becomes an item row. The price field is commonly represented in minor currency units in VTEX order data, so format currency only after confirming the order’s currency and pricing conventions for your store.

Before shipping, compare the API request fields and endpoint with the current API reference and setup guidance. Keep template construction separate from delivery code as the integration grows; it will make localized content, invoice messaging, and test coverage far easier.

Keep the Volanea API key out of the storefront

The Volanea API key belongs only in the server-side runtime that sends the email. In this architecture, that is the middleware’s secret store or protected environment-variable configuration—not a VTEX storefront app, checkout customization, JavaScript bundle, repository, or client-visible site setting.

A browser-visible key is effectively public. Anyone who can inspect page source, browser network requests, a source map, a deployed app artifact, or a leaked configuration object could use it to send mail as your account. That can damage sender reputation, expose customer data through templates, and create direct sending costs.

Separate credentials by responsibility

Your handler normally needs two credential sets:

  • A VTEX AppKey and AppToken with only the permissions needed to retrieve the relevant orders or configure the hook.
  • A Volanea API key scoped to the environment and sender identity used for the transactional stream.

Store both in a managed secret facility offered by your hosting platform. Grant production secrets only to the production service identity. Rotate keys when staff access changes or when an exposure is suspected, and make rotation routine by supporting a brief overlap of old and new credentials where your platform allows it.

Do not attempt to place the Volanea key in an OMS webhook header and point the webhook directly at Volanea. Even aside from the incompatible payload, this couples a reusable sending credential to a webhook configuration and gives you no safe place to look up order data, enforce idempotency, or apply consent rules.

Make email content correct, useful, and safe

Transactional messages should reflect the state that caused them. A payment-approved message can confirm that payment was received and list the order reference. It should not promise dispatch time unless your fulfillment data and policy support that promise.

Use a stable, customer-safe order identifier. In many stores, the VTEX orderId is appropriate. Avoid exposing internal payment IDs, gateway references, or values that a support agent would not normally share with a shopper.

Render from trusted fields

HTML email has the same output-encoding concern as a web page. Product names, customer names, and custom text can contain characters that break markup. Escape dynamic text as the example does, or use a template engine with contextual HTML escaping enabled.

Treat the order record as sensitive. A basic confirmation usually needs a first name, order ID, a summary of items, and an account or order-tracking destination. It rarely needs the complete shipping address, phone number, tax information, or payment details. Sending less data reduces the impact of a forwarding mistake or inbox compromise.

If you send marketing follow-ups after delivery, keep them distinct from operational email. Transactional necessity does not automatically establish permission for promotions. Apply your consent model, suppression data, and regional privacy obligations before adding campaign content.

When this breaks: failures specific to this hop

The fragile point in this design is not just whether either platform is online. It is the handoff between a status event, an order lookup, and a send request. Design for each failure mode before the first high-volume sale.

VTEX retries can create duplicate emails

A webhook sender may retry when your endpoint times out, returns a non-success status, or loses its response after doing work. Network failures can also produce the classic ambiguity: your middleware sends the email successfully, but the caller never receives the response and tries again.

Use an idempotency record keyed by at least OrderId and the message-producing State, such as v12345678store-01:payment-approved. Insert that key under a database unique constraint before enqueueing or sending. A process-local Set, shown only for clarity in the code sample, is not sufficient across restarts or multiple instances.

For more precision, record a message type as well: OrderId:payment-approved:payment-received-v1. This lets you intentionally introduce a revised message type later without reopening every historic event. Save the Volanea response identifier alongside the key so support staff can trace a shopper report without searching raw logs.

Webhook timeouts can turn slowness into retries

Do not make the webhook request wait on rendering, VTEX order lookup retries, and an external email API call if you can avoid it. The more work performed before the response, the more likely a temporary dependency slowdown becomes a webhook timeout.

The stronger pattern is to validate the small event, write it to a durable queue or database transaction, return success promptly, and let a worker do the lookup and send. The worker can use bounded retries with exponential backoff. It can also route repeated failures to a dead-letter queue for human review.

If you use a synchronous first version, set conservative HTTP timeouts for both the VTEX lookup and Volanea request, log timeout categories separately, and ensure the idempotency state is not permanently marked complete before a confirmed send.

Some payload fields will be missing

The webhook itself is deliberately limited, and full order records are not guaranteed to contain every field your template expects. Guest checkout, incomplete profile data, marketplace-originated orders, custom checkout changes, and the selected OMS state can all affect which information is present.

Build fallbacks. Use “there” when first name is absent. Skip an optional tracking block until a tracking number exists. Do not construct a message with undefined product names or an empty recipient. If no valid email address is available, mark the event as intentionally skipped and expose that outcome in operations reporting.

State drift and out-of-order delivery need a policy

An event for payment-approved can arrive after the order has already advanced to invoiced, or after cancellation. Fetching the current order protects you from acting on stale assumptions, but you still need a business rule. You may send the payment confirmation regardless of later progression, skip it if cancellation happened quickly, or send a cancellation-specific message instead.

Write those rules down and test them with real order timelines. The correct decision depends on what your brand promised, not merely on which event arrived first.

Test before enabling production emails

Use a staging VTEX account or controlled test order flow where possible. Configure a verified non-customer sender in Volanea and send only to internal test recipients during the first pass.

Test at least these cases:

  1. An ordinary order reaches payment-approved and produces one message with the correct recipient, order ID, and item summary.
  2. The identical webhook is delivered twice and produces one message.
  3. The webhook body lacks an expected optional value, and the handler skips or falls back safely.
  4. VTEX order lookup returns an error or times out, and the event is retried without losing it.
  5. Volanea returns a non-success response, and the worker retries according to policy without declaring success.
  6. An order changes to a later state before processing, and the configured state-drift rule is applied.

Monitor counts across the pipeline: OMS events received, events rejected, events deduplicated, order lookups failed, messages accepted by Volanea, and messages intentionally skipped. A sudden difference between payment-approved events and accepted sends is often the fastest signal of a mapping or permission regression.

Operational improvements after the first integration

Once one order-status email is stable, improve the system without expanding risk too quickly. Version templates and include the version in your event record. Separate delivery jobs from message composition. Add structured logs with an internal correlation ID, the VTEX order ID, event state, and Volanea message identifier—but not full customer payloads.

For high order volume, queues provide backpressure. They let OMS events be acknowledged quickly while a controlled number of workers call the Orders API and Volanea. This protects both external APIs during a sale, and it gives you a clear place to retry transient 429 and 5xx responses.

Also separate transactional and campaign streams in your operational design. Order messages have strict timeliness requirements and should not be delayed by a large marketing import or a campaign template issue. The same discipline improves deliverability: consistent sender identities, authenticated domains, clean recipient handling, and low error rates all help transactional mail remain dependable.

Conclusion

The dependable way to send email from VTEX with Volanea is not a marketplace installation. It is an OMS order-status webhook connected to a server-side integration that retrieves the order, maps validated fields, deduplicates retries, and sends through Volanea with a protected API key.

Start with one status, one message, and one durable idempotency rule. That small foundation handles the real-world conditions that matter most: delayed notifications, missing fields, changing order states, temporary API failures, and the customer frustration caused by duplicate emails.

FAQ

Can VTEX send a Volanea email directly from an OMS webhook?

Not reliably for a production integration. OMS webhook notifications are compact status events, while a Volanea email request needs recipient, sender, subject, and message content. Use middleware to retrieve the order, map fields, and keep sending credentials private.

Which VTEX status should trigger an order confirmation email?

payment-approved is a common choice when the email confirms successful payment. Use ready-for-handling or invoiced only when the email’s wording matches those later operational milestones.

Where should I store the Volanea API key?

Store it as a secret in the server-side middleware’s protected environment or secret manager. Never place it in storefront JavaScript, checkout settings visible to the browser, a public repository, or a client-side VTEX configuration.

How do I prevent duplicate messages after webhook retries?

Create a durable idempotency key such as OrderId:State:messageType under a database uniqueness constraint. Claim the key before sending, record the send result, and treat later deliveries of the same event as already processed.

Why should the handler fetch the VTEX order after receiving the webhook?

The webhook identifies an order and status change but does not provide the complete, reliable data required for a customer email. The order lookup supplies customer profile data, items, totals, and the current state needed for validation.