EU-Captcha Ruby client

Privacy-first, no-cookie, no-manual-interaction bot protection for Ruby applications. Automatically filters bots, spam, and credential-stuffing attempts without requiring any user interaction.

Requirements

  • Ruby 2.7 or later
  • No runtime dependencies (uses Ruby's built-in net/http)

Installation

gem install eu_captcha

Or add it to your Gemfile:

gem 'eu_captcha'

Getting credentials

  1. Register at app.eu-captcha.eu
  2. Create a site and copy the sitekey and secret from the dashboard

Quick start

Using a SPA framework? The script tag and <div> approach below is for server-rendered pages. If you are building with React, Vue, or Angular, use the matching npm package for the frontend widget and continue to use this gem for server-side verification only. See SPA integration guides for details.

Add the widget script to any page that contains a form you want to protect:

<script src="https://cdn.eu-captcha.eu/verify.js" async defer></script>

Place the widget inside your form:

<div class="eu-captcha" data-sitekey="EUCAPTCHA_SITE_KEY"></div>

Verify the submitted token on your server:

require 'eu_captcha'

captcha = EuCaptcha::Client.new(
  sitekey: ENV['EUCAPTCHA_SITE_KEY'],
  secret:  ENV['EUCAPTCHA_SECRET_KEY']
)

result = captcha.validate(
  token:       params['eu-captcha-response'],
  remote_addr: request.ip
)

render_error unless result.success?

Configuration options

All options are passed as keyword arguments to the constructor.

Option Type Default Description
sitekey String Required. Public sitekey from the dashboard.
secret String Required. Secret key from the dashboard. Never expose this client-side.
fail_default Boolean true Return value used for both network and token state when the API cannot be reached. true = fail open (allow on error); false = fail closed (deny on error).
check_cdn_headers Boolean true When true, the client IP is resolved from CDN/proxy headers (HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, HTTP_X_REAL_IP) in the Rack environment before falling back to REMOTE_ADDR. Set to false when not behind a proxy, or when you pass the IP explicitly.
verify_url String (production URL) Override the EU-Captcha verify endpoint. Useful for testing.
credentials_url String (production URL) Override the EU-Captcha verify-credentials endpoint.

The result object

validate returns an EuCaptcha::Result with these methods:

Method Returns true when…
success? The API was reached and the token is valid.
success_network? The API call completed without a network or transport error.
success_token? The API reported the submitted token as valid.
train Returns true/false/nil (see below).

Checking both states separately lets you distinguish a user failing the captcha from an API outage:

result = captcha.validate(token: params['eu-captcha-response'], remote_addr: request.ip)

unless result.success_network?
  # Could not reach the API — consider logging or alerting
end

unless result.success_token?
  # Token was rejected — the submission is likely automated
end

The train method returns true when the API skipped real validation and forced success — typically because the sitekey does not exist, the secret does not match, or the sitekey's protection toggle is disabled. success? already accounts for this flag and returns false in that case. Returns nil on network failure.

Verifying credentials

Use verify_credentials to confirm your sitekey and secret are valid without submitting a client token. Useful for startup or configuration checks:

captcha = EuCaptcha::Client.new(
  sitekey: ENV['EUCAPTCHA_SITE_KEY'],
  secret:  ENV['EUCAPTCHA_SECRET_KEY']
)

unless captcha.verify_credentials
  # Credentials are invalid or the API is unreachable — log and alert
end

verify_credentials returns false on any network or API error rather than raising, so it is safe to call during application initialisation.

Rails

Store credentials in config/credentials.yml.enc (or .env + dotenv) and create a client in an initializer:

config/initializers/eu_captcha.rb

EU_CAPTCHA = EuCaptcha::Client.new(
  sitekey: Rails.application.credentials.dig(:eucaptcha, :sitekey),
  secret:  Rails.application.credentials.dig(:eucaptcha, :secret)
)

Controller

class ContactController < ApplicationController
  def create
    result = EU_CAPTCHA.validate(
      token:       params['eu-captcha-response'],
      remote_addr: request.ip,
      user_agent:  request.user_agent
    )

    unless result.success?
      render json: { error: 'CAPTCHA verification failed' }, status: :unprocessable_entity
      return
    end

    # process the form...
  end
end

request.ip respects Rails' trusted-proxy configuration, so the real visitor IP is forwarded correctly when running behind a CDN or load balancer.

Sinatra / Rack

Pass the Rack environment directly and let the library resolve the client IP automatically:

require 'sinatra'
require 'eu_captcha'

CAPTCHA = EuCaptcha::Client.new(
  sitekey: ENV['EUCAPTCHA_SITE_KEY'],
  secret:  ENV['EUCAPTCHA_SECRET_KEY']
)

post '/contact' do
  result = CAPTCHA.validate(
    token:    params['eu-captcha-response'],
    rack_env: env
  )

  halt 422, 'CAPTCHA verification failed' unless result.success?

  # process the form...
end

When rack_env is provided, validate automatically resolves the client IP and User-Agent from the Rack environment, respecting the check_cdn_headers setting.

Further reading

License

BSD 2-Clause. See LICENSE for details.