# Python

> Read identification events with a secret key and verify signed webhooks from Python, with a synchronous client and an asyncio client that share one API.

Last updated: 2026-09-17

The `fingerly` package reads stored events by request ID and verifies webhook signatures. It ships two clients with the same methods: `Fingerly` for synchronous code such as Django and Flask, and `AsyncFingerly` for `asyncio` code such as FastAPI, Starlette and aiohttp.

## Requirements

- Python 3.9 or newer.
- 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 pip
pip install fingerly
```

```bash uv
uv add fingerly
```

```bash Poetry
poetry 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.

```python Python
import os
from fingerly import Fingerly

fingerly = Fingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])
event = fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

```python Python (async)
import os
from fingerly import AsyncFingerly

fingerly = AsyncFingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])
event = await fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

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

```python Python
from datetime import datetime, timedelta, timezone
from fingerly import Fingerly, FingerlyAPIError

fingerly = Fingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])

def decide(order_id: str, request_id: str) -> str:
    try:
        event = fingerly.events.get(request_id)
    except FingerlyAPIError as error:
        if error.status == 404:
            return "refuse"
        raise

    if event.tag != f"checkout:{order_id}":
        return "refuse"
    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):
        return "refuse"

    if event.suspect_level == "high":
        return "review"
    if event.suspect_level == "medium":
        return "challenge"
    return "allow"
```

```python Python (async)
from datetime import datetime, timedelta, timezone
from fingerly import AsyncFingerly, FingerlyAPIError

fingerly = AsyncFingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])

async def decide(order_id: str, request_id: str) -> str:
    try:
        event = await fingerly.events.get(request_id)
    except FingerlyAPIError as error:
        if error.status == 404:
            return "refuse"
        raise

    if event.tag != f"checkout:{order_id}":
        return "refuse"
    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):
        return "refuse"

    return {"high": "review", "medium": "challenge"}.get(event.suspect_level, "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.

```python Python
from flask import Flask, abort, request
from fingerly import verify_webhook

@app.post("/webhooks/fingerly")
def fingerly_webhook():
    payload = request.get_data()
    if not verify_webhook(
        secret=os.environ["FINGERLY_WEBHOOK_SECRET"],
        payload=payload,
        timestamp=request.headers.get("x-fingerly-timestamp"),
        signature=request.headers.get("x-fingerly-signature"),
    ):
        abort(400)

    event = json.loads(payload)
    queue.enqueue(event["id"], event)   # deduplicate on the event ID
    return "", 204
```

```python Python (async)
from fastapi import FastAPI, HTTPException, Request, Response
from fingerly import verify_webhook

@app.post("/webhooks/fingerly", status_code=204)
async def fingerly_webhook(request: Request) -> Response:
    payload = await request.body()
    if not verify_webhook(
        secret=os.environ["FINGERLY_WEBHOOK_SECRET"],
        payload=payload,
        timestamp=request.headers.get("x-fingerly-timestamp"),
        signature=request.headers.get("x-fingerly-signature"),
    ):
        raise HTTPException(status_code=400)

    event = json.loads(payload)
    await queue.enqueue(event["id"], event)
    return Response(status_code=204)
```

## Sync or async

|  | `Fingerly` | `AsyncFingerly` |
| --- | --- | --- |
| Use in | Django, Flask, scripts, Celery tasks | FastAPI, Starlette, aiohttp, Quart |
| Calls | `fingerly.events.get(id)` | `await fingerly.events.get(id)` |
| HTTP client | One pooled connection per client | One pooled connection per client, per event loop |
| Closing | `fingerly.close()`, or `with Fingerly(...) as fingerly:` | `await fingerly.aclose()`, or `async with AsyncFingerly(...) as fingerly:` |

```python lifespan.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fingerly import AsyncFingerly

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with AsyncFingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"]) as fingerly:
        app.state.fingerly = fingerly
        yield

app = FastAPI(lifespan=lifespan)
```

## API

| Member | Returns | Notes |
| --- | --- | --- |
| `Fingerly(secret_key, endpoint=None, timeout=10.0)` | client | The key's prefix decides the regional API. `AsyncFingerly` takes the same arguments. |
| `events.get(request_id)` | `Event` | Raises `FingerlyAPIError` with `.status` for a non-2xx response. |
| `events.list(from_=None, to=None, page=1, limit=10, visitor=None, level=None)` | `EventPage` | `EventPage` has `rows`, `page` and `page_size`. `from_` and `to` accept `datetime`. |
| `verify_webhook(secret, payload, timestamp, signature, tolerance_seconds=300)` | `bool` | Synchronous in both clients. Never raises for bad input. |

`Event` exposes every field of the event as an attribute, with `occurred_at` parsed to a timezone-aware `datetime`.
