SDKs

Rust

Read identification events with a secret key and verify signed webhooks from Rust, with an async client built on Tokio and typed events.

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.

Rust
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.

Rust
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.

Rust
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

MemberReturnsNotes
fingerly::Client::new(secret_key)ClientClient::builder() sets an endpoint or a reqwest::Client.
client.events().get(request_id).awaitResult<Event, fingerly::Error>A non-2xx response is Error::Api { status, .. }.
client.events().list(&ListEvents).awaitResult<EventPage, fingerly::Error>ListEvents has from, to, page, limit, visitor, level.
fingerly::webhook::verify(secret, body, timestamp, signature)boolFive minutes of tolerance.

Events deserialize with serde. Optional fields are Options, and suspect_level is a Level enum.