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.
Source: GitHub repository
Requires: Ruby
>= 3.1
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.
:basicis the SDK default and requires the application key and secret.:applicationalso requires both credentials and signs each request.:publicuses 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
allowordeny. 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 a verification for one phone number. |
|
|
Submit a code for a specific verification. |
|
|
Retrieve a specific verification. |
|
|
Submit a code without providing a verification ID. |
|
|
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 |
|---|---|---|
|
|
|
|
|
|
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 |
|---|---|
|
Preferred message-template languages as BCP 47 tags, ordered from most to least preferred. See Supported languages. |
|
Android SMS Retriever application hash. It must contain exactly 11 characters from
|
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 |
|---|---|
|
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 |
|---|---|---|
|
|
Verification identifier. |
|
|
Destination number normalized without a leading |
|
|
Delivery method used for the verification. |
|
|
Quoted verification fee. |
|
|
Current verification status. |
|
|
Machine-readable reason for a failed, expired, or denied verification. |
|
|
Human-readable text associated with |
|
|
Verification expiration time. |
|
|
SMS-specific response fields. It is |
|
|
Phone call response fields. It is |
|
|
Unmodified |
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 |
|---|---|
|
|
|
|
Registered environment |
A custom base URL registered with |
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 |
|---|---|---|---|
|
|
Required |
Server-to-server integrations. |
|
|
Not used |
Untrusted clients controlled through a request callback. |
|
|
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 |
|---|---|---|
|
Not applicable |
The client has a missing key or secret, an unknown environment or auth mode, or an invalid signing secret. |
|
|
Authentication failed or the authentication mode is not allowed. |
|
|
The account balance is insufficient. |
|
|
The requested verification could not be found, either because no such verification exists for the OTP application or because it passed the retention period. |
|
|
The request is invalid, including an incorrect code. |
|
|
The API encountered a server error. |
|
Other HTTP status |
The API returned another unsuccessful status or an unexpected successful response body. |
Every APIError provides the following readers:
Reader |
Description |
|---|---|
|
HTTP response status. |
|
Array of |
|
Machine-readable code from the first error, or |
|
Machine-readable codes from all returned errors. |
|
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
GETrequest 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.