Sendara Ruby
Email-first Ruby client for the Sendara API: transactional email, broadcasts, contacts, templates, domains, and signed webhooks. Pure stdlib HTTP, zero runtime dependencies.
Requires Ruby >= 3.0.
Installation
gem install sendara
Or with Bundler, add to your Gemfile:
gem "sendara"
Then run:
bundle install
Quickstart
require "sendara"
client = Sendara.new(ENV.fetch("SENDARA_API_KEY"))
result = client.emails.send(
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Welcome to Acme",
html: "<h1>Hello</h1><p>Thanks for signing up.</p>",
text: "Hello — thanks for signing up."
)
puts result["id"]
Sendara.new accepts the API key as the first argument plus optional keywords:
client = Sendara.new(
ENV.fetch("SENDARA_API_KEY"),
base_url: "https://api.sendara.dev",
timeout: 30,
max_retries: 2
)
The client automatically attaches an Idempotency-Key to every write and retries idempotent requests with exponential backoff (honoring Retry-After).
Sending email
emails.send is keyword-based. from, to, and subject are the essentials; provide html, text, or both:
client.emails.send(
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
text: "Thanks for your order.",
message_type: "transactional",
metadata: { "order_id" => "ord_123" }
)
Send with a stored template instead of inline content:
client.emails.send(
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Your receipt",
template_id: "tmpl_abc",
template_vars: { "name" => "Ada", "total" => "$42.00" }
)
Pass your own idempotency_key to make retries safe across process restarts:
client.emails.send(
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Your receipt",
html: "<p>Thanks!</p>",
idempotency_key: "receipt-ord_123"
)
For full control over the request envelope, use send_raw, or send_batch for many messages in one call:
client.emails.send_raw({
"channel" => "email",
"destination" => { "email" => "customer@example.com" },
"payload" => { "subject" => "Hi", "body_html" => "<p>Hi</p>" }
})
client.emails.send_batch([request_a, request_b, request_c])
Broadcasts
Broadcasts send one email to an audience — a saved list or an inline set of recipients.
broadcast = client.broadcasts.create(
from_email: "you@yourdomain.com",
name: "June newsletter",
subject: "What's new in June",
body_html: "<h1>June</h1><p>Updates inside.</p>",
body_text: "June updates inside.",
audience_list_id: "list_123"
)
client.broadcasts.send(broadcast["id"])
Schedule for later, or send immediately on create with send_now: true:
client.broadcasts.create(
from_email: "you@yourdomain.com",
name: "Launch",
subject: "We're live",
body_html: "<p>We launched.</p>",
recipients: ["a@example.com", "b@example.com"],
scheduled_at: "2026-07-01T09:00:00Z"
)
List, fetch, cancel, and delete:
client.broadcasts.list(limit: 20, offset: 0)
client.broadcasts.get("bcast_123")
client.broadcasts.cancel("bcast_123")
client.broadcasts.delete("bcast_123")
To create-and-send an audience in a single request, use bulk_send (same arguments as create):
client.broadcasts.bulk_send(
from_email: "you@yourdomain.com",
subject: "Flash sale",
body_html: "<p>24 hours only.</p>",
audience_list_id: "list_123",
send_now: true
)
Messages & pagination
Fetch a single page with messages.page, which returns a Sendara::MessagePage:
page = client..page(status: "delivered", limit: 50)
page..each { || puts ["id"] }
page.has_more? # => true / false
page.next_cursor # => cursor string or nil
messages.each iterates across all pages transparently, following the cursor for you:
client..each(status: "bounced") do ||
puts "#{['id']} → #{['to']}"
end
Called without a block it returns an Enumerator, so the full Enumerable API is available:
recent = client..each(channel: "email", limit: 100).first(10)
Filters: channel, status, from, to, limit, cursor. Fetch one message by id:
client..get("msg_123")
Webhook verification
Verify the signature on incoming webhooks with Sendara::Webhooks.verify. Pass the raw request body (not parsed), the request headers, and your signing secret. On success it returns the parsed event payload; on failure it raises Sendara::WebhookVerificationError.
require "sendara"
event = Sendara::Webhooks.verify(
raw_body,
request.headers,
ENV.fetch("SENDARA_WEBHOOK_SECRET")
)
event["type"] # => "email.delivered"
verify checks the Sendara-Signature HMAC and rejects requests whose Sendara-Timestamp is outside the tolerance window (default 300 seconds). Adjust it if needed:
Sendara::Webhooks.verify(raw_body, headers, secret, tolerance: 600)
Error handling
API responses outside the 2xx range raise Sendara::ApiError, which exposes the HTTP status, the machine-readable code, the request_id, and retry_after (seconds, when the server sent it):
begin
client.emails.send(
from: "you@yourdomain.com",
to: "customer@example.com",
subject: "Hi",
html: "<p>Hi</p>"
)
rescue Sendara::ApiError => e
warn "#{e.code} (HTTP #{e.status}): #{e.}"
warn "request_id=#{e.request_id}"
raise unless e.code == "rate_limited"
sleep(e.retry_after || 1)
retry
end
The exception hierarchy, all under Sendara::Error:
| Class | Raised when |
|---|---|
Sendara::ApiError |
The API returned a non-2xx response. Has status, code, request_id, retry_after. |
Sendara::ConnectionError |
The request could not reach the API (DNS, TLS, socket). |
Sendara::TimeoutError |
The request exceeded the configured timeout. Subclass of ConnectionError. |
Sendara::WebhookVerificationError |
A webhook signature, timestamp, or body failed verification. |
Rescue Sendara::Error to catch everything from the gem:
rescue Sendara::Error => e
# any Sendara failure
end
Rails usage
Build one client and share it. An initializer works well:
# config/initializers/sendara.rb
require "sendara"
SENDARA = Sendara.new(Rails.application.credentials.sendara_api_key)
Send from anywhere, ideally off the request cycle in a background job:
# app/jobs/welcome_email_job.rb
class WelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user)
SENDARA.emails.send(
from: "hello@yourdomain.com",
to: user.email,
subject: "Welcome to Acme",
html: WelcomeMailer.render(user),
idempotency_key: "welcome-#{user.id}"
)
rescue Sendara::ApiError => e
Rails.logger.error("sendara send failed: #{e.code} #{e.} (#{e.request_id})")
raise
end
end
Receiving webhooks — verify against the raw body, which Rails exposes via request.raw_post:
# config/routes.rb
post "/webhooks/sendara", to: "sendara_webhooks#receive"
# app/controllers/sendara_webhooks_controller.rb
class SendaraWebhooksController < ActionController::API
def receive
event = Sendara::Webhooks.verify(
request.raw_post,
request.headers,
Rails.application.credentials.sendara_webhook_secret
)
case event["type"]
when "email.delivered" then handle_delivered(event)
when "email.bounced" then handle_bounced(event)
end
head :ok
rescue Sendara::WebhookVerificationError
head :bad_request
end
end
License
MIT