Ruby SDK#

The Ruby SDK (didww-otp-verification) is a server-side client for the Verification API. It provides methods for starting, reporting, and retrieving verifications by ID or phone number. It also supports all three authentication modes and verifies inbound callback signatures.


Before you begin#

Complete Getting Started in the environment where the Ruby client will send verification requests. Create the OTP application and copy its credentials from that environment.

  • Choose the authentication mode for the integration. :basic is the SDK default and requires the application key and secret. :application also requires both credentials and signs each request. :public uses only the application key but requires a callback URL so your server can approve each start.

  • Store the application secret in a server-side environment variable or secret manager. Never include it in browser or mobile application code.

  • If the OTP application has a callback URL, expose a server endpoint that can receive the request and verify its signature before returning allow or deny. See Callbacks.

Installation#

Add the gem to your Gemfile:

gem "didww-otp_verification"

Then install the dependency:

bundle install

Quick start#

Create a client, start an SMS verification, submit the code entered by the user, and retrieve the current status. This example uses the sandbox environment and HTTP Basic authentication:

require "didww/otp_verification"

client = DIDWW::OTPVerification::Client.new(
  key:    ENV.fetch("DIDWW_OTP_KEY"),
  secret: ENV.fetch("DIDWW_OTP_SECRET"),
  env:    :sandbox
)

verification = client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms",
  sms:             {languages: ["en-US"]}
)

verification.id           # => "0f9c8b7a-1e2d-4c3b-9a8f-7e6d5c4b3a21"
verification.status       # => "pending"
verification.pending?     # => true
verification.sms_language # => "en-US"

result = client.report_verification(
  verification.id,
  delivery_method: "sms",
  code:            "123456"
)

grant_access if result.verified?

current = client.get_verification(verification.id)
current.status

The start, report, and status methods each return a new Verification object containing the state returned by that request. The object is a snapshot and is not updated automatically. Call get_verification(...) when you need the latest state.

An incorrect code does not return a Verification object. It raises DIDWW::OTPVerification::ValidationError. See Error handling.

Available methods#

SDK method

HTTP request

Purpose

start_verification(...)

POST /verifications

Start a verification for one phone number.

report_verification(id, ...)

PATCH /verifications/{id}

Submit a code for a specific verification.

get_verification(id)

GET /verifications/{id}

Retrieve a specific verification.

report_verification_by_number(number, ...)

PATCH /verifications/by_number/{number}

Submit a code without providing a verification ID.

get_verification_by_number(number)

GET /verifications/by_number/{number}

Retrieve the latest verification associated with a phone number.

The SDK adds the /api/v1 base path to these requests.

Delivery methods#

The delivery method determines which value your application must submit when reporting a verification:

Delivery method

Report parameter

Example

sms

code:

code: "123456"

callout

code:

code: "123456"

The delivery_method supplied when reporting must match the method used to start the verification.

Delivery-method options#

Pass method-specific options in a hash named after the delivery method: sms: when starting an SMS verification, callout: when starting a phone call verification. The SDK sends only the hash matching delivery_method, so passing both is safe.

SMS options#

Field

Description

languages

Preferred message-template languages as BCP 47 tags, ordered from most to least preferred. See Supported languages.

app_hash

Android SMS Retriever application hash. It must contain exactly 11 characters from A-Z, a-z, 0-9, +, and /. Omit this field when automatic Android SMS capture is not used.

The following example starts an SMS verification with a preferred language and an app hash provided by the Android application:

verification = client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms",
  sms: {
    languages: ["en-US"],
    app_hash:  "A1b2C3d4E5f"
  }
)

The app hash is derived from the Android package name and signing certificate. It is not a secret and does not authenticate the application. A Ruby backend can pass the value supplied by the Android application to the Verification API.

The SDK passes keys inside sms: to the API without validating their names. Check the key names carefully because an unsupported or misspelled key may be ignored and the API defaults may be used.

Phone call options#

Field

Description

languages

Preferred announcement languages as BCP 47 tags, ordered from most to least preferred. See Supported languages.

verification = client.start_verification(
  destination:     "+5511987654321",
  delivery_method: "callout",
  callout:         {languages: ["pt-BR", "pt-PT"]}
)

The language shorthand#

sms.languages and callout.languages accept the same tags with the same semantics, so one language list works for both delivery methods. Pass it as languages: and the SDK puts it in the hash for the delivery method you named:

preferred = ["pt-BR", "pt-PT"]

client.start_verification(destination: number, delivery_method: "sms", languages: preferred)
client.start_verification(destination: number, delivery_method: "callout", languages: preferred)

An explicit sms: or callout: hash takes precedence over languages:. Use the explicit hash when you also need app_hash.

Verification responses#

Every successful SDK request returns a DIDWW::OTPVerification::Verification object.

Reader

Ruby type

Description

id

String

Verification identifier.

destination

String

Destination number normalized without a leading +.

delivery_method

String

Delivery method used for the verification.

fee

BigDecimal

Quoted verification fee.

status

String

Current verification status.

error_code

String or nil

Machine-readable reason for a failed, expired, or denied verification.

error_detail

String or nil

Human-readable text associated with error_code.

expires_at

Time or nil

Verification expiration time.

sms

Hash or nil

SMS-specific response fields. It is nil for phone call.

callout

Hash or nil

Phone call response fields. It is nil for SMS.

raw

Hash

Unmodified data object returned by the API.

Use to_h to retrieve the same raw response data as raw.

Reading SMS response details#

An SMS verification can include an sms object in the response. Read its fields through the convenience methods or from the raw hash:

verification.sms_template             # => "Your code is {{CODE}}"
verification.sms_language             # => "en-US"
verification.sms_interception_timeout # => 120
verification.sms_app_hash             # => "A1b2C3d4E5f", or nil
verification.sms                      # => the raw SMS Hash

sms_interception_timeout is the number of seconds an on-device client should continue listening for automatic SMS capture. It is not the verification expiration time. Manual code entry remains available until expires_at.

sms_app_hash is returned only when an app hash was stored for the verification.

sms_language is the language the API selected, which is not necessarily the first one requested. Compare it with the list you sent to detect a fallback to en-US.

Reading phone call response details#

A phone call verification includes a callout object in the response:

verification.callout_language # => "de-DE"
verification.callout          # => the raw callout Hash

callout_language is the language the announcement was played in. The announcement recordings are a different set from the SMS templates, so a tag that is honored for SMS can still fall back for a phone call.

Handling verification status#

Use the status predicates instead of comparing status strings directly:

verification.pending?
verification.verified?
verification.failed?
verification.expired?
verification.denied?
verification.finished?

finished? returns true for verified, failed, expired, and denied. It indicates that polling can stop, but it does not mean that the phone number was verified. Grant access only when verified? returns true:

current = client.get_verification(verification.id)

if current.verified?
  grant_access
elsif current.pending?
  keep_waiting
elsif current.finished?
  show_verification_error(current.error_code, current.error_detail)
else
  report_unknown_status(current.status)
end

error_code and error_detail describe an unsuccessful verification result. They are nil while the verification is pending and when it is verified. These fields are different from the errors attached to a raised APIError.

Note

The fee value is the quoted verification fee, not an immediate charge. It is billed only when the verification becomes verified. SMS or call delivery costs are billed separately.

Using phone numbers instead of IDs#

Use the by-number methods when your application retained the destination number but not the verification ID:

current = client.get_verification_by_number("+4915112345678")

result = client.report_verification_by_number(
  "+4915112345678",
  delivery_method: "sms",
  code:            "123456"
)

Supply the phone number in E.164 format. The leading + is optional. The SDK percent-encodes the number for the URL path, but it does not normalize or validate the number.

get_verification_by_number(...) returns the latest verification associated with the number. report_verification_by_number(...) reports against the unfinished verification selected by the API for that number. If no verification can receive the report, the SDK raises NotFoundError.

If another verification is started for the same application and phone number, it supersedes the previous unfinished verification. Use the ID-based methods when the report or status check must apply to the exact verification originally shown to the user.

Environments#

Choose the environment when creating the client. The SDK uses :production by default.

Environment

Base URL

:production (default)

https://verification.didww.com

:sandbox

https://verification-sandbox.didww.com

Registered environment

A custom base URL registered with register_env.

Use :sandbox while building and testing your integration:

client = DIDWW::OTPVerification::Client.new(
  key:    ENV.fetch("DIDWW_OTP_KEY"),
  secret: ENV.fetch("DIDWW_OTP_SECRET"),
  env:    :sandbox
)

Use credentials from an OTP application created in the same environment as the client. See Choose an environment for the corresponding User Panel and API URLs.

Register a custom environment, such as a local test server, and select it by name:

DIDWW::OTPVerification.configure do |config|
  config.register_env(:local, "http://localhost:3000")
end

client = DIDWW::OTPVerification::Client.new(
  key:    "your-app-key",
  secret: "your-app-secret",
  env:    :local
)

The SDK adds /api/v1 to each request path. Do not include that path when registering a custom base URL.

Configuration#

Set shared defaults with DIDWW::OTPVerification.configure. Values passed directly to Client.new override the global defaults.

DIDWW::OTPVerification.configure do |config|
  config.key       = ENV.fetch("DIDWW_OTP_KEY")
  config.secret    = ENV.fetch("DIDWW_OTP_SECRET")
  config.env       = :sandbox
  config.auth_mode = :basic

  config.faraday do |connection|
    connection.options.open_timeout = 5
    connection.options.timeout = 10
  end
end

client = DIDWW::OTPVerification::Client.new

For a multi-tenant application, pass credentials and connection options to each client:

client = DIDWW::OTPVerification::Client.new(
  key:      tenant.otp_key,
  secret:   tenant.otp_secret,
  env:      :production,
  base_url: "https://verification-proxy.example.com"
) do |connection|
  connection.proxy = "http://proxy.example.com:3128"
  connection.options.timeout = 10
end

base_url: overrides env:. The URL must contain the scheme and host but should not include /api/v1.

The SDK uses Faraday for HTTP transport. A custom Faraday adapter can be supplied with adapter:, but the adapter gem must also be added to your application.

Warning

The SDK does not log requests by default. If you add Faraday logging middleware, configure it to redact the Authorization header, application credentials, phone numbers, and submitted codes.

Authentication#

Select an authentication mode with auth_mode: on Client.new or with the global config.auth_mode setting. The OTP application’s minimum authentication mode must allow the selected mode. See Authentication.

Mode

Header

Secret

Use

:basic (default)

Basic base64(key:secret)

Required

Server-to-server integrations.

:public

Application <key>

Not used

Untrusted clients controlled through a request callback.

:application

Application <key>:<signature> and x-timestamp

Required

Signed server-to-server integrations.

HTTP Basic authentication#

:basic is the default mode. It sends the application key as the username and the secret as the password:

client = DIDWW::OTPVerification::Client.new(
  key:       ENV.fetch("DIDWW_OTP_KEY"),
  secret:    ENV.fetch("DIDWW_OTP_SECRET"),
  auth_mode: :basic
)

Public authentication#

:public sends only the application key. A callback URL must be configured on the OTP application so your backend can approve each start request:

client = DIDWW::OTPVerification::Client.new(
  key:       "your-app-key",
  auth_mode: :public
)

Public authentication is primarily intended for untrusted clients. A Ruby server can use it when the verification must still pass through the callback approval flow.

Signed application authentication#

:application signs each request with HMAC-SHA256 and sends an x-timestamp header. The SDK builds the signature from the final request bytes, so your application does not need to implement signing:

client = DIDWW::OTPVerification::Client.new(
  key:       ENV.fetch("DIDWW_OTP_KEY"),
  secret:    ENV.fetch("DIDWW_OTP_SECRET"),
  auth_mode: :application
)

The application secret must be valid URL-safe Base64. The client validates it during initialization and raises ConfigurationError when it cannot be decoded.

Warning

Treat the application secret as a password. Store it in a secrets manager or protected environment variable, never in source control or client-side code.

Handling a denied start#

With :public authentication, the Verification API sends a synchronous request to the OTP application’s callback URL before attempting delivery. A denied start can still return 201 Created because the verification record was created successfully. In this case, start_verification(...) returns a Verification with status denied instead of raising an exception.

Handle the initial status before asking the user to enter a code:

verification = client.start_verification(
  destination:     "+4915112345678",
  delivery_method: "sms"
)

if verification.pending?
  show_code_entry(verification)
elsif verification.denied?
  show_unavailable(verification.error_code, verification.error_detail)
else
  handle_unexpected_start_status(verification.status)
end

An explicit callback denial returns error_code denied_by_callback. An unusable callback response returns denied_invalid_callback_response. If no callback URL is configured, the result is denied_missing_callback_url. See Callbacks for the complete flow.

Verifying inbound callbacks#

Use CallbackVerifier to verify a signed verification_request callback before parsing its body or deciding whether to allow the verification. The verifier checks the signature and enforces a five-minute timestamp window by default.

Select the application secret using the key from the Authorization header, then verify the exact request bytes:

key, signature = DIDWW::OTPVerification::CallbackVerifier
                 .parse_authorization(request.headers["Authorization"])

secret = find_application_secret(key)

valid = secret && DIDWW::OTPVerification::CallbackVerifier.new(secret: secret).valid?(
  method:       request.request_method,
  path:         request.path,
  content_type: request.content_type,
  body:         request.raw_post,
  timestamp:    request.headers["x-timestamp"],
  signature:    signature
)

unless valid
  # Return 401 Unauthorized and stop processing the callback.
end

# After verification, apply your rules and return {"action":"allow"} or
# {"action":"deny"} with a 2xx status.

Use the raw body exactly as received. Parsing and serializing the JSON again can change the bytes and invalidate an otherwise correct signature. The path value must exactly match the path of the configured callback URL because it is part of the signature.

The callback verifier can be loaded without the HTTP client in a callback-only service:

require "didww/otp_verification/callback_verifier"

Set a different timestamp tolerance only when required by your deployment:

verifier = DIDWW::OTPVerification::CallbackVerifier.new(
  secret:    application_secret,
  tolerance: 300
)

Rails#

RailsCallbackVerifier reads the signed values from an ActionDispatch::Request. Rails support is optional and must be required explicitly:

require "didww/otp_verification/rails"

Verify the request in a before_action. After verification succeeds, return the callback decision from the controller action:

class DidwwCallbacksController < ActionController::API
  before_action :verify_didww_signature

  def create
    allowed = expected_destination?(params.dig(:data, :destination))
    render json: {action: allowed ? "allow" : "deny"}
  end

  private

  def verify_didww_signature
    verifier = DIDWW::OTPVerification::RailsCallbackVerifier.new(
      secret: ENV.fetch("DIDWW_OTP_SECRET")
    )

    return if verifier.valid?(request)

    head :unauthorized
  end
end

Route the same path that is configured as the callback URL on the OTP application:

# config/routes.rb
post "/callbacks/didww", to: "didww_callbacks#create"

The example above uses one application secret. If the endpoint receives callbacks for multiple OTP applications, parse the application key first and select the matching secret before verifying the request.

Error handling#

Client configuration problems and API responses raise errors under DIDWW::OTPVerification::Error. Network failures raised by Faraday are not wrapped by the SDK.

Error class

HTTP status

When it is raised

ConfigurationError

Not applicable

The client has a missing key or secret, an unknown environment or auth mode, or an invalid signing secret.

UnauthorizedError

401

Authentication failed or the authentication mode is not allowed.

BalanceInsufficientError

402

The account balance is insufficient.

NotFoundError

404

The requested verification could not be found, either because no such verification exists for the OTP application or because it passed the retention period.

ValidationError

400 or 422

The request is invalid, including an incorrect code.

ServerError

5xx

The API encountered a server error.

APIError

Other HTTP status

The API returned another unsuccessful status or an unexpected successful response body.

Every APIError provides the following readers:

Reader

Description

status

HTTP response status.

errors

Array of ErrorItem objects from the API response.

code

Machine-readable code from the first error, or nil.

codes

Machine-readable codes from all returned errors.

response

Original Faraday response.

Each ErrorItem contains code and detail. Branch on code in application logic and use detail only as display text. Unknown error codes remain available as raw strings. An error produced by a proxy or another non-JSON response can have an empty errors array. See Errors and status codes.

The following example handles an incorrect code separately from authentication, API, and network failures:

begin
  result = client.report_verification(
    verification.id,
    delivery_method: "sms",
    code:            submitted_code
  )

  grant_access if result.verified?
rescue DIDWW::OTPVerification::ValidationError => error
  case error.code
  when "code_invalid"
    show_code_error(error.errors.first&.detail)
  when "too_many_attempts"
    require_new_verification
  else
    show_request_error(error.code, error.message)
  end
rescue DIDWW::OTPVerification::UnauthorizedError
  report_configuration_error
rescue DIDWW::OTPVerification::APIError => error
  report_api_error(error.status, error.codes)
rescue Faraday::TimeoutError, Faraday::ConnectionFailed => error
  show_network_error(error)
end

Timeouts and retries#

Set connection and response timeouts through the Faraday configuration block:

client = DIDWW::OTPVerification::Client.new(
  key:    ENV.fetch("DIDWW_OTP_KEY"),
  secret: ENV.fetch("DIDWW_OTP_SECRET")
) do |connection|
  connection.options.open_timeout = 5
  connection.options.timeout = 10
end

The SDK does not retry requests automatically. If you add retry middleware, choose retry behavior by operation:

  • A status GET request can be retried after a temporary network failure.

  • Do not automatically retry start_verification(...). The first request may already have created a verification, superseded an earlier one, or started delivery.

  • Do not automatically retry a report request. Each report can consume one of the allowed attempts.

If a report request times out after it may have reached the API, retrieve the verification status before deciding whether to submit the value again.