# Quickstart: Ruby

> Send email from Ruby with the standard library: one Net::HTTP POST to the Email Fast REST API with a sandbox key, an idempotency key, and the timeline.

Canonical: https://emailfast.dev/docs/quickstart-ruby

## 1. Send

The API is plain JSON over HTTPS with bearer auth. Ruby 3's `Net::HTTP.post`
takes a headers hash directly. (A generated Ruby SDK covers the same surface —
see [the docs](/docs) — but the raw calls hide nothing.)

```ruby
require "net/http"
require "json"
require "uri"

API_KEY = ENV.fetch("EMAILFAST_API_KEY") # ef_sandbox_… works everywhere

res = Net::HTTP.post(
  URI("https://api.emailfast.dev/v1/emails"),
  {
    to: "ada@example.com",
    subject: "Welcome to the ledger",
    html: "<h1>Hi {{name}}</h1><p>Your account is ready.</p>",
    data: { name: "Ada" },
    idempotency_key: "welcome-ada-1" # retry-safe: same key can never double-send
  }.to_json,
  "Authorization" => "Bearer #{API_KEY}",
  "Content-Type" => "application/json"
)

msg = JSON.parse(res.body)
puts "#{msg["id"]} #{msg["status"]}" # msg_… queued
```

a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send. That is what `idempotency_key` buys you: wrap this call in
`retryable`, Sidekiq's retry, or a bare `begin/rescue/retry` — a timeout after
the server committed the send still can't produce a duplicate.

## 2. Watch it happen

```ruby
timeline = JSON.parse(
  Net::HTTP.get(
    URI("https://api.emailfast.dev/v1/messages/#{msg["id"]}"),
    { "Authorization" => "Bearer #{API_KEY}" }
  )
)

timeline["events"].each { |e| puts e["type"] } # admitted → rendered → (sandbox-)delivered …
```

With a sandbox key the message lands in your hosted capture inbox — same
pipeline, nothing sent: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.

## Rails note

Nothing here needs Rails, but if you are in one: put the two calls in a job, keep
the `idempotency_key` derived from your own record id (`"welcome-user-#{user.id}"`),
and job retries become harmless by construction.

## Next

- [The raw REST reference](/docs/quickstart-curl)
- [Templates & MJML](/features/templates)
- [Migrating from SendGrid/Mailgun/Postmark](/features/migration)
