Ruby SDK
The NoResponse Ruby SDK is the server-side client for Ruby 3.2 and later. It covers every route in the interactive reference, verifies webhooks and turns your auth provider’s verified claims into auth_context. Every method returns an envelope: result holds the typed object and headers holds the response headers (X-Request-ID, Retry-After, Idempotent-Replayed).
Keep the client on your backend, where your API key lives.
Install
Section titled “Install”The gem is named noresponse. RubyGems publishing is pending, so install it from the GitHub tag at github.com/noresponsehq/ruby. In your Gemfile:
gem "noresponse", git: "https://github.com/noresponsehq/ruby.git", tag: "v2.0.0"Use the latest tag from the repository’s tags page. Once the gem is on RubyGems, the line is gem "noresponse", "~> 2.0".
Client
Section titled “Client”require "noresponse"
Models = NoResponse::Models
client = NoResponse::NoResponseClient.new(api_key: ENV.fetch("NORESPONSE_API_KEY"))client = NoResponse::NoResponseClient.new( api_key: key, server_url: "https://api.noresponse.org", timeout_ms: 10_000, client: Faraday.new { |f| f.proxy = "http://proxy:8080" }, retry_config: NoResponse::Utils::RetryConfig.new(strategy: "none"))| Option | Meaning |
|---|---|
api_key |
Your nrk_ key. Read it from NORESPONSE_API_KEY as above. |
security_source |
A lambda returning Models::Shared::Security.new(api_key: key) on each request, for a secret that rotates. |
server_url |
The API host. The default is https://api.noresponse.org. |
timeout_ms |
Timeout per request, in milliseconds. |
client |
Your own Faraday connection, for a proxy or TLS settings. |
retry_config |
Overrides the retry policy for every call. See Retries. |
Every method also takes timeout_ms:, retries: and http_headers: for one call.
Methods
Section titled “Methods”| Call | Does |
|---|---|
client.assessments.create(body:, idempotency_key:) |
POST /v1/assessments. Answers 200 (complete or partial) or 202 (pending). |
client.assessments.get(assessment_id:, response_mode:) |
GET /v1/assessments/{id}. |
client.assessments.wait(assessment_id, timeout:) |
Polls get until the assessment is no longer pending. |
client.assessments.query(request:) |
POST /v1/assessments/query. One page of an account’s history. |
client.assessments.record_outcome(assessment_id:, body:, idempotency_key:) |
POST /v1/assessments/{id}/outcomes. |
client.keys.rotate(request:) |
POST /v1/keys/rotate. |
NoResponse::Webhooks.construct_event(payload, headers, secrets) |
Verifies a webhook delivery. No request is made. |
Create an assessment
Section titled “Create an assessment”require "noresponse"
Models = NoResponse::Models
client = NoResponse::NoResponseClient.new(api_key: ENV.fetch("NORESPONSE_API_KEY"))
response = client.assessments.create( body: Models::Shared::AssessmentRequest.new( account_id: "usr_8f2k1", identifiers: Models::Shared::AssessmentRequestIdentifiers.new( name: "Jordan Lee", ip: "81.2.69.160" ), policy_context: Models::Shared::AssessmentRequestPolicyContext.new( flow: Models::Shared::Flow::SIGNUP, region: "GB" ), request_id: "req_01", deadline_ms: 400, response_mode: Models::Shared::ResponseMode::STANDARD, client_context: Models::Shared::AssessmentRequestClientContext.new( timezone: "Europe/London", locale: "en-GB" ) ), idempotency_key: "signup-usr_8f2k1-01JB3C4D")
assessment = response.resultif assessment.status == Models::Shared::Status::PENDING # No recommendation yet. Apply your fallback, then wait or take the webhook.else puts assessment.recommendation # allow, step_up or reviewendThe request mirrors the contract: identifiers holds email, username, phone, name and ip, and policy_context holds flow and region. The limits are on Create an assessment. The recommendation is advice: your application decides what to do.
The method returns the assessment for both a 200 and a 202. complete and partial carry risk and recommendation; pending carries neither.
Pending and wait
Section titled “Pending and wait”settled = client.assessments.wait(assessment.assessment_id, timeout: 10)wait polls get until the status is no longer pending, sleeping for Retry-After between polls with a one second floor. It raises NoResponse::WaitTimeoutError, which carries assessment_id and the last pending assessment as last, once the timeout (30 seconds by default) is reached. NoResponse::Wait.wait(client, assessment_id) does the same. Apply your fallback first and let the result arrive by webhook or a background wait, so the signup is never held open.
latest = client.assessments.get( assessment_id: assessment.assessment_id, response_mode: Models::Shared::ResponseMode::DETAILED).resultDetailed responses need the detailed scope.
Query and paging
Section titled “Query and paging”cursor = nilloop do page = client.assessments.query( request: Models::Shared::AssessmentQuery.new(account_id: "usr_8f2k1", limit: 50, cursor: cursor) ).result page.items.each { |item| puts item.assessment_id } # newest first cursor = page.next_cursor break if cursor.nil?endnext_cursor is nil on the last page. The query needs a key with the history scope. See Get and query assessments.
Record an outcome
Section titled “Record an outcome”client.assessments.record_outcome( assessment_id: assessment.assessment_id, body: Models::Shared::AssessmentOutcomeRequest.new( outcome: Models::Shared::Outcome::CONFIRMED_ABUSE, observed_at: "2026-10-20T05:00:00Z", external_ref: "case-123", notes: "Card testing from this account." ), idempotency_key: "outcome-case-123")Outcome has CONFIRMED_ABUSE, ACCOUNT_TAKEOVER, CHARGEBACK, FALSE_POSITIVE, LEGITIMATE and UNKNOWN. The idempotency key stays bound for as long as the outcome is kept, so derive it from your case.
Key rotation
Section titled “Key rotation”rotation = client.keys.rotate( request: Models::Shared::KeyRotationRequest.new(overlap_seconds: 86_400)).resultrotation.key # the new key, shown oncerotation.previous_key_expires_at # when the key that made this call stops workingThe new key has the scopes of the key that made the call. Store it at once and roll it out before previous_key_expires_at. See Authentication.
Idempotency keys
Section titled “Idempotency keys”create and record_outcome send a fresh UUID as the Idempotency-Key when you pass none, and the same key on every retry of that call, so a retried request is recorded once. Pass your own key, derived from your signup attempt, to make a retry from your side replay the same assessment: the API answers the latest version with Idempotent-Replayed: true. The same key with a different body answers 409 (ConflictError).
Errors
Section titled “Errors”Every API error is a NoResponse::Models::Errors::NoResponseError. The problem-document errors carry status, type, title, detail, code, request_id and invalid_params.
Errors = NoResponse::Models::Errors
begin client.assessments.create(body: request)rescue Errors::RateLimitError => e log(e.request_id) # retries already honoured Retry-After and ran outrescue Errors::NoResponseError => e warn "#{e.class}: #{e.message}"rescue Faraday::Error # DNS, TLS or connection failure: no answer at all.end# Apply your fallback.| Class | When |
|---|---|
BadRequestError |
400: the body or a header is malformed. See invalid_params. |
AuthenticationError |
401: a missing, malformed, revoked or expired key. |
PermissionDeniedError |
403: the key lacks the operation’s scope. |
NotFoundError |
404: an unknown, expired or foreign assessment. |
ConflictError |
409: an idempotency key reused with a different body, or a key already rotating. |
RateLimitError |
429, after the retries (which honour Retry-After) ran out. |
APIStatusError |
Any other 4xx or 5xx, including 503. It has status_code, body and raw_response. |
Quote request_id when you contact us. The codes are listed on Errors and limits.
Retries
Section titled “Retries”The client retries connection errors, 408, 429 and every 5xx answer with exponential backoff: 500 ms first, 8 seconds at most between attempts and 30 seconds in all. A 409 and every other 4xx are not retried. Every attempt of a create or outcome call sends the same Idempotency-Key, so a retried call is recorded once.
Override the policy for the client with retry_config: or for one call with retries::
client.assessments.create( body: request, retries: NoResponse::Utils::RetryConfig.new(strategy: "none"))Webhooks
Section titled “Webhooks”Verify the exact bytes you received, then act on the typed event. secrets maps each whk_ key id to its secret; hold both during a rotation.
# Rack or Sinatrapost "/webhooks/noresponse" do payload = request.body.read secrets = { ENV.fetch("NORESPONSE_WEBHOOK_KEY_ID") => ENV.fetch("NORESPONSE_WEBHOOK_SECRET") }
begin event = NoResponse::Webhooks.construct_event(payload, request.env, secrets) rescue NoResponse::Webhooks::VerificationError => e halt 400, e.code.to_s end
if event.type == "endpoint.verification" content_type :json next NoResponse::Webhooks.verification_response(event).to_json end
if event.expired? # Forget the assessment: GET now answers 404. else # event.data is the full assessment. Keep the highest assessment_version. end status 204endconstruct_event(payload, headers, secrets, tolerance_seconds: 300) takes the raw body, a hash of headers in any letter case (a Rack env works) and a map from webhook key id (whk_ and 26 characters) to secret. It checks that each NR-Webhook-* header is present exactly once, that the timestamp is within the tolerance, that the key id names a secret you hold, that the signature over timestamp + "." + raw body matches in constant time, and that the body’s id equals NR-Webhook-Id. A failure raises NoResponse::Webhooks::VerificationError, whose code is :missing_header, :duplicate_header, :invalid_timestamp, :unknown_key, :invalid_signature or :invalid_payload.
The event has id, type, created_at and data. data is an assessment, an expiry notice (expired? is true) or an endpoint verification. Deliveries arrive at least once: deduplicate on event.id, apply an assessment only when its assessment_version is higher than the one you hold, and always apply an expiry. See Webhooks.
Auth adapters
Section titled “Auth adapters”auth_context = NoResponse::Auth.from_clerk(verified_claims, { email_verified: true })NoResponse::Auth has from_workos, from_clerk, from_auth0, from_supabase, from_firebase and from_oidc(provider, claims, overrides). Each takes the claims your backend has already verified, as a hash such as JSON.parse gives (symbol keys also work), and returns an AuthContext with verified true and transport set to backend. Pass it as auth_context: on AssessmentRequest.new.
Each adapter reads the standard claims (issuer, subject, audience, session and token ids, times), the provider’s organisation, role and scope claims, and multi-factor evidence where the provider exposes it. The optional overrides set facts the token leaves out, using the contract’s field names: email_verified, phone_verified, account_created_at, session_created_at and the like. For from_oidc, provider is a lowercase slug of 2 to 32 characters. A claim that is missing or out of range is left out, not turned into a stronger assurance, and an adapter raises ArgumentError when the claims have no valid issuer and subject. Pass verified claims, never the raw token.