Send email from Ruby

Five steps: check your Ruby, create a test key, send to the sandbox, read the log, then verify a domain so you can send to anyone. Using Rails? The Rails quickstart puts the same steps in a service object.

1. Install

No SDK needed. The API is plain JSON over HTTPS, and net/http and json ship with Ruby.

shell
ruby --version   # 3.0 or newer; net/http and json ship with Ruby

2. Create an API key

Sign in, open API keys in the dashboard and create a test key. It starts with av_test_. Export it so the script can read it:

shell
export AVELTO_API_KEY=av_test_...
Sandbox rules

Test keys never deliver anything; they run the pipeline and record events. The sandbox sender you@sandbox.avelto.dev only delivers to your account's verified owner email and to the simulator addresses delivered@, bounced@ and complained@sandbox.avelto.dev. Anything else is refused with 403 sandbox_recipient_not_allowed. To send to anyone, verify a domain (step 5).

3. Send your first email

Every request carries the key as a bearer token. The small api helper below sends JSON, parses the response and aborts with the code and message from the error envelope on any non-2xx status.

Ruby
# send.rb
require "net/http"
require "json"

API = "https://api-staging.avelto.dev"
HEADERS = {
  "Authorization" => "Bearer #{ENV.fetch("AVELTO_API_KEY")}",
  "Content-Type" => "application/json",
}

def api(method, path, body = nil)
  uri = URI("#{API}#{path}")
  req = Net::HTTP.const_get(method).new(uri, HEADERS)
  req.body = JSON.generate(body) if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
  data = res.body.to_s.empty? ? nil : JSON.parse(res.body)
  unless res.is_a?(Net::HTTPSuccess)
    abort "#{res.code} #{data["error"]["code"]}: #{data["error"]["message"]}"
  end
  data
end

email = api(:Post, "/v1/emails", {
  from: "you@sandbox.avelto.dev",
  to: "delivered@sandbox.avelto.dev",
  subject: "Hello from Avelto",
  text: "It works.",
})
puts email["id"] # "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10"
shell
ruby send.rb

The API answers 201 Created with the email id:

HTTP
HTTP/1.1 201 Created
Content-Type: application/json

{ "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" }
Retrying safely

Send an Idempotency-Key header (any unique string, such as your order id) with every POST /v1/emails. If the request times out or comes back 429, 502, 503 or 504, wait a moment and send it again unchanged with the same key: the API returns the original email id instead of sending twice. See Idempotency.

4. Check the log

Fetch the email by id with the same helper. status moves from queued to sent to delivered, and events records each step: email.queued, email.sent, email.delivered.

Ruby
email = api(:Get, "/v1/emails/#{email["id"]}")

puts email["status"] # "queued", then "sent", then "delivered"
email["events"].each { |e| puts "#{e["type"]} #{e["occurred_at"]}" }
JSON
{
  "id": "9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10",
  "mode": "test",
  "from": "you@sandbox.avelto.dev",
  "to": ["delivered@sandbox.avelto.dev"],
  "subject": "Hello from Avelto",
  "status": "delivered",
  "events": [
    {
      "id": "e1f0c3a4-8b2d-4c6e-9a1f-5d7b3e2c8a90",
      "type": "email.queued",
      "payload": {},
      "occurred_at": "2026-09-17T10:12:04.000Z"
    },
    {
      "id": "a7c2e9d1-3f4b-4a8e-b6c0-2d9e1f7b5c34",
      "type": "email.sent",
      "payload": { "test": true, "ses_message_id": "test-9c1f4a52-6f6e-4b8f-9b8e-2e1a5c7d3f10" },
      "occurred_at": "2026-09-17T10:12:06.000Z"
    },
    {
      "id": "c4b8d2f6-7e1a-4d3c-8f9b-6a2e0c5d1b78",
      "type": "email.delivered",
      "payload": { "test": true, "recipients": ["delivered@sandbox.avelto.dev"] },
      "occurred_at": "2026-09-17T10:12:06.000Z"
    }
  ]
}

5. Verify a domain

Add a domain, publish the DNS records it prints (three DKIM CNAMEs, an SPF TXT and a DMARC TXT), then poll GET /v1/domains/:id until status is verified. The GET re-checks DNS on every call. Use a subdomain such as mail.acme.com.

Ruby
# verify_domain.rb (reuse the api helper from send.rb)
domain = api(:Post, "/v1/domains", { name: "mail.acme.com" })

domain["dns_records"].each do |r|
  puts "#{r["type"]}\t#{r["name"]}\t#{r["value"]}\t(#{r["purpose"]})"
end

# Publish the records, then poll. GET re-checks DNS on every call.
while domain["status"] == "pending"
  sleep 30
  domain = api(:Get, "/v1/domains/#{domain["id"]}")
end
puts domain["status"] # "verified" or "failed"
shell
ruby verify_domain.rb

Once the domain is verified, switch AVELTO_API_KEY to a live key (av_live_) and change from to an address on it, such as hello@mail.acme.com. Nothing else changes.

Next

  • Send email: every field, attachments, tags, scheduling and idempotency.
  • Webhooks: get events pushed to your app.
  • Test mode: test keys, the sandbox sender and the simulator addresses.