# Ruby SDK

URL: https://email-validation.org/docs/sdks/ruby/
Updated: 2026-10-23

> Documentation index: https://email-validation.org/llms.txt

The official Ruby SDK returns immutable result objects, raises typed errors and retries the answers that are never
charged. It needs Ruby 3.2 or later and has no runtime dependencies: only the standard library (`net/http`, `json`,
`openssl`).

**Not published yet.** The gem `email-validation-sdk` is not on RubyGems yet, so the
install step below does not work today. The SDK is complete and tested; it will be published once the
registry account and the licence are in place. Until then, use the [code samples](https://email-validation.org/docs/code-samples/), which
call the API directly.

## Install

```bash
gem install email-validation-sdk
```

or in your `Gemfile`:

```ruby
gem "email-validation-sdk"
```

The gem is required as `email_validation`.

## Validate an address

```ruby
require "email_validation"

client = EmailValidation::Client.new(api_key: ENV.fetch("EV_API_KEY"))

result = client.validate("accepted@sandbox.email-validation.org")
puts result.status, result.reason, result.action # valid accepted send
```

Build the client once and share it: it is safe to use from several threads and keeps connections alive. Keyword
arguments: `ip_address:`, `timeout:` (seconds the check may take, 5–15) and `fresh:`.
`fresh: true` checks the address again even when it was checked in the last 12 months (see
[repeat checks](https://email-validation.org/docs/api/validate/#repeat-checks)). Results are `Data` objects with the API's field names; `to_h`
and `to_json` give back the API's fields.

## Async

The Ruby client is synchronous and thread-safe: share one client between threads, within your
[rate limit](https://email-validation.org/docs/rate-limits/), or use a bulk job for lists.

## Handle errors

```ruby
begin
  client.validate(address)
rescue EmailValidation::InsufficientCreditsError
  # buy credits; nothing was charged
rescue EmailValidation::RateLimitError => e
  puts "retry in #{e.retry_after} seconds"
rescue EmailValidation::Error => e
  puts "#{e.code} #{e.status} #{e.request_id}"
end
```

Every error is an `EmailValidation::Error` (a `StandardError`) with `status` (0 when no answer arrived), `code`,
`request_id` and `retry_after`. Subclasses: `InvalidRequestError`, `AuthenticationError`,
`InsufficientCreditsError`, `PermissionDeniedError`, `NotFoundError`, `ConflictError`, `RateLimitError`,
`ServiceUnavailableError`, `ServerError` and `ConnectionError`. Wrong arguments raise `ArgumentError` before any
request is sent.

## Retries and timeouts

```ruby
client = EmailValidation::Client.new(
  api_key: ENV.fetch("EV_API_KEY"),
  timeout: 30,        # seconds per attempt
  max_retries: 2,     # after 429 / 503 answers
  max_retry_wait: 10  # longer Retry-After values are raised to you instead of waited out
)
```

`429` and `503` answers are retried with backoff, honouring `Retry-After`. A daily key cap is never retried, and a
validation is never retried after a network failure, because it may already have been charged.

## Credits, usage and status

```ruby
credits = client.credits # balance, debt, pending, lots
usage = client.usage(from: "2026-10-19", to: "2026-11-30", group_by: "day")
status = client.status
```

## Bulk jobs

```ruby
job = client.bulk.create(emails: ["accepted@sandbox.email-validation.org", "catch_all@sandbox.email-validation.org"])

# or from a CSV file (up to 100 MB): a String, an IO or a Pathname
job = client.bulk.create(file: Pathname.new("customers.csv"), column: "email", has_header: true)

job = client.bulk.wait(job.id) { |j| puts "#{j.status} #{j.processed}" }

if job.status == EmailValidation::BulkJobStatus::COMPLETED
  csv = client.bulk.results(job.id, filter: %w[valid risky])
elsif job.stuck?
  puts "could not start: #{job.error_code}" # e.g. insufficient_credits: buy credits, then client.bulk.start(job.id)
end
```

`wait` returns when the job is `completed`, `failed`, `cancelled` or `expired`, or when it is still `ready` with an
`error_code`: it could not start and does not move on by itself. Pass `timeout:` (seconds) to give up earlier. Also:
`bulk.get`, `bulk.start` for jobs created with `start: false` (call it before `wait`), `bulk.cancel`, and
`bulk.results` with a block, which streams large files in chunks.

## Verify webhooks

```ruby
# Rack, Sinatra or Rails
payload = request.body.read # the raw body, before any JSON parsing
begin
  event = EmailValidation.verify_webhook(payload, request.env, ENV.fetch("EV_WEBHOOK_SECRET"))
rescue EmailValidation::WebhookVerificationError
  halt 400, "invalid signature"
end

puts "job done #{event.data.job_id}" if event.type == "bulk.completed"
status 204
```

Headers can be any Hash with names in any case, or a Rack env. `event.type` can also be `webhook.test`, sent when you
press **Send test** in the dashboard: answer it with a `2xx` like any other event.
`EmailValidation.sign_webhook` creates a valid signature for local tests.

## Constants

`EmailValidation::STATUSES`, `REASONS`, `REASONS_BY_STATUS`, `REASON_INFO`, `ACTIONS`, `ERROR_CODES`,
`WEBHOOK_EVENTS`, the named values in `Status`, `Reason`, `Action`, `BulkJobStatus` and `ErrorCode`, and
`EmailValidation.sandbox_address(reason)` mirror the API's vocabulary. See [statuses and reasons](https://email-validation.org/docs/statuses/)
and [test addresses](https://email-validation.org/docs/test-addresses/).

## Bring your own HTTP client

Pass `transport:` with any object that has `call(request)` and returns an `EmailValidation::HttpResponse` (or raises
`EmailValidation::TransportError` when no answer arrived), for example to log requests or in tests. The default
`EmailValidation::NetHttpTransport` keeps connections alive and honours the standard proxy environment variables.
