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.message     # 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.