> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proofage.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Create verifications and verify webhooks from Python with the proofage package.

`proofage` has a sync and an async client for every endpoint, typed Pydantic models, typed errors, and webhook verification with extras for FastAPI, Django and Flask.

## Install

```bash theme={null}
pip install proofage
# with a webhook integration:
pip install "proofage[fastapi]"   # or proofage[django], proofage[flask]
```

## Supported Python versions

A Python version stays supported for 12 months after its upstream end of life.

| Python | Supported until |
| - | - |
| 3.10 | 2027-10 |
| 3.11 | 2028-10 |
| 3.12 | 2029-10 |
| 3.13 | 2030-10 |
| 3.14 | 2031-10 |

## Configure

| Argument | Environment variable | Default |
| - | - | - |
| `api_key` | `PROOFAGE_API_KEY` | required |
| `secret_key` | `PROOFAGE_SECRET_KEY` | required |
| `base_url` | `PROOFAGE_BASE_URL` | `https://api.proofage.net` |
| `timeout` | `PROOFAGE_TIMEOUT` (seconds) | `30.0` |
| `retry_attempts` | `PROOFAGE_RETRY_ATTEMPTS` | `3` |
| `retry_delay` (seconds) | `PROOFAGE_RETRY_DELAY` (milliseconds) | `1.0` |

## Create a verification

```python theme={null}
from proofage import ProofAge

with ProofAge() as client:
    verification = client.verifications.create(
        external_id="user_12345",
        callback_url="https://yourapp.example/verified",
    )
    print(verification.url)  # send the person here
```

`AsyncProofAge` has the same methods, awaited, for FastAPI, aiogram and other asyncio code:

```python theme={null}
from proofage import AsyncProofAge

async with AsyncProofAge() as client:
    verification = await client.verifications.create(external_id="user_12345")
```

## Read results

```python theme={null}
with ProofAge() as client:
    verification = client.verifications.get(verification_id)
    document = client.verifications.document(verification_id)
    for item in document.media:
        if item.url is not None:  # None once deleted by retention or erasure
            client.verifications.download_media_to(verification_id, item.id, f"{item.id}.jpg")
```

Statuses are `VerificationStatus` members; a status this version does not know arrives as a plain string, so compare with `==`:

```python theme={null}
from proofage import VerificationStatus

if verification.status == VerificationStatus.APPROVED:
    ...
```

## Receive webhooks

<CodeGroup>
  ```python FastAPI theme={null}
  from fastapi import FastAPI
  from proofage.integrations.fastapi import ProofAgeWebhook

  app = FastAPI()

  @app.post("/webhooks/proofage")
  async def proofage_webhook(event: ProofAgeWebhook):
      print(event.verification_id, event.status, event.reason)
  ```

  ```python Django theme={null}
  from django.http import HttpResponse
  from proofage.integrations.django import proofage_webhook

  @proofage_webhook
  def webhook(request):
      event = request.proofage_event
      return HttpResponse(status=200)
  ```

  ```python Flask theme={null}
  from proofage.integrations.flask import proofage_webhook

  @app.post("/webhooks/proofage")
  @proofage_webhook
  def webhook(event):
      return "", 200
  ```

  ```python Other frameworks theme={null}
  from proofage import WebhookVerificationError, verify_webhook

  try:
      event = verify_webhook(raw_body, headers)  # the raw body, not re-serialised JSON
  except WebhookVerificationError as error:
      ...  # answer error.http_status
  ```
</CodeGroup>

A request that fails verification never reaches your handler: it is answered `401` with the reason. De-duplicate on `event.delivery_id`, which stays the same on every automatic retry.

## Errors and retries

`AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ValidationError` (422), `RateLimitError` (429), `ServerError` (5xx) and `TransportError` (no response) all extend `ProofAgeError`, which has `status_code`, `code` and `message`.

A `GET` is retried on `408`, `429`, `5xx`, timeouts and connection failures. A `POST` is retried only on `429` and when the connection never opened. A `429` waits for `Retry-After` up to 60 seconds; a longer one raises `RateLimitError` with `retry_after` set.

`client.workspace.consent()` returns the active consent text, and `client.verifications.accept_consent`, `upload_media` and `submit` do the rest of [Capture in your own UI](/integration/web/custom-ui).

## Links

* [PyPI: proofage](https://pypi.org/project/proofage/)
* [GitHub: ProofAge/python-sdk](https://github.com/ProofAge/python-sdk), with runnable examples: an aiogram bot, a FastAPI app and Django views


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.