mailfloss — official Ruby SDK
Ruby client for the mailfloss email verification API.
Zero runtime dependencies (stdlib Net::HTTP), Ruby 2.6+.
API reference: https://developers.mailfloss.com
Installation
gem install mailfloss
Or in your Gemfile:
gem "mailfloss", "~> 0.1"
Authentication
Grab an API key from your mailfloss dashboard, then either pass it directly:
require "mailfloss"
client = Mailfloss::Client.new(api_key: "mf_rk_your_key")
...or set the MAILFLOSS_API_KEY environment variable and omit the kwarg:
export MAILFLOSS_API_KEY="mf_rk_your_key"
client = Mailfloss::Client.new
If no key is available in either place, Mailfloss::ConfigurationError is
raised. The key is sent as Authorization: Bearer <key> on every request.
Quickstart
Verify a single email — GET /v1/verify
result = client.verify(email: "jane@acme.com")
result[:status] # "passed" | "undeliverable" | "risky" | "unknown"
result[:passed] # true
result[:reason] # e.g. "valid"
result[:domain] # "acme.com"
result[:disposable] # false
result[:role] # false
result[:free] # false
result[:suggestion] # typo suggestion, when detected
Batch verification — POST /v1/batch-verify
batch = client.batch_verify.create(
emails: ["jane@acme.com", "bob@invalid.example"],
webhook_url: "https://hooks.example.com/mailfloss-done" # optional callback
)
batch[:id] # => the batch job id
# Poll progress
status = client.batch_verify.status(batch[:id])
status[:status] # e.g. "processing"
status[:progress] # 0.0..1.0
# Page through results when done
page = client.batch_verify.results(batch[:id], per_page: 100)
page[:results]
# Or cancel
client.batch_verify.cancel(batch[:id]) # => { success: true }
All responses are JSON parsed with symbol keys.
Full surface
| Resource | Methods |
|---|---|
client.verify(email:, timeout: nil) |
GET /verify |
client.batch_verify |
.create(emails:, webhook_url: nil) · .status(id) · .results(id, per_page:, next_cursor:) · .cancel(id) |
client.jobs |
.list(per_page:, cursor:, source:, status:) · .get(id) |
client.users |
.list(per_page:, cursor:) · .get(user_id) |
client.reports |
.usage(period:, connection_id:) — period: this_week / this_month / this_year / lifetime |
client.check_key(...) |
GET /check-key |
client.account |
.get · .update(name:, organization:, phone:, vat_id:, country:, address:, notifications:) |
client.organization |
.get — plan, credits, usage, trial, entitlements |
client.integrations |
.list(per_page:, cursor:) · .get(type) |
client.integrations.connections |
.create(type, credentials:, name:) · .get(type, id) · .update(type, id, aggressiveness:, manual_mode:, autofloss:, decay_protection:, action:, checks:) · .delete(type, id) · .sync(type, id) · .test(type, id) |
client.integrations.keywords |
.list(type, id, list, per_page:, cursor:) · .add(type, id, list, rules:) · .delete(type, id, list, rule_id) — list: "blacklist" / "whitelist" |
client.erasures |
.create(emails:, webhook_url: nil) — GDPR/CCPA data erasure |
List endpoints return a cursor-paginated envelope:
page = client.jobs.list(per_page: 50)
page[:data] # => [{ id:, status:, source:, ... }, ...]
page[:pagination][:has_more] # => true
page[:pagination][:next_cursor] # opaque — pass back as cursor:
Example: connect an ESP and add keyword rules
conn = client.integrations.connections.create(
"klaviyo",
credentials: { api_key: "pk_..." },
name: "Main Klaviyo account"
)
client.integrations.connections.update("klaviyo", conn[:id], autofloss: true)
client.integrations.keywords.add(
"klaviyo", conn[:id], "whitelist",
rules: [{ keyword: "vip", match: "contains", applies_to: { domain: true } }]
)
Configuration
client = Mailfloss::Client.new(
api_key: "mf_rk_...", # or ENV["MAILFLOSS_API_KEY"]
base_url: "https://api.mailfloss.com/v1", # default
max_retries: 3, # retries after the first attempt
timeout: 30, # seconds (open/read)
transport: nil # injectable, see below
)
Retries
429 and 5xx responses are retried automatically (as are transient
connection errors). A Retry-After header with integer seconds is
honored; otherwise exponential backoff with full jitter is used
(base 0.5s, factor 2, capped at 8s).
Idempotency
Every POST carries an Idempotency-Key header — a fresh
SecureRandom.uuid per call by default. Pass idempotency_key: on any
create/cancel/sync/test call to supply your own (useful when you retry at
the application level):
client.batch_verify.create(emails: emails, idempotency_key: "import-2026-07-06")
Errors
Non-2xx responses raise Mailfloss::APIError (< Mailfloss::Error):
begin
client.jobs.get("nope")
rescue Mailfloss::APIError => e
e.status # 404
e.code # "not_found"
e. # human-readable message
e.type # error type, when provided
e.request_id # for support requests
end
Custom transport
The low-level transport is injectable — any object responding to
call(method, url, headers, body) and returning an object with status,
headers, and body. This is how the test suite mocks HTTP without a
network:
fake = ->(method, url, headers, body) {
Mailfloss::Transport::Response.new(200, {}, '{"passed":true,"email":"a@b.com","status":"passed","reason":"valid"}')
}
client = Mailfloss::Client.new(api_key: "test", transport: fake)
Type signatures
RBS signatures for the client, resources, and all response shapes
(hand-derived from the OpenAPI spec) ship under sig/.
Development
ruby test/run_tests.rb
# or
ruby -Ilib -Itest test/mailfloss_test.rb
License
MIT — see LICENSE.