# Rust

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

Last updated: 2026-09-17

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](https://docs.fingerly.io/docs/api-keys), and a webhook signing secret if you receive [webhooks](https://docs.fingerly.io/docs/webhooks).

## Install

```bash Cargo
cargo add fingerly
```

```toml Cargo.toml
[dependencies]
fingerly = "0.1"
```

## 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 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](https://docs.fingerly.io/reference/get-event#response). `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](https://docs.fingerly.io/docs/server-side-verification).

```rust 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 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

| 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 `Option`s, and `suspect_level` is a `Level` enum.
