The fingerly crate reads stored events by request ID and verifies webhook signatures. The client is async, built on Tokio and reqwest, cheap to clone, and safe to share across tasks.
Requirements
- Rust 1.75 or newer, with the Tokio runtime.
- A secret key, and a webhook signing secret if you receive webhooks.
Install
cargo 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.
let fingerly = fingerly::Client::new(std::env::var("FINGERLY_SECRET_KEY")?);
let event = fingerly.events().get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4").await?;
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.
async fn decide(fingerly: &fingerly::Client, order_id: &str, request_id: &str) -> Result<Decision, fingerly::Error> {
let event = match fingerly.events().get(request_id).await {
Ok(event) => event,
Err(fingerly::Error::Api { status: 404, .. }) => return Ok(Decision::Refuse),
Err(error) => return Err(error),
};
if event.tag.as_deref() != Some(&format!("checkout:{order_id}")) {
return Ok(Decision::Refuse);
}
if chrono::Utc::now() - event.occurred_at > chrono::Duration::minutes(2) {
return Ok(Decision::Refuse);
}
Ok(match event.suspect_level {
Some(Level::High) => Decision::Review,
Some(Level::Medium) => Decision::Challenge,
_ => Decision::Allow,
})
}
Verify a webhook
Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.
async fn fingerly_webhook(State(state): State<AppState>, headers: HeaderMap, body: Bytes) -> StatusCode {
let header = |name| headers.get(name).and_then(|v| v.to_str().ok()).unwrap_or_default();
if !fingerly::webhook::verify(
&state.webhook_secret,
&body,
header("x-fingerly-timestamp"),
header("x-fingerly-signature"),
) {
return StatusCode::BAD_REQUEST;
}
let event: fingerly::WebhookEvent = serde_json::from_slice(&body).unwrap();
state.queue.enqueue(event.id.clone(), body).await; // deduplicate on the event ID
StatusCode::NO_CONTENT
}
API
| Member | Returns | Notes |
|---|---|---|
fingerly::Client::new(secret_key) | Client | Client::builder() sets an endpoint or a reqwest::Client. |
client.events().get(request_id).await | Result<Event, fingerly::Error> | A non-2xx response is Error::Api { status, .. }. |
client.events().list(&ListEvents).await | Result<EventPage, fingerly::Error> | ListEvents has from, to, page, limit, visitor, level. |
fingerly::webhook::verify(secret, body, timestamp, signature) | bool | Five minutes of tolerance. |
Events deserialize with serde. Optional fields are Options, and suspect_level is a Level enum.