Project

quolle

0.0
The project is in a healthy, maintained state
Send transactional email through Quolle from Ruby.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0
 Project Readme

Quolle Ruby SDK

Official Ruby client for the Quolle email API.

Install

gem install quolle

Or in your Gemfile:

gem "quolle"

Requires Ruby 2.7+. No third-party dependencies.

Quick start

require "quolle"

quolle = Quolle.new(api_key: "qle_...") # or set QUOLLE_API_KEY

result = quolle.emails.send(
  from: "hello@mail.yourdomain.com",
  to: "customer@example.com",
  subject: "Welcome!",
  html: "<h1>Thanks for signing up</h1>"
)

puts "Queued: #{result['id']}"

Sending

Multiple recipients

quolle.emails.send(
  from: "hello@mail.yourdomain.com",
  to: ["a@example.com", "b@example.com"],
  subject: "Announcement",
  html: "<p>Hello everyone</p>"
)

Templates

quolle.emails.send(
  from: "hello@mail.yourdomain.com",
  to: "customer@example.com",
  template: "welcome-email",
  variables: { firstName: "Amaka", planName: "Starter" }
)

Scheduled send

quolle.emails.send(
  from: "hello@mail.yourdomain.com",
  to: "customer@example.com",
  subject: "Your weekly digest",
  html: "<p>Here's what happened this week.</p>",
  scheduled_at: "2026-12-25T09:00:00.000Z"
)

Idempotency

quolle.emails.send(
  from: "billing@mail.yourdomain.com",
  to: "customer@example.com",
  subject: "Invoice #1234",
  html: "<p>Your invoice is attached.</p>",
  idempotency_key: "order_invoice_12345"
)

Batch

Up to 100 emails in one all-or-nothing request:

result = quolle.emails.send_batch([
  { from: "hello@mail.yourdomain.com", to: "a@example.com", subject: "Hi Alice", html: "<p>Hi Alice</p>" },
  { from: "hello@mail.yourdomain.com", to: "b@example.com", subject: "Hi Bob", html: "<p>Hi Bob</p>" }
])
puts "Queued #{result['queued']}"

Attachments

Pass attachments: — an array of hashes with filename, base64 content, and an optional contentType. Up to 20 files, 10 MB total.

require "base64"

quolle.emails.send(
  from: "billing@mail.yourdomain.com",
  to: "customer@example.com",
  subject: "Your invoice",
  html: "<p>Invoice attached.</p>",
  attachments: [
    {
      filename: "invoice.pdf",
      content: Base64.strict_encode64(File.binread("invoice.pdf")),
      contentType: "application/pdf"
    }
  ]
)

Retrieve & cancel

email = quolle.emails.get("a1b2c3d4-...")
puts email["status"] # queued | sending | sent | delivered | bounced | failed

quolle.emails.cancel("a1b2c3d4-...") # only while status == "scheduled"

Error handling

begin
  quolle.emails.send(
    from: "hello@mail.yourdomain.com", to: "customer@example.com",
    subject: "Welcome!", html: "<h1>Welcome</h1>"
  )
rescue Quolle::Error => e
  puts e.status_code # e.g. 402
  puts e.message     # e.g. "Monthly limit reached"
  puts e.data        # extra fields, e.g. {"limit" => 3000}
end

Testing your integration

Send to a reserved test address to simulate any outcome without touching your sending reputation: delivered@test.quolle.com, bounced@test.quolle.com, complained@test.quolle.com, suppressed@test.quolle.com.

Verifying webhooks

Confirm an incoming webhook really came from Quolle. Pass the raw request body, the Quolle-Signature header, and your signing secret (whsec_…):

begin
  event = quolle.webhooks.verify(
    request.body.read,                  # raw body
    request.env["HTTP_QUOLLE_SIGNATURE"],
    "whsec_your_signing_secret"
  )
  # event["event"] == "email.delivered"
rescue Quolle::Error
  halt 400
end

HMAC-SHA256 with a 5-minute timestamp window (replay protection).

Automatic retries

Transient failures — HTTP 429 (rate limit) and 5xx, plus network errors — are retried automatically with exponential backoff, honoring the Retry-After header. To avoid double-sending, a POST is only retried on a 5xx/network error when you pass an idempotency key; a 429 is always safe to retry (the request was never processed). Tune with max_retries: on Quolle.new (default 3).

License

MIT