Send email with Rails through Volanea by making a server-side JSON request to the transactional sending endpoint. This guide uses a small Ruby service object, an API key stored in an environment variable, and Faraday for dependable HTTP handling.

Overview: send email with Rails through the REST API

Rails does not require a provider-specific gem to send a transactional email through an HTTP API. The application already gives you a good place to keep application services, environment configuration, logging, background jobs, and tests. Adding a focused HTTP client lets you keep the provider boundary explicit while avoiding an invented or unsupported SDK abstraction.

This integration sends a single transactional message with Volanea’s POST /v1/send endpoint. The request is made from your Rails server, never from browser JavaScript. That distinction matters: your Volanea API key is a secret with permission to send on your behalf, so it must stay in server-side environment configuration.

The implementation below uses Faraday because it provides a clean Ruby HTTP interface, timeout configuration, and a straightforward request/response model. It is intentionally small enough to understand and adapt, but it also includes practical details that matter in production:

  • The API key is loaded with ENV.fetch, so a missing secret fails early and visibly.
  • The request sends JSON with the correct Content-Type header.
  • A new idempotency key is generated for each new email-send operation.
  • Non-success HTTP responses raise an application error that includes the status and response body.
  • The code does not assume a particular response field such as id; it returns the parsed Volanea response exactly as received.

Before sending, make sure the from address belongs to a sending domain you have configured for your Volanea account. A message can be correctly constructed in Rails and still be rejected if its sender identity is not authorized for your account.

Prerequisites

You need a Rails application that can make outbound HTTPS requests, a Volanea API key, and a sender address from a configured sending domain. The sample is designed for a conventional Rails application and works whether you call it from a controller, a model callback, a background job, or bin/rails runner.

Have these values ready before continuing:

  1. A Volanea secret API key. Store it in a secret manager or deployment environment variable rather than committing it to source control.
  2. A sender address on a domain configured for sending, such as notifications@your-verified-domain.com.
  3. A recipient address that you control for the first test.
  4. Ruby and Bundler available in the Rails project.

This is a REST integration rather than an SMTP configuration. That means Rails sends an HTTPS POST request with a JSON body, while Volanea handles the downstream email-delivery pipeline. REST is useful when your application needs a structured response and when you want the sending call to be explicit in application code.

For local development, use a .env file only if your project already loads one safely. In hosted environments, configure VOLANEA_API_KEY through the platform’s secrets or environment-variable system. The code below only depends on the environment variable itself; it does not require any particular secrets-management gem.

Install the HTTP dependency

Install Faraday from the root of your Rails application:

bundle add faraday

Bundler adds Faraday to your Gemfile and updates Gemfile.lock. Commit both files so every developer and deployment environment resolves the same dependency set.

Faraday is the only additional gem this example needs. Ruby already includes json and securerandom, and Rails provides the application structure, logging, and runtime configuration around the client.

After installation, verify that Bundler can load the dependency:

bundle exec ruby -e "require 'faraday'; puts Faraday::VERSION"

Do not install a random gem named after an email provider unless its ownership, maintenance status, and API compatibility are verified. This guide uses the documented REST endpoint directly, so the request format remains visible in your application and is easy to audit during upgrades.

Configure the Volanea API key

Set VOLANEA_API_KEY in the environment that runs Rails. The variable must be available to the Rails web process and to any worker process that sends email jobs.

For a temporary local shell session, you can export it before starting Rails:

export VOLANEA_API_KEY="sk_your_volanea_secret_key"

If your development environment uses a .env file, add this value locally and keep the file out of version control:

VOLANEA_API_KEY=sk_your_volanea_secret_key

Restart the Rails server, console, or worker after changing environment variables. A process cannot automatically see a secret that was added after it started.

You can confirm Rails can read the variable without printing the key itself:

bin/rails runner 'puts ENV.fetch("VOLANEA_API_KEY").start_with?("sk_")'

The expected output is true. Do not log the full key, interpolate it into exceptions, send it to browser code, or add it to a client-visible build-time variable. Treat it like a password: anyone who obtains it may be able to make authenticated API requests as your account.

Complete Rails service object

Create app/services/volanea_client.rb with the following complete implementation. This is the copy-pasteable client used throughout the rest of the guide.

# app/services/volanea_client.rb
require "faraday"
require "json"
require "securerandom"

class VolaneaClient
  API_BASE_URL = "https://api.volanea.com"

  class ApiError < StandardError
    attr_reader :status, :body

    def initialize(status:, body:)
      @status = status
      @body = body
      super("Volanea API request failed with status #{status}: #{body}")
    end
  end

  def initialize(api_key: ENV.fetch("VOLANEA_API_KEY"))
    @api_key = api_key
  end

  def send_transactional_email(from:, to:, subject:, html:)
    response = connection.post("/v1/send") do |request|
      request.headers["Authorization"] = "Bearer #{@api_key}"
      request.headers["Content-Type"] = "application/json"
      request.headers["Accept"] = "application/json"
      request.headers["Idempotency-Key"] = SecureRandom.uuid

      request.body = JSON.generate(
        from: from,
        to: [to],
        subject: subject,
        html: html
      )
    end

    unless response.status.between?(200, 299)
      raise ApiError.new(status: response.status, body: response.body)
    end

    JSON.parse(response.body.presence || "{}")
  end

  private

  def connection
    @connection ||= Faraday.new(url: API_BASE_URL) do |faraday|
      faraday.options.open_timeout = 5
      faraday.options.timeout = 10
    end
  end
end

The client receives from, to, subject, and html as explicit keyword arguments. This makes the service easy to call, avoids loose positional arguments, and gives Ruby useful errors if a required field is omitted.

The to value is sent as a one-item array. The Volanea send endpoint supports sending one message to one address or multiple recipients, and using an array keeps the representation consistent when your application later needs to add recipients.

The Idempotency-Key is generated per call. An idempotency key is especially useful when your application has a network timeout after submitting a request: it gives the API a way to identify a retry of the same logical send instead of treating it as a new message. For a true retry implementation, persist the generated key with the business event and reuse that same key only when retrying that event.

The sample raises VolaneaClient::ApiError for any non-2xx response. This is preferable to silently returning false, because a failed receipt, password reset, or account-verification message deserves visible logging and application-level handling.

Send your first transactional email

You can test the service without building a controller or mailer first. Replace both example addresses with real addresses appropriate for your configured sender domain and test recipient.

bin/rails runner '
result = VolaneaClient.new.send_transactional_email(
  from: "notifications@your-verified-domain.com",
  to: "you@example.com",
  subject: "Your Rails integration is working",
  html: "<h1>Email sent from Rails</h1><p>Your Volanea REST API integration is ready.</p>"
)

pp result
'

If the request succeeds, Rails prints the JSON response returned by Volanea. Check the test recipient’s inbox and spam folder, then inspect the response in your terminal or application logs for the API’s message metadata.

For this first request, keep the content deliberately simple. Do not begin with an account-confirmation link, a customer record lookup, or a database callback. First verify that the application has the correct API key, endpoint, sender address, JSON structure, and outbound network access. Once the basic send succeeds, move the call into the application flow that needs it.

A common next step is to invoke the service after a successful signup or purchase. For example, a controller can call the client after a user record has been persisted:

result = VolaneaClient.new.send_transactional_email(
  from: "notifications@your-verified-domain.com",
  to: @user.email,
  subject: "Welcome to Example App",
  html: "<h1>Welcome, #{ERB::Util.html_escape(@user.name)}!</h1><p>Your account is ready.</p>"
)

Rails.logger.info("Volanea send response: #{result.inspect}")

When interpolating user-provided data into HTML, escape it. In the example, ERB::Util.html_escape prevents a name or other untrusted value from being interpreted as email markup. For substantial email layouts, keep rendering logic in Rails templates or use managed email templates rather than assembling large HTML strings in controllers.

Understand what the request sends

The service object sends a standard authenticated HTTPS request. Conceptually, the request looks like this:

POST /v1/send HTTP/1.1
Host: api.volanea.com
Authorization: Bearer sk_your_volanea_secret_key
Content-Type: application/json
Accept: application/json
Idempotency-Key: a-unique-uuid-for-this-send

{
  "from": "notifications@your-verified-domain.com",
  "to": ["you@example.com"],
  "subject": "Your Rails integration is working",
  "html": "<h1>Email sent from Rails</h1>"
}

The authorization header identifies your Volanea account. The JSON content type tells the server how to parse the body. The Accept header asks for a JSON response. The idempotency header identifies this one sending operation so that a carefully designed retry can avoid creating a duplicate send.

Do not use form encoding such as application/x-www-form-urlencoded for this request. Do not use multipart/form-data unless you are specifically implementing an API operation that documents multipart uploads. For the transactional JSON request shown here, serialize the Ruby hash with JSON.generate and send application/json.

The HTML body in the example is intentionally minimal. Production transactional email should generally include semantic markup, inline-friendly styles, readable plain-language copy, accessible link text, and a useful subject. Keep messages focused on the event that caused them: a receipt should explain a purchase, a password reset should explain the reset action, and an account alert should explain the security event.

Use the client safely in Rails application flows

A successful HTTP request means the API accepted the request; it is not the same thing as proving a recipient has opened or read the email. Keep that distinction clear in product logic. A signup flow should not mark a user as confirmed merely because an email-send request returned successfully.

For user-facing actions, decide how email failure should affect the request. Password resets commonly need a resilient path: record that the reset was requested, enqueue delivery, and surface a generic response rather than exposing delivery details. Receipts may be sent after payment has been committed, with retry handling if the send call fails. Administrative alerts may be synchronous if an operator needs immediate feedback.

Avoid making an external API call inside a database transaction when possible. If the message is accepted but the database transaction later rolls back, your recipient may receive an email about a record that does not exist. A safer pattern is to save the business event first and enqueue a job after the transaction commits.

For example, an application can enqueue a job after a user is created:

# app/jobs/welcome_email_job.rb
class WelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)

    VolaneaClient.new.send_transactional_email(
      from: "notifications@your-verified-domain.com",
      to: user.email,
      subject: "Welcome to Example App",
      html: "<h1>Welcome, #{ERB::Util.html_escape(user.name)}!</h1><p>Thanks for signing up.</p>"
    )
  end
end

Then enqueue it only after the user exists:

WelcomeEmailJob.perform_later(@user.id)

This is the Rails equivalent of handling asynchronous work. Ruby and Rails do not use JavaScript-style async and await syntax here. perform_later hands work to the Active Job backend configured by your application; whether that is truly asynchronous depends on the queue adapter you have configured. In development, some adapters execute jobs immediately, while production deployments commonly use a separate worker process.

Make retries and idempotency intentional

Email sending is a side effect. If a request times out after leaving your server, your application may not know whether Volanea received it. Retrying blindly can create duplicate receipts, duplicate alerts, or multiple password-reset messages.

The sample generates an idempotency key for every new send. That is correct for a one-time synchronous request, but a production retry strategy needs one additional step: store the key alongside the business event before attempting the HTTP request. If the job retries, pass the stored key again instead of generating another UUID.

A robust pattern looks like this:

  1. Create a durable application record for the notification or event.
  2. Generate and persist one idempotency key for that record.
  3. Attempt the Volanea send using that key.
  4. Mark the record as submitted only after a successful API response.
  5. On a retry, reuse the same key and payload for the same notification record.

Do not reuse one idempotency key for unrelated emails. A welcome email for one user and a receipt for another are separate operations and require different keys. Similarly, do not accidentally regenerate the key every time a retry job runs, because doing so turns the retry into a new request from the API’s point of view.

Set reasonable timeout values. The example uses five seconds to establish the connection and ten seconds for the full request. Your application may need different values, but avoid an unbounded wait that consumes web threads during provider or network incidents.

Common errors when sending email with Rails

Authentication failures

An authentication failure usually means the API key is missing, invalid, copied with extra whitespace, revoked, or attached to a different environment than expected. First verify that the running Rails process has VOLANEA_API_KEY, not merely your interactive shell.

Use ENV.fetch("VOLANEA_API_KEY") rather than ENV["VOLANEA_API_KEY"] in the client. fetch fails immediately when the value is absent, whereas the bracket form returns nil and can lead to a confusing header such as Authorization: Bearer .

Do not put quotation marks into a hosted environment variable unless the hosting platform explicitly requires them. In most deployment dashboards, enter the raw key value only.

Wrong Content-Type or invalid JSON

The send request body must be JSON. Set Content-Type to application/json and serialize the payload with JSON.generate exactly as the service object does.

A frequent mistake is using request.params with an HTTP client configured for form submissions. Another is assigning a Ruby hash directly to request.body, which may serialize differently or not at all. Make the serialization explicit:

request.headers["Content-Type"] = "application/json"
request.body = JSON.generate(payload)

If you receive a validation response, log the response body from VolaneaClient::ApiError. The body usually contains the most useful information about malformed fields, missing values, or an invalid sender identity.

Sender address is not configured

A valid API key does not authorize every possible from address. The sender must use a domain that is configured for sending in your Volanea account. Test with a real address from that domain, and use the same address in local, staging, and production only when each environment is intended to send from it.

Do not substitute a personal mailbox address merely to get past an error. That can create authentication and deliverability problems. Configure the domain you control, then use a role-based sender such as notifications@, receipts@, or support@ as appropriate.

Background job appears to do nothing

perform_later does not guarantee that a separate worker is running. Check the Active Job adapter in the current environment and confirm the queue worker process is active in production.

For diagnosis, temporarily call perform_now in a safe development environment. If perform_now works and perform_later does not, the HTTP client is probably fine; the issue is likely queue configuration, worker availability, or a failed job that needs inspection.

Rails does not use async/await syntax for this workflow. Do not copy JavaScript examples that call await sendEmail(...) into Ruby. In Rails, use a synchronous service call or enqueue an ApplicationJob with perform_later.

Duplicate messages after a retry

A network timeout can occur after the API receives the request but before your process receives the response. If your retry creates a new idempotency key, the second request may be treated as a separate message.

Persist one idempotency key per notification event and reuse it for retries of that same event. Also make sure a job retry uses the same semantic payload. Changing recipients, subject, or content while reusing a key can make troubleshooting much harder.

HTML renders unexpectedly

Email clients have inconsistent HTML and CSS support. A message that looks correct in a browser can render differently in Outlook, Gmail, Apple Mail, and mobile clients. Keep layout HTML simple, test real messages across the clients your users use, and avoid relying on unsupported browser features.

Escape user-supplied values before inserting them into HTML. Use ERB::Util.html_escape for dynamic text, and avoid directly interpolating raw names, comments, or database fields into email markup.

Next steps: webhooks, templates, and delivery workflows

Once your first direct send works, move the message content and delivery workflow toward a production-ready design.

Webhooks let your application receive event notifications from the email platform. They are useful for updating internal records when delivery-related events occur, for observing bounces or complaints, and for connecting email activity to support or analytics workflows. A webhook endpoint should verify incoming requests according to the provider’s webhook-signature documentation, respond quickly, store the event durably, and process expensive work asynchronously.

Templates let you centralize reusable email markup and separate content maintenance from application logic. Instead of embedding a full HTML document in a Rails controller or job, you can create a reusable template and send a template reference with event-specific data where supported. This reduces duplicated layouts across welcome messages, receipts, alerts, and lifecycle emails.

For endpoint details, payload fields, template operations, and webhook configuration, consult the API reference and setup guides. Keep your Rails service object narrow: it should send a well-defined event, surface errors clearly, and let the rest of the application own business decisions such as when an email is appropriate.

FAQ

Do I need a Volanea Ruby SDK to send email with Rails?

No. This guide uses Faraday to call the Volanea REST API directly. That keeps the integration compatible with standard Ruby HTTP tooling and avoids relying on an unverified provider-specific Ruby method.

Should I send email from a Rails controller?

You can for a simple test, but production applications often enqueue sending through Active Job after the related database transaction commits. That reduces request latency and makes transient failures easier to retry.

Why does the sample use an idempotency key?

An idempotency key helps identify a retry of the same send operation. It is important when a timeout leaves your application uncertain whether the API already accepted the message.

Can I use Action Mailer with this integration?

Yes, but this guide uses a direct REST service object to keep the Volanea request visible and provider-specific behavior contained. If your application depends heavily on Action Mailer views and previews, you can render HTML with Rails and pass the rendered result to this service.

Is a successful API response proof that the recipient read the message?

No. It confirms that the API accepted the send request. Delivery, bounce, complaint, and engagement information should be handled separately through the provider’s event and webhook capabilities.