[{"data":1,"prerenderedAt":143},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fsdks\u002Fpython":3},{"page":4,"toc":134,"updated":142},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fsdks\u002Fpython","Python","Python SDK: Sync and Asyncio Clients","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.",[10,13,18,23,26,39,42,44,52,54,57,59,65,68,70,76,79,102,107,110,132],{"type":11,"text":12},"p","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.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Requirements","requirements",{"type":19,"items":20},"list",[21,22],"Python 3.9 or newer.","A [secret key](\u002Fdocs\u002Fapi-keys), and a webhook signing secret if you receive [webhooks](\u002Fdocs\u002Fwebhooks).",{"type":14,"level":15,"text":24,"id":25},"Install","install",{"type":27,"samples":28},"code",[29,33,36],{"label":30,"lang":31,"code":32},"pip","bash","pip install fingerly",{"label":34,"lang":31,"code":35},"uv","uv add fingerly",{"label":37,"lang":31,"code":38},"Poetry","poetry add fingerly",{"type":14,"level":15,"text":40,"id":41},"Read an event","read-an-event",{"type":11,"text":43},"Create one client with your secret key and reuse it. The key decides the regional API and the environment the client reads.",{"type":27,"samples":45},[46,49],{"label":6,"lang":47,"code":48},"python","import os\nfrom fingerly import Fingerly\n\nfingerly = Fingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\nevent = fingerly.events.get(\"01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4\")",{"label":50,"lang":47,"code":51},"Python (async)","import os\nfrom fingerly import AsyncFingerly\n\nfingerly = AsyncFingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\nevent = await fingerly.events.get(\"01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4\")",{"type":11,"text":53},"An event has the fields listed in [Get an event](\u002Freference\u002Fget-event#response). `suspect_score` is `null` when the request was not scored.",{"type":14,"level":15,"text":55,"id":56},"Verify a checkout","verify-a-checkout",{"type":11,"text":58},"Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See [server-side verification](\u002Fdocs\u002Fserver-side-verification).",{"type":27,"samples":60},[61,63],{"label":6,"lang":47,"code":62},"from datetime import datetime, timedelta, timezone\nfrom fingerly import Fingerly, FingerlyAPIError\n\nfingerly = Fingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\n\ndef decide(order_id: str, request_id: str) -> str:\n    try:\n        event = fingerly.events.get(request_id)\n    except FingerlyAPIError as error:\n        if error.status == 404:\n            return \"refuse\"\n        raise\n\n    if event.tag != f\"checkout:{order_id}\":\n        return \"refuse\"\n    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):\n        return \"refuse\"\n\n    if event.suspect_level == \"high\":\n        return \"review\"\n    if event.suspect_level == \"medium\":\n        return \"challenge\"\n    return \"allow\"",{"label":50,"lang":47,"code":64},"from datetime import datetime, timedelta, timezone\nfrom fingerly import AsyncFingerly, FingerlyAPIError\n\nfingerly = AsyncFingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"])\n\nasync def decide(order_id: str, request_id: str) -> str:\n    try:\n        event = await fingerly.events.get(request_id)\n    except FingerlyAPIError as error:\n        if error.status == 404:\n            return \"refuse\"\n        raise\n\n    if event.tag != f\"checkout:{order_id}\":\n        return \"refuse\"\n    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):\n        return \"refuse\"\n\n    return {\"high\": \"review\", \"medium\": \"challenge\"}.get(event.suspect_level, \"allow\")",{"type":14,"level":15,"text":66,"id":67},"Verify a webhook","verify-a-webhook",{"type":11,"text":69},"Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.",{"type":27,"samples":71},[72,74],{"label":6,"lang":47,"code":73},"from flask import Flask, abort, request\nfrom fingerly import verify_webhook\n\n@app.post(\"\u002Fwebhooks\u002Ffingerly\")\ndef fingerly_webhook():\n    payload = request.get_data()\n    if not verify_webhook(\n        secret=os.environ[\"FINGERLY_WEBHOOK_SECRET\"],\n        payload=payload,\n        timestamp=request.headers.get(\"x-fingerly-timestamp\"),\n        signature=request.headers.get(\"x-fingerly-signature\"),\n    ):\n        abort(400)\n\n    event = json.loads(payload)\n    queue.enqueue(event[\"id\"], event)   # deduplicate on the event ID\n    return \"\", 204",{"label":50,"lang":47,"code":75},"from fastapi import FastAPI, HTTPException, Request, Response\nfrom fingerly import verify_webhook\n\n@app.post(\"\u002Fwebhooks\u002Ffingerly\", status_code=204)\nasync def fingerly_webhook(request: Request) -> Response:\n    payload = await request.body()\n    if not verify_webhook(\n        secret=os.environ[\"FINGERLY_WEBHOOK_SECRET\"],\n        payload=payload,\n        timestamp=request.headers.get(\"x-fingerly-timestamp\"),\n        signature=request.headers.get(\"x-fingerly-signature\"),\n    ):\n        raise HTTPException(status_code=400)\n\n    event = json.loads(payload)\n    await queue.enqueue(event[\"id\"], event)\n    return Response(status_code=204)",{"type":14,"level":15,"text":77,"id":78},"Sync or async","sync-or-async",{"type":80,"columns":81,"rows":85},"table",[82,83,84],"","`Fingerly`","`AsyncFingerly`",[86,90,94,98],[87,88,89],"Use in","Django, Flask, scripts, Celery tasks","FastAPI, Starlette, aiohttp, Quart",[91,92,93],"Calls","`fingerly.events.get(id)`","`await fingerly.events.get(id)`",[95,96,97],"HTTP client","One pooled connection per client","One pooled connection per client, per event loop",[99,100,101],"Closing","`fingerly.close()`, or `with Fingerly(...) as fingerly:`","`await fingerly.aclose()`, or `async with AsyncFingerly(...) as fingerly:`",{"type":27,"samples":103},[104],{"label":105,"lang":47,"code":106},"lifespan.py","from contextlib import asynccontextmanager\nfrom fastapi import FastAPI\nfrom fingerly import AsyncFingerly\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    async with AsyncFingerly(secret_key=os.environ[\"FINGERLY_SECRET_KEY\"]) as fingerly:\n        app.state.fingerly = fingerly\n        yield\n\napp = FastAPI(lifespan=lifespan)",{"type":14,"level":15,"text":108,"id":109},"API","api",{"type":80,"columns":111,"rows":115},[112,113,114],"Member","Returns","Notes",[116,120,124,128],[117,118,119],"`Fingerly(secret_key, endpoint=None, timeout=10.0)`","client","The key's prefix decides the regional API. `AsyncFingerly` takes the same arguments.",[121,122,123],"`events.get(request_id)`","`Event`","Raises `FingerlyAPIError` with `.status` for a non-2xx response.",[125,126,127],"`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`.",[129,130,131],"`verify_webhook(secret, payload, timestamp, signature, tolerance_seconds=300)`","`bool`","Synchronous in both clients. Never raises for bad input.",{"type":11,"text":133},"`Event` exposes every field of the event as an attribute, with `occurred_at` parsed to a timezone-aware `datetime`.",[135,136,137,138,139,140,141],{"id":17,"text":16,"level":15},{"id":25,"text":24,"level":15},{"id":41,"text":40,"level":15},{"id":56,"text":55,"level":15},{"id":67,"text":66,"level":15},{"id":78,"text":77,"level":15},{"id":109,"text":108,"level":15},"2026-09-17T08:28:36.000Z",1789667797819]