The fingerly gem reads stored events by request ID and verifies webhook signatures. It has no runtime dependencies beyond the standard library.
Requirements
- Ruby 3.1 or newer.
- A secret key, and a webhook signing secret if you receive webhooks.
Install
bundle add fingerly
Read an event
Create one client with your secret key and reuse it. The key decides the regional API and the environment the client reads.
require "fingerly"
fingerly = Fingerly::Client.new(secret_key: ENV.fetch("FINGERLY_SECRET_KEY"))
event = fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
An event has the fields listed in Get an event. suspect_score is null when the request was not scored.
Verify a checkout
Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See server-side verification.
def decide(order_id, request_id)
event = fingerly.events.get(request_id)
return "refuse" unless event.tag == "checkout:#{order_id}"
return "refuse" if event.occurred_at < Time.now - 120
case event.suspect_level
when "high" then "review"
when "medium" then "challenge"
else "allow"
end
rescue Fingerly::APIError => e
raise unless e.status == 404
"refuse"
end
Verify a webhook
Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.
class FingerlyWebhooksController < ActionController::API
def create
payload = request.raw_post
valid = Fingerly::Webhook.verify(
secret: ENV.fetch("FINGERLY_WEBHOOK_SECRET"),
payload: payload,
timestamp: request.headers["x-fingerly-timestamp"],
signature: request.headers["x-fingerly-signature"],
)
return head :bad_request unless valid
event = JSON.parse(payload)
FingerlyEventJob.perform_later(event) # deduplicate on event["id"]
head :no_content
end
end
Rails
FINGERLY = Fingerly::Client.new(secret_key: Rails.application.credentials.dig(:fingerly, :secret_key))
API
| Member | Returns | Notes |
|---|---|---|
Fingerly::Client.new(secret_key:, endpoint: nil, timeout: 10) | client | Thread-safe; create one per process. |
events.get(request_id) | Fingerly::Event | Raises Fingerly::APIError with #status for a non-2xx response. |
events.list(from: nil, to: nil, page: 1, limit: 10, visitor: nil, level: nil) | Fingerly::EventPage | Has rows, page and page_size. |
Fingerly::Webhook.verify(secret:, payload:, timestamp:, signature:) | true or false | Five minutes of tolerance. |