SDKs

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.

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

Install

pip install 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.

import os
from fingerly import Fingerly

fingerly = Fingerly(secret_key=os.environ["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.

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"

Verify a webhook

Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.

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

Sync or async

`Fingerly``AsyncFingerly`
Use inDjango, Flask, scripts, Celery tasksFastAPI, Starlette, aiohttp, Quart
Callsfingerly.events.get(id)await fingerly.events.get(id)
HTTP clientOne pooled connection per clientOne pooled connection per client, per event loop
Closingfingerly.close(), or with Fingerly(...) as fingerly:await fingerly.aclose(), or async with AsyncFingerly(...) as fingerly:
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

MemberReturnsNotes
Fingerly(secret_key, endpoint=None, timeout=10.0)clientThe key's prefix decides the regional API. AsyncFingerly takes the same arguments.
events.get(request_id)EventRaises FingerlyAPIError with .status for a non-2xx response.
events.list(from_=None, to=None, page=1, limit=10, visitor=None, level=None)EventPageEventPage has rows, page and page_size. from_ and to accept datetime.
verify_webhook(secret, payload, timestamp, signature, tolerance_seconds=300)boolSynchronous 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.