Official Smile ID server-side SDK for Ruby, covering the V3 APIs.
The SDK handles authentication, request serialization, retries and typed errors so you can call Smile ID from Ruby with plain method calls. You never handle tokens yourself.
Requires Ruby 3.0 or later.
Add the gem to your Gemfile:
gem "usesmileid"Or install it directly:
gem install usesmileidConstruct one client with your partner id and API key. The client is thread-safe and can be shared across your application.
require "usesmileid"
smile = SmileID::Client.new(
partner_id: "1234",
api_key: ENV.fetch("SMILE_API_KEY"),
environment: :sandbox, # default
default_callback_url: "https://app.example.com/cb" # optional
)Partner ids are displayed zero-padded (for example 002) but must be passed without leading zeros (2).
Authentication is internal: the SDK fetches a short-lived token from the API, caches it until just before expiry, and refreshes it once automatically if a request returns 401. You never see or pass a token.
The client uses the sandbox by default. Set environment: :production to go live. Only :sandbox and :production are named.
| Environment | Base URL |
|---|---|
:sandbox (default) |
https://testapi.smileidentity.com |
:production |
https://api.smileidentity.com |
Any other host needs an explicit base_url:, which wins over environment:
smile = SmileID::Client.new(
partner_id: "2",
api_key: ENV.fetch("SMILE_API_KEY"),
base_url: "https://your-environment.example.com"
)A custom base_url must be an absolute https URL with no query string or fragment. There is deliberately no way to use plain http. Callback URLs (default_callback_url and any per-request callback_url) must also be https; an insecure callback raises a validation error before any request is sent.
Non-production environments match test identities on given names + last name + email; an unrecognised identity resolves to block.
| Option | Default | Purpose |
|---|---|---|
timeout |
30 | Per-request timeout in seconds. Every method also accepts a per-call timeout: override. |
max_retries |
2 | Retries for idempotent calls only (status and services reads, and the internal token fetch). Job submissions are never retried automatically. |
http_client |
SDK default | Inject your own Faraday::Connection for testing or proxies. |
All verification submissions need consent and user details.
consent = SmileID::Consent.granted(
granted_at: Time.now.utc,
notice_language: "EN",
notice_privacy_policy_url: "https://example.com/privacy"
)
user_details = {
given_names: "John",
last_name: "Doe",
email: "john@example.com" # at least one of email / phone_number is required
}Image inputs accept a file path ("selfie.jpg"), raw bytes, an IO object, or a hash such as { path: "front.png", content_type: "image/png" }.
accepted = smile.enhanced_kyc.verify(
country: "NG",
id_type: "NIN",
id_number: "12345678901",
user_details: user_details,
consent: consent,
user_id: "user_01h8x9y2z3a4b5c6d7e8f9g0h1" # optional
)
accepted.job_id # => "job_..."
accepted.accepted? # => trueaccepted = smile.documents.verify(
selfie_image: "selfie.jpg",
liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
document: "doc_front.jpg",
document_back: "doc_back.jpg", # optional
country: "NG",
user_details: user_details,
consent: consent
)Same shape as document verification, but id_type is required.
accepted = smile.documents.verify_enhanced(
id_type: "PASSPORT",
selfie_image: "selfie.jpg",
liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
document: "doc_front.jpg",
country: "NG",
user_details: user_details,
consent: consent
)accepted = smile.biometric_kyc.verify(
selfie_image: "selfie.jpg",
liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
country: "NG",
id_type: "NIN",
id_number: "12345678901",
user_details: user_details,
consent: consent
)accepted = smile.biometric.enroll(
selfie_image: "selfie.jpg",
liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
user_details: user_details,
consent: consent,
user_id: "user-42" # optional partner-provided id
)user_id is required and must match an enrolled user. Images are required unless you set use_enrolled_image: true.
accepted = smile.biometric.authenticate(
user_id: "user-42",
selfie_image: "selfie.jpg",
liveness_images: ["live1.jpg", "live2.jpg", "live3.jpg",
"live4.jpg", "live5.jpg", "live6.jpg"],
user_details: user_details,
consent: consent
)accepted = smile.biometric.compare(
selfie_image: "selfie.jpg",
comparison_image: "id_photo.jpg",
comparison_image_type: "ID_PHOTO", # DOCUMENT | ID_PHOTO | PORTRAIT
user_details: user_details,
consent: consent
)status = smile.verifications.retrieve("job_01h8x9y2z3a4b5c6d7e8f9g0h1")
status.status # "processing", "not_found", or the decision: "clear", "block", "attention", "error"
status.complete? # true when terminal, i.e. neither processing nor not_found
status.message # e.g. "Job completed"A job the API does not know yet returns a status of not_found rather than raising an error, so you can poll safely right after submission.
status = smile.verifications.wait_until_complete(
"job_01h8x9y2z3a4b5c6d7e8f9g0h1",
interval: 2, # seconds between polls (default 2)
timeout: 60 # give up after this many seconds (default 60)
)Polling stops as soon as the job reaches a terminal decision (clear, block, attention or error). Raises SmileID::Errors::TimeoutError if the job does not complete in time. Pass treat_not_found_as_pending: false to return immediately when the job is unknown instead of polling on.
smile.verifications.replay(
"job_01h8x9y2z3a4b5c6d7e8f9g0h1",
callback_url: "https://app.example.com/cb" # optional override
)Replaying a job that is still processing raises SmileID::Errors::ConflictError.
smile.users.report_fraud(
"user-42",
is_fraud: true,
reason: "ACCOUNT_TAKEOVER",
reported_by: "risk@example.com"
)Or use the convenience wrappers:
smile.users.flag_fraud("user-42", reason: "ACCOUNT_TAKEOVER", reported_by: "risk@example.com")
smile.users.clear_fraud("user-42", notes: "Cleared after review", reported_by: "risk@example.com")When flagging, reason is required (and notes too if the reason is OTHER). When clearing, notes is required.
Bank codes, supported ID types and supported documents need no authentication.
smile.services.bank_codes(country: "NG").bank_codes
# => [{ "code" => "044", "country" => "NG", "name" => "Access Bank" }, ...]
smile.services.supported_id_types(country: "NG").id_types
# => [{ "country" => "NG", "type" => "BVN", "label" => ..., "regex" => ..., ... }, ...]
smile.services.supported_documents(country_code: "NG").valid_documents
# => [{ "country" => { "code" => "NG", ... }, "id_types" => [...] }, ...]
smile.services.id_status(country: "NG", id_type: "BVN")
# => #<IdStatusResponse last_known_status="online" last_hour_success_rate="95%" ...>Every API failure raises a typed error under SmileID::Errors, keyed on the HTTP status:
| Error | Raised on |
|---|---|
InvalidRequestError |
400, 415, and failed client-side validation (ValidationError subclass) |
AuthenticationError |
401 after one automatic token refresh |
PaymentRequiredError |
402 — insufficient wallet balance |
PermissionError |
403 |
NotFoundError |
404 (except verifications.retrieve, which returns a not_found status) |
ConflictError |
409 — for example replaying a job that is still processing |
PayloadTooLargeError |
413 |
RateLimitError |
429 |
APIError |
any 5xx |
ConnectionError |
network failure or timeout with no HTTP response |
TimeoutError |
wait_until_complete deadline passed (no HTTP response) |
UnexpectedResponseError |
a 2xx response whose body is not the expected JSON object, for example proxy interference |
Each error exposes status_code, status, message, code, request_id and raw_body.
begin
smile.enhanced_kyc.verify(...)
rescue SmileID::Errors::PaymentRequiredError => e
e.status_code # 402
e.message # "Insufficient wallet balance."
rescue SmileID::Errors::SmileIDError => e
# catch-all for anything the API raised
endThe SDK sends three telemetry headers on every request: SmileID-Source-SDK (ruby), SmileID-Source-SDK-Version and a User-Agent identifying the SDK and Ruby version. These identify the SDK for observability. They are never used for authentication and carry no personal data.
bundle install
bundle exec rspec # unit tests, fully offline
bundle exec rubocopThe end-to-end sandbox test runs only when SMILE_PARTNER_ID and SMILE_API_KEY are set in the environment; otherwise it skips. Set SMILE_BASE_URL to run it against a host other than the sandbox.
See SECURITY.md for how to report a security issue.