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, and a webhook signing secret if you receive webhooks.
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 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: |
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.