Customer.io to Volanea migration is simplest when you treat it as an email-infrastructure change rather than a search-and-replace exercise. The send call is usually the easy part; preserving authentication, opt-out protections, event handling, template output, and operational confidence is the work that prevents surprises in production.

This guide focuses on developers moving transactional email from Customer.io to Volanea. It covers a staged migration for password resets, receipts, verification emails, alerts, and application notifications. If your Customer.io workspace also runs lifecycle campaigns, in-app messages, push, SMS, complex segmentation, or cross-channel journeys, separate those workloads from the transactional migration plan instead of assuming every feature maps directly.

Start by defining the migration boundary

Before changing code, make an inventory of every place your application sends through Customer.io. A useful first split is between messages initiated directly by application code and messages initiated by Customer.io automation.

Application-triggered transactional messages are typically straightforward candidates for Volanea. Examples include a password reset requested in your API, an account-verification email after signup, a payment receipt after a successful charge, an invitation created by an administrator, or a security alert generated by your authentication service.

Customer.io can also be the system that stores people, receives events, evaluates segments, renders Liquid content, runs campaigns, and coordinates messages across email, push, SMS, in-app, and webhooks. Those are different responsibilities from accepting a transactional email request. Do not silently relocate them just because a transactional send endpoint changes.

Create a message inventory with at least these columns:

  • Message name and business purpose.
  • Current trigger: application code, Customer.io campaign, workflow, broadcast, or manual operation.
  • Sending address and sending domain.
  • Whether the message is legally or operationally transactional.
  • Current Customer.io transactional message ID or trigger name, if applicable.
  • Required dynamic data and its expected types.
  • Template owner: engineering, product, lifecycle marketing, support, or another team.
  • Expected events: accepted, delivered, bounced, complained, opened, clicked, or unsubscribed.
  • Whether an attachment, CC, BCC, AMP content, or a scheduled send is required.
  • Failure behavior: retry, queue, alert, fallback, or user-visible error.

This inventory prevents a common migration mistake: moving the message that is easy to find while overlooking a secondary code path, a background job, or a webhook-driven notification. It also gives support and product teams a shared reference when they compare old and new output.

Understand what changes in a Customer.io to Volanea migration

Customer.io’s transactional API lets an application send an email using a stored transactional message or by providing message content directly. Its request can associate a send with a person through identifiers and can pass message_data for Liquid-based personalization. Customer.io recommends including a transactional_message_id so messages are categorized and reported consistently.

Volanea’s POST /v1/send endpoint accepts a transactional send request and runs it through its sending pipeline, including suppression checking, contact upsert, template rendering when a template is selected, tracking instrumentation, and dispatch. A single request can address up to 50 recipients; use separate sends when recipient-level privacy or content differs. Volanea also supports an Idempotency-Key header for retry-safe sends.

The practical difference is not that one platform can send a receipt and the other cannot. The difference is where you keep the surrounding concerns:

  1. Message definition. Customer.io transactional templates can be selected with a transactional ID or name. In Volanea, you can send fully rendered content from code or use reusable templates addressed by templateId.
  2. Recipient identity. Customer.io’s transactional API expects an identifiers object so the message can be associated with a person. Volanea’s send pipeline can upsert the contact from the send request, but a migration should still decide which system is authoritative for customer data.
  3. Personalization. Customer.io templates use Liquid references such as {{customer.first_name}} and {{trigger.reset_url}}. Volanea templates have their own rendering behavior and variable model, so template output must be tested—not assumed equivalent.
  4. Event processing. Both products expose event and webhook capabilities, but event names, payload fields, signing headers, retry behavior, and IDs are provider-specific.
  5. Suppression enforcement. A safe cutover imports the addresses you must not mail before production traffic reaches the new sender.

The best migration preserves behavior deliberately while allowing the application interface to become simpler where that is useful.

Build a parallel-send plan before cutover

Avoid changing every production message at once. Start with one low-risk, internally observable transactional message—for example, a staging-only test notification or an internal account alert. Then migrate one message family at a time.

A conservative sequence looks like this:

  1. Authenticate the sending domain in Volanea without changing production application traffic.
  2. Send test messages to seed addresses at Gmail, Outlook, iCloud, Yahoo, and a company mailbox.
  3. Configure and verify Volanea webhook delivery in a non-production endpoint.
  4. Import the suppression data required for safe sending.
  5. Move one transactional code path behind a provider adapter or feature flag.
  6. Run controlled production traffic through Volanea while preserving Customer.io as the rollback path.
  7. Compare send acceptance, delivery, bounce, complaint, and support signals.
  8. Migrate the remaining transactional message classes incrementally.
  9. Keep Customer.io enabled long enough to process any outstanding campaign, journey, or delayed-message responsibilities that remain there.

A feature flag is more useful than a one-time environment-variable switch. It lets you route a small percentage of a message type to Volanea, route selected internal accounts to Volanea, or revert a message family without redeploying. For security-sensitive messages such as password resets, use a deterministic rule—such as a controlled allowlist during validation—rather than randomly routing a single user’s retries between providers.

Do not dual-send the same real transactional message to customers merely to compare providers. A customer receiving two reset emails or two receipts creates confusion and can introduce a security risk. Instead, compare rendered previews, use test recipients, or split traffic so each production event produces only one external email.

Customer.io SDK call vs Volanea API call

The cleanest first migration is often a direct transactional message that your application already renders or can render safely. The example below sends a password-reset message. The Customer.io version uses the Node SDK’s transactional request object, a stored Customer.io transactional message, person identifiers, and message_data for the template.

The Volanea version sends the same message purpose through POST /v1/send. It includes both HTML and text bodies, uses a sender on an authenticated domain, and adds an idempotency key tied to the reset event. Keep API keys on the server in both cases.

Before: Customer.io Node SDK

const {
  APIClient,
  RegionUS,
  SendEmailRequest,
} = require("customerio-node");

const customerIo = new APIClient(
  process.env.CUSTOMERIO_APP_API_KEY,
  { region: RegionUS },
);

async function sendPasswordReset({ user, resetUrl }) {
  const request = new SendEmailRequest({
    to: `${user.firstName} <${user.email}>`,
    transactional_message_id: "password-reset",
    identifiers: {
      id: user.id,
      email: user.email,
    },
    message_data: {
      first_name: user.firstName,
      passwordResetURL: resetUrl,
    },
  });

  return customerIo.sendEmail(request);
}

In this version, password-reset identifies the transactional message stored in Customer.io. Its Liquid template might reference values passed in message_data, while customer attributes can be referenced separately in the Customer.io template. Customer.io supports content overrides for transactional sends, but a migration is easier to reason about when each message has an explicit source of truth.

After: Volanea REST API

const crypto = require("node:crypto");

async function sendPasswordReset({ user, resetUrl, resetRequestId }) {
  const response = await fetch("https://api.volanea.com/v1/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VOLANEA_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `password-reset:${resetRequestId}`,
    },
    body: JSON.stringify({
      from: "Acme Security <security@mail.example.com>",
      to: [user.email],
      subject: "Reset your Acme password",
      html: `
        <p>Hello ${escapeHtml(user.firstName)},</p>
        <p>We received a request to reset your password.</p>
        <p><a href="${escapeHtml(resetUrl)}">Reset your password</a></p>
        <p>If you did not request this, you can safely ignore this email.</p>
      `,
      text: [
        `Hello ${user.firstName},`,
        "",
        "We received a request to reset your password.",
        `Reset your password: ${resetUrl}`,
        "",
        "If you did not request this, you can safely ignore this email.",
      ].join("\n"),
    }),
  });

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

  return response.json();
}

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

The example is intentionally explicit. It makes the message body visible in the application code, makes HTML escaping a responsibility of the application, and creates a stable idempotency key from the reset request rather than from an arbitrary retry attempt. Do not use a random UUID generated inside the retry loop: that would defeat retry deduplication.

If a message belongs in a reusable template rather than code, create it in Volanea and send using its templateId according to the API reference. That can be a better fit when non-engineering teammates need to own content, when several services send the same message, or when template history and test sends matter. Consult the email API reference and setup guides for the exact current template payload before implementing it.

Map data and rendering rules deliberately

The code comparison hides the part that requires the most review: template and data semantics. Customer.io transactional templates can draw from customer attributes and API trigger data. A template might contain nested conditions, loops, date formatting, defaulting rules, or links composed from multiple attributes.

Do not copy Customer.io template source into a new system and presume it will render identically. Liquid syntax itself is expressive, but each product’s available objects, filters, HTML processing, preview data, unsubscribe handling, and error behavior affect output. A template that renders without an error can still produce the wrong URL, an empty greeting, a literal placeholder, malformed HTML, or a missing text alternative.

Choose one of three content strategies

Render in application code. This is usually the fastest option for a small set of stable, engineering-owned transactional messages. Your application passes final subject, html, and text values. It also makes unit testing easy because the same template function can be tested with fixtures.

Move reusable templates to Volanea. This works well for shared receipts, notifications, and email designs maintained by product or lifecycle teams. Inventory every variable, use representative sample data, and use template test sends before routing customers to the new template.

Keep Customer.io content temporarily while moving only selected sends later. This is the honest choice if the message is embedded in a broader Customer.io journey or uses a complex Liquid library. There is no prize for forcing a complex template conversion into the first release.

Establish a data contract

For each migrated message, define a typed input object. For a receipt, that might include orderId, customerName, amount, currency, items, receiptUrl, and issuedAt. Define whether missing values are invalid, optional, or replaced with a default.

Avoid sending an entire user record or database entity as template data by default. It creates accidental coupling, can expose fields that were never intended for email, and makes template changes harder to review. Pass only the values required to render the message.

Also normalize the differences between a person identifier and an email address. In Customer.io, identifiers associate a transactional send with a person. In Volanea, decide whether your service will separately upsert contacts, rely on the send pipeline’s contact handling, or treat transactional messages as event-only sends. The right answer depends on whether Volanea will also own campaigns, segmentation, or contact-level preferences for that audience.

Re-verify sending domains, SPF, DKIM, and DMARC

Do not remove Customer.io DNS records as the first step. Add and verify the Volanea domain configuration, confirm mail flow, and only retire old records after Customer.io is no longer sending from that identity and no delayed work remains.

The exact DNS record names and values are generated for your domain by each provider. Copy the current values from the Volanea domain setup rather than guessing hostnames, DKIM selectors, return-path records, or tracking records. DNS labels are provider-specific, and a record that looks plausible can still fail authentication.

DNS and authentication checklist

  • Confirm the exact From domain and subdomain used by every migrated message.
  • Add the Volanea-provided SPF record or SPF mechanism exactly as documented for the domain.
  • Check that the domain has one valid SPF TXT policy; do not publish multiple independent SPF TXT records for the same hostname.
  • Publish every Volanea-provided DKIM record and wait for domain verification before production sending.
  • Verify your DMARC policy aligns with the visible From domain and the authenticated SPF or DKIM domain.
  • Review any custom return-path, tracking, or link-branding records required for your configuration.
  • Confirm that the from address in code belongs to a verified Volanea sending domain.
  • Preserve Customer.io authentication records while its messages still send from the same domain or subdomain.
  • Send test mail to multiple mailbox providers and inspect headers for SPF, DKIM, and DMARC pass results.
  • Record the DNS change owner, TTL, deployment time, and rollback plan.

A dedicated transactional subdomain can reduce the chance that promotional-mail behavior influences critical password resets or receipts. Customer.io similarly recommends separating transactional and marketing sending domains. Whether you keep the existing transactional subdomain or introduce a new one, changing the visible From domain at the same time as changing providers creates an extra variable in deliverability analysis. Prefer one major change at a time when possible.

DNS correctness is necessary but not sufficient. Monitor bounce and complaint signals after cutover, especially if you move a large volume quickly. A domain’s reputation reflects recipient engagement and complaint behavior over time, not simply whether the records resolve.

Migrate webhook handling as a contract change

Webhook migration is not just changing a callback URL. It is replacing one provider-specific event schema and signature mechanism with another. Your event consumer should be designed around your own normalized internal events, not deeply coupled to a vendor payload.

Volanea webhook endpoints are created through the webhooks API and receive a signing secret. Deliveries include Volanea-specific headers and retry with backoff for roughly 24 hours. Store the signing secret securely, verify the signature against the raw request bytes before parsing JSON, and make the receiver idempotent.

Customer.io webhook and delivery-event payloads should remain active until all Customer.io-originated sends that matter to your downstream systems have completed. During the transition, your endpoint may need to accept both providers or you may run separate endpoints that publish into the same internal event queue.

What to test for each webhook endpoint

  1. Authentication: Verify the provider signature using the documented header and the unmodified request body. Do not verify a reconstructed JSON string.
  2. Idempotency: Store the provider event ID or a deterministic event key. Webhooks can be retried, and your code must tolerate duplicate delivery.
  3. Ordering: Do not assume a delivered event always arrives before an open, click, bounce, or complaint event. Process events independently and update state safely.
  4. Failure behavior: Return a successful response only after durable acceptance by your queue or database. If the receiver cannot process the event, return an error so the provider can retry according to its policy.
  5. Schema changes: Log unknown event types and retain enough raw metadata to diagnose changes without storing unnecessary message content or personal data.
  6. Operational alerts: Alert on signature failures, sustained non-2xx responses, event lag, and a sudden change in bounce or complaint rates.

Map events at the business level rather than trying to force identical labels. For example, your application may normalize provider events into email.accepted, email.delivered, email.bounced, email.complained, email.opened, and email.clicked. Preserve the original provider, provider message ID, event ID, recipient, timestamp, and reason fields alongside the normalized event for troubleshooting.

Import suppression data before sending customers mail

A suppression list is safety infrastructure. It prevents sends to addresses that hard bounced, complained, unsubscribed, or were manually blocked. Customer.io and Volanea may distinguish between message-level suppression, person-level subscription preference, and contact status differently, so export and classify the data before importing it.

Volanea exposes suppressions as a do-not-send list with reasons including bounces, complaints, unsubscribes, and manual blocks. It also supports a bulk contacts action that can suppress up to 10,000 addresses at a time. For large exports, batch your import, retain a source manifest, and reconcile counts instead of assuming a CSV upload completed correctly.

Suppression migration checklist

  • Export Customer.io suppression and unsubscribe data with available reason, timestamp, source, and identifier fields.
  • Deduplicate and lowercase email addresses using your own migration process before import.
  • Separate hard bounces, spam complaints, explicit unsubscribes, and internal/manual blocks where the source data allows it.
  • Import addresses that must not receive any migrated sends before enabling Volanea production traffic.
  • Reconcile exported, accepted, rejected, and imported row counts.
  • Keep an immutable export and an import manifest with timestamps and checksums.
  • Test known suppressed addresses and ensure a send is skipped rather than delivered.
  • Decide separately how marketing consent and transactional eligibility should work in the new model.
  • Do not automatically clear bounce or complaint suppressions just to increase reach.
  • Define ownership and approval rules for removing a suppression later.

Be especially careful with the word “unsubscribe.” A recipient may unsubscribe from marketing while still expecting a password reset or receipt. Customer.io treats transactional messaging differently from marketing communication, and your application should continue to respect the legal and product rules that distinguish the two. Do not use a transactional classification as a shortcut for sending promotional content to people who opted out.

Customer.io features that do not map one-to-one

A migration guide should be honest about tradeoffs. Volanea can cover transactional sending, contacts, campaigns, templates, webhooks, suppressions, and workflows, but that does not mean every Customer.io implementation transfers with the same data model, authoring experience, or delivery channel.

The following Customer.io-oriented capabilities require explicit assessment rather than an assumed direct migration:

  • Liquid templates and filters: Template variables, filters, conditions, and fallback behavior need conversion and rendered-output tests.
  • Customer attributes, relationships, and objects: If templates or journeys rely on Customer.io’s person and object model, decide whether to recreate equivalent data structures or supply the necessary data from your application.
  • Journeys and behavioral campaigns: Event-driven campaigns with delays, branching, frequency constraints, and segment entry rules need a separate workflow design and acceptance test plan.
  • Cross-channel messaging: Customer.io transactional and campaign capabilities can include email, push, SMS, in-app, inbox, and other channels. Migrating email sending does not migrate mobile SDK behavior or messaging consent automatically.
  • Broadcasts, newsletters, and reporting: Metrics definitions, attribution windows, filtering, and dashboards may differ even when event names look similar.
  • Attachments and recipient addressing: Customer.io’s transactional API supports attachments with documented limits and can handle CC and BCC. Confirm the Volanea endpoint’s current fields and limits before migrating an attachment-heavy or copied-recipient use case.
  • AMP email: If you send AMP variants from Customer.io, plan this independently. Treat the normal HTML and text versions as the baseline acceptance requirement.
  • Regional configuration and data residency: Customer.io has US and EU regions. Confirm the applicable Volanea configuration, data-processing requirements, and vendor review expectations before moving personal data.

The right outcome may be hybrid operation: Volanea handles application-owned transactional email while Customer.io remains the engagement platform for lifecycle journeys and mobile channels. That is a valid architecture when it matches your product and team boundaries.

What is harder to migrate than it looks

Large historical suppression lists are difficult because exports may contain duplicates, old identifiers, inconsistent casing, missing reasons, and records that reflect multiple consent systems. Importing everything blindly can preserve stale blocks; importing too little can cause preventable bounces, complaints, and trust damage. Preserve provenance and favor safety when a record is ambiguous.

Template syntax differences are also harder than they first appear. A message can have dozens of branches based on locale, plan, billing state, device, or purchase contents. The visible HTML may include logic that is not obvious in a visual editor. Build a fixture set that includes empty fields, unusually long values, non-ASCII names, multiple line items, currencies, locale variants, and edge-case URLs.

Historical analytics do not become directly comparable merely because both systems report deliveries, opens, and clicks. Privacy features, image loading, bot activity, tracking configuration, and event definitions affect those numbers. Export the Customer.io reports you need for historical reference, then treat Volanea metrics as the start of a new, clearly labeled measurement series.

Finally, retry behavior deserves careful migration. Customer.io SDKs and APIs may have different timeout and error semantics than your new HTTP integration. With Volanea, generate one idempotency key per business event, persist it alongside the job if possible, and reuse it for retries. A retry strategy without idempotency can quietly produce duplicate receipts, duplicate invites, or repeated alert emails.

Validate deliverability and behavior with a release checklist

A successful HTTP response means the provider accepted the request; it does not prove that the recipient received the message or that the content was correct. Build release validation around the full path from application event to mailbox and back through delivery events.

Pre-production verification

  • Unit-test message construction, escaping, required fields, and plain-text alternatives.
  • Render each migrated template using representative fixture data.
  • Compare the Customer.io and Volanea versions side by side for subject, From name, recipient, preheader, links, branding, locale, and text content.
  • Confirm password-reset and invite URLs are single-use, expire as intended, and are not logged with secrets unnecessarily.
  • Verify domain authentication and inspect actual received headers.
  • Test webhook signature verification, duplicate event processing, retries, and invalid payload handling.
  • Test known suppression and unsubscribe outcomes.
  • Validate failure behavior when Volanea returns a non-success response or the network times out.
  • Test idempotent retries using the same business event identifier.
  • Confirm logs contain provider message IDs and internal correlation IDs but do not expose message secrets.

Production rollout verification

  • Route a controlled, non-duplicative subset of traffic through Volanea.
  • Compare accepted sends against application events and background-job counts.
  • Watch delivery, bounce, complaint, and webhook-failure trends by message type.
  • Check support tickets for missing, duplicated, malformed, or unexpected emails.
  • Confirm rollback routing works before expanding traffic.
  • Increase volume in planned steps rather than moving all critical traffic at once.
  • Keep the Customer.io configuration and credentials needed for rollback until the agreed observation period ends.

For most teams, a message-level dashboard is more actionable than a provider-wide one during migration. Break down send outcomes by password reset, receipt, verification, invitation, security alert, and other critical category. This makes it easier to spot a template or data-contract defect that is hidden inside healthy aggregate delivery numbers.

Design an adapter so the next provider change is smaller

Even if you do not expect to move providers again, an internal email interface gives you cleaner application code. Your product services should express an intent such as sendPasswordReset, sendReceipt, or sendInvitation; a provider adapter should translate that intent into Volanea’s HTTP request and interpret the response.

A small adapter also centralizes authentication, timeouts, idempotency-key generation, structured logging, and error classification. It prevents the Volanea API key and payload shape from spreading across dozens of controllers and workers.

For example, define an internal result that includes provider, providerMessageId, acceptedAt, recipient, and correlationId. Your webhooks can later connect delivery events back to this result. When the provider returns an error, classify it as retryable, non-retryable, or needing operator review rather than retrying every failure indefinitely.

If cost planning is part of the migration decision, review transactional email pricing alongside your expected volume, campaign use, retention needs, and operational requirements. Price per email is only one dimension; engineering ownership, template workflow, data architecture, and observability also affect the real cost of a migration.

Conclusion: move sends first, then expand deliberately

A Customer.io to Volanea migration can be a focused improvement when your immediate goal is application-owned transactional email over a direct REST or SMTP interface. Start with a precise inventory, verify the new sending domain, preserve suppression protections, convert templates with real fixtures, and treat webhooks as a signed event-contract change.

Keep the scope honest. Customer.io is broader than a transactional email endpoint for teams using journeys, segmentation, customer data, and cross-channel messaging. Move those capabilities only after deciding whether Volanea should own them, your application should own them, or Customer.io should remain in place for that layer.

The lowest-risk path is incremental: one message type, measurable outcomes, working rollback, and no duplicate sends. Once your password resets, receipts, and alerts have proven stable, the remaining work becomes an informed architectural choice instead of an emergency cutover.

FAQ

Can I keep Customer.io for campaigns and use Volanea for transactional email?

Yes. Many teams separate application-owned transactional sends from lifecycle campaigns and journeys. Make the ownership boundary explicit, ensure each system has the right sending-domain configuration, and avoid sending the same message from both platforms.

Do I need to move Customer.io people and events before sending through Volanea?

Not necessarily for a narrow transactional migration. You do need the recipient address and the data required to render each message. Move or synchronize contacts and events only if Volanea will also own your contact management, campaigns, segments, or workflows.

Should I use Volanea templates or render email in my app?

Use application rendering for a small set of engineering-owned, stable transactional messages. Use reusable templates when content needs shared ownership, version history, test sends, or reuse across services. In either case, send both HTML and plain-text content where appropriate.

How do I prevent duplicate emails when a job retries?

Create a stable idempotency key from the underlying business event—such as an order ID plus receipt version or a reset-request ID—and reuse that key for every retry of that event. Do not generate a fresh random key on each attempt.

What should I migrate first?

Start with an internally observable, low-risk message, then move a small transactional family such as verification emails or non-critical alerts. Move password resets, receipts, and other high-impact messages only after DNS, suppression imports, webhook verification, and rollback behavior have been tested.