proofage 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proofage-0.1.0/.gitignore +10 -0
- proofage-0.1.0/AGENTS.md +202 -0
- proofage-0.1.0/CHANGELOG.md +17 -0
- proofage-0.1.0/LICENSE +21 -0
- proofage-0.1.0/PKG-INFO +243 -0
- proofage-0.1.0/README.md +215 -0
- proofage-0.1.0/pyproject.toml +76 -0
- proofage-0.1.0/src/proofage/__init__.py +44 -0
- proofage-0.1.0/src/proofage/_async_client.py +186 -0
- proofage-0.1.0/src/proofage/_client.py +181 -0
- proofage-0.1.0/src/proofage/_config.py +188 -0
- proofage-0.1.0/src/proofage/_signing.py +107 -0
- proofage-0.1.0/src/proofage/_transport.py +290 -0
- proofage-0.1.0/src/proofage/_version.py +3 -0
- proofage-0.1.0/src/proofage/errors.py +113 -0
- proofage-0.1.0/src/proofage/models.py +210 -0
- proofage-0.1.0/src/proofage/openapi.json +1795 -0
- proofage-0.1.0/src/proofage/py.typed +0 -0
- proofage-0.1.0/src/proofage/resources/__init__.py +1 -0
- proofage-0.1.0/src/proofage/resources/_payloads.py +86 -0
- proofage-0.1.0/src/proofage/resources/verifications.py +352 -0
- proofage-0.1.0/src/proofage/resources/workspace.py +41 -0
- proofage-0.1.0/src/proofage/webhooks.py +127 -0
- proofage-0.1.0/tests/__init__.py +0 -0
- proofage-0.1.0/tests/conftest.py +70 -0
- proofage-0.1.0/tests/fixtures/hmac-vectors.json +205 -0
- proofage-0.1.0/tests/test_api_contract.py +224 -0
- proofage-0.1.0/tests/test_config.py +179 -0
- proofage-0.1.0/tests/test_downloads.py +112 -0
- proofage-0.1.0/tests/test_errors.py +110 -0
- proofage-0.1.0/tests/test_models.py +152 -0
- proofage-0.1.0/tests/test_package.py +7 -0
- proofage-0.1.0/tests/test_signing.py +88 -0
- proofage-0.1.0/tests/test_transport.py +255 -0
- proofage-0.1.0/tests/test_upload.py +86 -0
- proofage-0.1.0/tests/test_verifications.py +141 -0
- proofage-0.1.0/tests/test_webhooks.py +184 -0
- proofage-0.1.0/tests/test_workspace.py +53 -0
proofage-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# ProofAge Python SDK — API contract for agents
|
|
2
|
+
|
|
3
|
+
This package wraps the ProofAge v1 HTTP API. Methods live on `client.workspace` and
|
|
4
|
+
`client.verifications`; `AsyncProofAge` has the same methods, awaited. Responses are Pydantic
|
|
5
|
+
models from `proofage.models`, and they are lenient: a field the SDK does not know yet is kept in
|
|
6
|
+
`model.model_extra`, and a status the SDK does not know yet arrives as a plain `str` instead of a
|
|
7
|
+
`VerificationStatus` member (compare with `==`, not `is`). `model.model_dump(mode="json")` gives a
|
|
8
|
+
plain dict. A machine-readable spec ships next to this file as `openapi.json` (authoritative for
|
|
9
|
+
endpoints and request bodies; where it does not describe a response, the shapes below are
|
|
10
|
+
authoritative).
|
|
11
|
+
|
|
12
|
+
All requests send `X-API-Key` and `X-HMAC-Signature`. Request bodies use **snake_case** to match
|
|
13
|
+
the API. Responses are never wrapped in `data`.
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
Argument → environment variable → default. The environment variables use the **Laravel/PHP
|
|
18
|
+
SDK's units**, so one `.env` serves both; constructor arguments are seconds as floats.
|
|
19
|
+
|
|
20
|
+
| Argument | Environment | Default | Notes |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `api_key` | `PROOFAGE_API_KEY` | required | Workspace API key |
|
|
23
|
+
| `secret_key` | `PROOFAGE_SECRET_KEY` | required | Used only to sign; never sent, never in `repr` |
|
|
24
|
+
| `base_url` | `PROOFAGE_BASE_URL` | `https://api.proofage.xyz` | API origin without the version; a trailing `/v1` is stripped; a non-http(s) URL, a query or a fragment raises `ConfigurationError` |
|
|
25
|
+
| `version` | `PROOFAGE_VERSION` | `v1` | |
|
|
26
|
+
| `timeout` | `PROOFAGE_TIMEOUT` (seconds) | `30.0` | httpx per-operation timeout (connect, each read, each write), not a deadline for the whole request. Node reads this variable in milliseconds |
|
|
27
|
+
| `retry_attempts` | `PROOFAGE_RETRY_ATTEMPTS` | `3` | Attempts for interactive requests |
|
|
28
|
+
| `retry_delay` | `PROOFAGE_RETRY_DELAY` (**milliseconds**) | `1.0` s | Linear backoff unit; `PROOFAGE_RETRY_DELAY=1000` is one second |
|
|
29
|
+
| `download_retry_attempts` | `PROOFAGE_DOWNLOAD_RETRY_ATTEMPTS` | `1` | Media downloads; only transport failures are retried |
|
|
30
|
+
| `http_client` | — | owned | Your `httpx.Client` / `httpx.AsyncClient`; the SDK never closes it |
|
|
31
|
+
| `sdk_tokens` | — | `()` | `name/version` tokens for a package wrapping this SDK (see "SDK identification") |
|
|
32
|
+
| `user_agent` | — | SDK default | Replaces the User-Agent; `X-ProofAge-Sdk` is still sent |
|
|
33
|
+
|
|
34
|
+
## Errors
|
|
35
|
+
|
|
36
|
+
Every non-2xx response raises. All request errors extend `ProofAgeError`, which carries
|
|
37
|
+
`status_code`, `message`, `code` (the API's machine-readable code, when sent), `error_data` and the
|
|
38
|
+
raw `response_body`:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
ProofAgeError
|
|
42
|
+
├── AuthenticationError 401
|
|
43
|
+
├── PaymentRequiredError 402 code PAYMENT_METHOD_REQUIRED; error_data has free_verifications_remaining, trial_ends_at, trial_active
|
|
44
|
+
├── PermissionDeniedError 403
|
|
45
|
+
├── NotFoundError 404
|
|
46
|
+
├── ValidationError 422 .errors: dict[str, list[str]]; media rejections carry .code (e.g. FACE_NOT_FOUND)
|
|
47
|
+
├── RateLimitError 429 .retry_after: float | None, raised after the retries run out
|
|
48
|
+
├── ServerError 5xx
|
|
49
|
+
└── TransportError no response (DNS, connection, TLS, timeout); __cause__ is the httpx exception
|
|
50
|
+
ConfigurationError raised when a client is built
|
|
51
|
+
WebhookVerificationError see "Outbound webhook"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The API uses four error body shapes; the client reads all of them:
|
|
55
|
+
|
|
56
|
+
- `{ error: { code, message } }` — most errors (401 auth, 429 `RATE_LIMIT`, submit 422, media download 404). `error_data` is the inner object.
|
|
57
|
+
- `{ code, message, ...extra }` — flat: `402 PAYMENT_METHOD_REQUIRED` (extra: `free_verifications_remaining`, `trial_ends_at`, `trial_active`) and media-quality rejections on upload (`422`, e.g. `FACE_NOT_FOUND`; `500 VALIDATION_SERVICE_UNAVAILABLE`). `error_data` is the whole body.
|
|
58
|
+
- `{ message, errors }` — request validation (`422`). `code` is `None`; `errors` has the fields.
|
|
59
|
+
- `{ message }` — `403` (verification not in your workspace) and `404` (`Resource not found`).
|
|
60
|
+
|
|
61
|
+
A 2xx with an empty body returns `None` from the methods typed `-> None`; where a model is
|
|
62
|
+
promised, an empty or non-JSON 2xx raises `ProofAgeError` (a non-JSON one almost always means
|
|
63
|
+
`base_url` points at a website rather than the API).
|
|
64
|
+
|
|
65
|
+
Retries: GETs retry on 408, 429, 5xx, timeouts and transport errors. POSTs retry **only** on 429
|
|
66
|
+
and on errors raised before anything was sent (`httpx.ConnectError`, `httpx.ConnectTimeout`) —
|
|
67
|
+
never on 5xx or after sending began (`WriteError`, `WriteTimeout`, `ReadError`, `ReadTimeout`,
|
|
68
|
+
`RemoteProtocolError`), where the server may already have created the verification or stored the
|
|
69
|
+
upload. A 429 waits for `Retry-After` (seconds or an HTTP date) when present, up to 60 seconds —
|
|
70
|
+
a longer one raises `RateLimitError` at once with `retry_after` set — else `retry_delay * attempt`.
|
|
71
|
+
A 2xx whose body does not match the model raises `ProofAgeError` ("Unexpected response shape from
|
|
72
|
+
GET /v1/..."), naming the fields but never their values.
|
|
73
|
+
|
|
74
|
+
## Auth / HMAC
|
|
75
|
+
|
|
76
|
+
- `X-API-Key`: workspace API key (plaintext; the server SHA256-hashes it).
|
|
77
|
+
- `X-HMAC-Signature`: hex HMAC-SHA256 with the workspace secret key over a canonical string:
|
|
78
|
+
- JSON / no-file requests: `METHOD + /{version}/{path} + ?query + rawJsonBody` (direct
|
|
79
|
+
concatenation, no delimiter; the query part only when there is one, keys sorted and values
|
|
80
|
+
RFC 3986-encoded). The body is serialised once, compact, and sent as exactly the signed bytes;
|
|
81
|
+
an empty payload is the empty string, never `{}`.
|
|
82
|
+
- Multipart (file) requests: `METHOD/{version}/{path}\n{fields}\n{comma-joined sorted sha256(file) hashes}`,
|
|
83
|
+
where `{fields}` is PHP `http_build_query(ksort($fields), '', '&', PHP_QUERY_RFC3986)` — keys
|
|
84
|
+
sorted, values `rawurlencode`d (so `! ' ( ) *` are percent-encoded too). The client signs
|
|
85
|
+
exactly what it sends: `None` fields are dropped, numbers are `str(n)`, booleans `"1"`/`"0"`,
|
|
86
|
+
dicts and lists compact JSON strings. Golden vectors: `tests/fixtures/hmac-vectors.json` in the
|
|
87
|
+
source repository.
|
|
88
|
+
|
|
89
|
+
## Endpoints
|
|
90
|
+
|
|
91
|
+
### GET /workspace — `client.workspace.get()` → `WorkspaceInfo`
|
|
92
|
+
Request: none.
|
|
93
|
+
Response: `{ id: str, name: str, flow_type: str, mode: str, age_mode: str|None, age_threshold: int|None, verification_type: str, redirect_url: str|None, webhook_url: str|None, allow_expired_documents: bool, allow_duplicate_accounts: bool }`
|
|
94
|
+
|
|
95
|
+
### GET /consent — `client.workspace.consent()` → `ConsentInfo`
|
|
96
|
+
Request: none.
|
|
97
|
+
Response: `{ id: int, version: str, text_sha256: str, url: str }`
|
|
98
|
+
|
|
99
|
+
### POST /verifications — `client.verifications.create(**kwargs)` → `CreatedVerification`
|
|
100
|
+
Request (all optional keywords): `fingerprint: str(64), callback_url: url(<=2048), external_id: str(<=255), external_metadata: dict, metadata: dict, page_url: str(<=8192)` (`page_url`: the page the verification was started on; only scheme, host and path are kept).
|
|
101
|
+
Response (`201`): `{ id, external_id, external_metadata, redirect_url, status, reason, duplicate_check: DuplicateCheck, erasure: Erasure|None, consent_accepted_at, created_at, updated_at, url }` — `url` is the hosted session the person opens.
|
|
102
|
+
Errors: `402` `PaymentRequiredError`; `422` `ValidationError`.
|
|
103
|
+
|
|
104
|
+
- `DuplicateCheck`: `{ checked: bool, duplicate_count: int, duplicates: [ { verification_id: str, external_id: str|None, similarity_score: float, verified_at: datetime|None } ] }` — always present.
|
|
105
|
+
- `Erasure`: `{ erased_at: datetime, scope: "personal_data", reason: str|None, requested_via: "customer"|"proofage"|"retention"|None }` — `None` until the verification's personal data is erased. `reason` is an erasure reason code (`data_subject_request`, `customer_request`, `retention_policy`, `test_data`, `other`) or `None` if unrecorded.
|
|
106
|
+
|
|
107
|
+
### GET /verifications/{verification} — `client.verifications.get(verification_id)` → `Verification`
|
|
108
|
+
Request: none.
|
|
109
|
+
Response: same as create **without** `url`.
|
|
110
|
+
|
|
111
|
+
### POST /verifications/{verification}/consent — `client.verifications.accept_consent(verification_id, *, consent_version_id, text_sha256, ...)` → `AcceptConsentResult`
|
|
112
|
+
Request: `consent_version_id: int, text_sha256: str(64 hex)`, optional `device: { platform, screen, language, timezone, hardware_concurrency, device_memory }, in_app_browser: str|None, camera_permission: "granted"|"denied"|"prompt"|"unsupported"|None, camera_policy_allowed: bool|None, in_iframe: bool|None, referrer: str|None`. `consent_version_id` / `text_sha256` are `id` / `text_sha256` from `workspace.consent()`. Custom capture flows only.
|
|
113
|
+
Response: `{ consent_version_id: int, consent_accepted_at: datetime }`
|
|
114
|
+
|
|
115
|
+
### POST /verifications/{verification}/media — `client.verifications.upload_media(verification_id, *, file, type, ...)` → `None`
|
|
116
|
+
Request (multipart): `file: bytes | pathlib.Path | binary file object (image, <=10 MB; documents >=200px per edge)`, `type: "selfie"|"liveness_selfie"|"document"`, `side: "front"|"back"` and `document: "id"|"driver_license"|"passport"|"residence_permit"` (both required when `type="document"`), optional `filename` (default: the path's name, else `upload.bin`), `fingerprint: str(64), head_turn_step: int(0..10), capture_resolution: str|dict, device_info: str|dict, liveness_telemetry: str|list`. Dicts and lists are sent as JSON strings. A text-mode file raises `TypeError`; an invalid `type`/`side`/`document` combination raises `ValueError`, both before any request. Requires consent accepted first.
|
|
117
|
+
Response: `200` with an **empty body**; returns `None`.
|
|
118
|
+
Errors: `422` `ValidationError` with `.code` when the image is rejected (e.g. `FACE_NOT_FOUND`) or `.errors` for invalid fields; `500` `ServerError` `VALIDATION_SERVICE_UNAVAILABLE`.
|
|
119
|
+
|
|
120
|
+
### POST /verifications/{verification}/submit — `client.verifications.submit(verification_id)` → `None`
|
|
121
|
+
Request: none.
|
|
122
|
+
Response: `200` with an **empty body**. Error: `422` `{ error: { code, message } }` (e.g. `MISSING_REQUIRED_MEDIA`).
|
|
123
|
+
|
|
124
|
+
### GET /verifications/{verification}/document — `client.verifications.document(verification_id)` → `VerificationDocument`
|
|
125
|
+
Request: none.
|
|
126
|
+
Response: `{ document: { fields: { first_name: str|None, last_name: str|None, date_of_birth: date|None, document_number: str|None } }, media: [ { id: str, type: "selfie"|"document_front"|"document_back", url: str|None } ], meta: { attempt_id: str|None } }`. `url` is None once the media has been purged or is past retention.
|
|
127
|
+
|
|
128
|
+
### GET /verifications/{verification}/media/{media} — `download_media()` / `download_media_to()`
|
|
129
|
+
`with client.verifications.download_media(verification_id, media_id) as chunks:` yields the image bytes as an iterator (`async with` and an async iterator on `AsyncProofAge`); `client.verifications.download_media_to(verification_id, media_id, path)` streams to disk and returns the `Path`, writing to a temporary sibling and renaming only after a 2xx, so a failure never leaves a partial file. `media_id` is `media[].id` from `document()`; check its `url` is not None first. Requests send `Accept: application/json, */*;q=0.8` so errors come back as JSON. Error: `404` `NotFoundError` with `code == "MEDIA_NOT_FOUND"`. An HTTP status is never retried, 429 included; a transport failure is retried `download_retry_attempts` times, only before the first byte.
|
|
130
|
+
|
|
131
|
+
### GET /verifications/{verification}/estimation — `client.verifications.estimation(verification_id)` → `AgeEstimation`
|
|
132
|
+
Request: none.
|
|
133
|
+
Response: `{ verification_id: str, attempt_id: str|None, age_threshold: { minimum: int|None, passed: bool|None, confidence: float|None }, gender: { value: 0|1|None, confidence: float|None }|None }` (gender value: 0=female, 1=male).
|
|
134
|
+
|
|
135
|
+
### POST /verifications/{verification}/blocked-face — `client.verifications.block_face(verification_id, *, reason_code=None, reason=None)` → `None`
|
|
136
|
+
Request: `reason_code: BlockFaceReasonCode | str`, `reason: str(<=1000)`.
|
|
137
|
+
Response: `204 No Content`.
|
|
138
|
+
|
|
139
|
+
Every `verification_id` and `media_id` must be a ProofAge id (letters, digits, `-`, `_`); anything else raises `ValueError` before a request is built.
|
|
140
|
+
|
|
141
|
+
## Enums
|
|
142
|
+
|
|
143
|
+
- `status` (`VerificationStatus`): `created`, `started`, `submitted`, `resubmission_requested`, `approved`, `declined`, `abandoned`, `expired`, `review`, or `documents_required` (the last is surfaced from the latest attempt's state, not a verification status). Open: an unknown value arrives as `str`.
|
|
144
|
+
- `reason_code` (`BlockFaceReasonCode`): `presentation_attack` (spoof: screen, print or mask), `fraudulent_document` (forged, edited, or not a real document), `scam_or_abuse` (identity may be genuine — blocked for behaviour on your platform), `underage`, `other` (explain in `reason`). Optional over the API, mandatory in the ProofAge consoles: send it whenever a person made the decision, or the block cannot be told apart from an automated one in reporting.
|
|
145
|
+
- `reason` (on `declined` / `resubmission_requested`): dotted codes from the server's reason catalog — illustrative examples: `aml.blocklist.face_match`, `document.face.mismatch`, `verification.age_threshold.failed`. Treat `reason` as an open string.
|
|
146
|
+
|
|
147
|
+
## Outbound webhook (ProofAge → your `callback_url` / workspace webhook URL)
|
|
148
|
+
|
|
149
|
+
Headers: `X-Auth-Client` (api key), `X-Timestamp` (unix seconds), `X-HMAC-Signature`
|
|
150
|
+
(= hex HMAC-SHA256 of `{timestamp}.{rawJsonBody}` with the active secret key),
|
|
151
|
+
`X-ProofAge-Webhook-Delivery-Id` (the same on every automatic retry of one delivery, a new one on a
|
|
152
|
+
manual resend — de-duplicate on it).
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from proofage import WebhookVerificationError, verify_webhook
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# api_key=, secret_key= and tolerance= fall back to the environment
|
|
159
|
+
event = verify_webhook(raw_body, headers)
|
|
160
|
+
except WebhookVerificationError as error:
|
|
161
|
+
... # answer error.http_status; error.code says which check failed
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- `verify_webhook(raw_body, headers, *, api_key=None, secret_key=None, tolerance=None)` → `WebhookEvent`; `verify_webhook_signature(...)` → `None` (checks only). Keys and tolerance fall back to `PROOFAGE_API_KEY`, `PROOFAGE_SECRET_KEY`, `PROOFAGE_WEBHOOK_TOLERANCE` (default 300 seconds, in both directions).
|
|
165
|
+
- Pass the body **exactly as received** (`bytes` or `str`). A body whose only change is whitespace is still accepted (the canonical compact JSON is tried once), but one that lost PHP's `\/` escapes is not.
|
|
166
|
+
- Error codes, in check order: `MISSING_SIGNATURE`, `MISSING_TIMESTAMP`, `MISSING_AUTH_CLIENT` (401); `CONFIGURATION_ERROR` (500, keys missing — checked after the three headers); `INVALID_AUTH_CLIENT`, `MISSING_TIMESTAMP` for a non-integer timestamp, `TIMESTAMP_TOO_OLD`, `INVALID_SIGNATURE` (401); `INVALID_PAYLOAD` (400, correctly signed but not a webhook event; the message names the fields, never the body's values).
|
|
167
|
+
- `WebhookEvent`:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
{
|
|
171
|
+
"verification_id": str,
|
|
172
|
+
"status": VerificationStatus | str,
|
|
173
|
+
"external_id": str|None,
|
|
174
|
+
"external_metadata": dict|None,
|
|
175
|
+
"reason": str|None, # a code only on resubmission_requested / declined
|
|
176
|
+
"timestamp": datetime,
|
|
177
|
+
"duplicate_detected": bool (default False), # the three duplicate_* keys appear together
|
|
178
|
+
"duplicate_count": int|None,
|
|
179
|
+
"duplicate_of": { "verification_id": str, "external_id": str|None }|None,
|
|
180
|
+
"fingerprint_signals": dict|None,
|
|
181
|
+
"manual_moderation": { # after a console approve/decline
|
|
182
|
+
"action": "approve"|"decline", "reason": str, "source": "tenant_admin"|"landlord_admin",
|
|
183
|
+
"performed_by": { "id": int, "name": str|None, "email": str|None, "role": str|None },
|
|
184
|
+
"source_status": str|None, "source_reason": str|None
|
|
185
|
+
}|None,
|
|
186
|
+
"delivery_id": str|None # from X-ProofAge-Webhook-Delivery-Id
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## SDK identification
|
|
191
|
+
|
|
192
|
+
Every request carries `X-ProofAge-Sdk: [wrapper tokens ]python/{version}` (wrappers pass
|
|
193
|
+
`sdk_tokens=["telegram-bot/1.2.0"]`, outermost first; a wrapper token named `python` is dropped)
|
|
194
|
+
and `User-Agent: ProofAge-Python/{version} (Python {x.y.z})` unless you pass `user_agent` or your
|
|
195
|
+
`http_client` carries its own. Neither header is part of the signature.
|
|
196
|
+
|
|
197
|
+
## Keeping this in sync
|
|
198
|
+
|
|
199
|
+
This contract is drift-tested against `openapi.json` by `tests/test_api_contract.py`, so it stays
|
|
200
|
+
aligned with the API. Maintainers refreshing it after an API change: see the SDK contract-sync
|
|
201
|
+
runbook in the ProofAge app repo (`developer-docs/README.md`, "Keeping the SDK clients in sync"),
|
|
202
|
+
the single source of truth for all SDKs.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/); while the version is 0.x, a minor
|
|
5
|
+
release may change the API and a patch release never does.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 — 2026-09-28
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
- `ProofAge` and `AsyncProofAge` clients for every `/v1` endpoint.
|
|
12
|
+
- Pydantic v2 response models; unknown fields and statuses never break parsing.
|
|
13
|
+
- Typed errors for the API's four error body shapes.
|
|
14
|
+
- Retries that never repeat a POST the server may already have acted on.
|
|
15
|
+
- Webhook verification (`verify_webhook`, `verify_webhook_signature`).
|
|
16
|
+
- `X-ProofAge-Sdk` and User-Agent identification on every request.
|
|
17
|
+
- Supports Python 3.10–3.14.
|
proofage-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ProofAge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
proofage-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: proofage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the ProofAge age and identity verification API
|
|
5
|
+
Project-URL: Homepage, https://proofage.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/ProofAge/python-sdk
|
|
7
|
+
Project-URL: Changelog, https://github.com/ProofAge/python-sdk/blob/main/CHANGELOG.md
|
|
8
|
+
Author-email: ProofAge <support@proofage.xyz>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: age verification,identity verification,kyc,proofage,webhooks
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# proofage — ProofAge for Python
|
|
30
|
+
|
|
31
|
+
Python client for [ProofAge](https://proofage.xyz), the age and identity verification API. It
|
|
32
|
+
gives you a sync and an async client for every `/v1` endpoint, typed Pydantic models, typed errors
|
|
33
|
+
and webhook verification — enough to go from `pip install` to a verified result in a few minutes,
|
|
34
|
+
from a backend, a Telegram bot or an AI agent.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install proofage
|
|
40
|
+
# or
|
|
41
|
+
uv add proofage
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
Set `PROOFAGE_API_KEY` and `PROOFAGE_SECRET_KEY` (from your workspace in the ProofAge console),
|
|
47
|
+
then:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from proofage import ProofAge
|
|
51
|
+
|
|
52
|
+
with ProofAge() as client:
|
|
53
|
+
verification = client.verifications.create(
|
|
54
|
+
external_id="candidate-42",
|
|
55
|
+
callback_url="https://your-app.example/verified",
|
|
56
|
+
)
|
|
57
|
+
print(verification.url) # send the person here
|
|
58
|
+
|
|
59
|
+
# later, or when the webhook arrives
|
|
60
|
+
print(client.verifications.get(verification.id).status)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The person opens `verification.url` on their phone, ProofAge runs the checks, and you learn the
|
|
64
|
+
outcome from a webhook (below) or by calling `get()`.
|
|
65
|
+
|
|
66
|
+
## Async
|
|
67
|
+
|
|
68
|
+
`AsyncProofAge` has the same methods, awaited — the natural choice in aiogram bots, FastAPI and
|
|
69
|
+
other asyncio code:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from proofage import AsyncProofAge
|
|
73
|
+
|
|
74
|
+
async with AsyncProofAge() as client:
|
|
75
|
+
verification = await client.verifications.create(external_id="tg-12345")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Receiving results (webhooks)
|
|
79
|
+
|
|
80
|
+
ProofAge POSTs the outcome to your workspace's webhook URL. Verify it with the **raw** request
|
|
81
|
+
body — not a re-serialised copy of its JSON — and answer 2xx.
|
|
82
|
+
|
|
83
|
+
FastAPI:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from fastapi import FastAPI, Request, Response
|
|
87
|
+
from proofage import WebhookVerificationError, verify_webhook
|
|
88
|
+
|
|
89
|
+
app = FastAPI()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.post("/webhooks/proofage")
|
|
93
|
+
async def proofage_webhook(request: Request) -> Response:
|
|
94
|
+
try:
|
|
95
|
+
event = verify_webhook(await request.body(), request.headers)
|
|
96
|
+
except WebhookVerificationError as error:
|
|
97
|
+
return Response(status_code=error.http_status)
|
|
98
|
+
print(event.verification_id, event.status, event.reason)
|
|
99
|
+
return Response(status_code=200)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Django:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from django.http import HttpResponse
|
|
106
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
107
|
+
from django.views.decorators.http import require_POST
|
|
108
|
+
from proofage import WebhookVerificationError, verify_webhook
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@csrf_exempt
|
|
112
|
+
@require_POST
|
|
113
|
+
def proofage_webhook(request):
|
|
114
|
+
try:
|
|
115
|
+
event = verify_webhook(request.body, request.headers)
|
|
116
|
+
except WebhookVerificationError as error:
|
|
117
|
+
return HttpResponse(status=error.http_status)
|
|
118
|
+
...
|
|
119
|
+
return HttpResponse(status=200)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A delivery can arrive more than once: de-duplicate on `event.delivery_id`, which stays the same
|
|
123
|
+
on every automatic retry. Ready-made FastAPI, Django and Flask integrations are coming in 0.1.x.
|
|
124
|
+
|
|
125
|
+
## Statuses
|
|
126
|
+
|
|
127
|
+
`event.status` and `verification.status` are `VerificationStatus` members (`APPROVED`,
|
|
128
|
+
`DECLINED`, `RESUBMISSION_REQUESTED`, `REVIEW`, …). A status this version does not know yet
|
|
129
|
+
arrives as a plain string instead of raising, so compare with `==`:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from proofage import VerificationStatus
|
|
133
|
+
|
|
134
|
+
if event.status == VerificationStatus.APPROVED:
|
|
135
|
+
...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Unknown fields are kept too, in `model.model_extra`; `model.model_dump(mode="json")` gives a plain
|
|
139
|
+
dict.
|
|
140
|
+
|
|
141
|
+
## Errors
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from proofage import PaymentRequiredError, ProofAgeError, RateLimitError, ValidationError
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
client.verifications.create(external_id="x" * 300)
|
|
148
|
+
except ValidationError as error:
|
|
149
|
+
print(error.errors) # {"external_id": ["..."]}
|
|
150
|
+
except PaymentRequiredError:
|
|
151
|
+
print("Add a payment method to the workspace")
|
|
152
|
+
except RateLimitError as error:
|
|
153
|
+
print("Try again in", error.retry_after, "seconds")
|
|
154
|
+
except ProofAgeError as error:
|
|
155
|
+
print(error.status_code, error.code, error.message)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403),
|
|
159
|
+
`NotFoundError` (404), `ValidationError` (422), `RateLimitError` (429), `ServerError` (5xx) and
|
|
160
|
+
`TransportError` (no response) all extend `ProofAgeError`. `ConfigurationError` is raised when a
|
|
161
|
+
client is built with missing or invalid settings. A response whose shape this SDK version does not
|
|
162
|
+
recognise raises `ProofAgeError` naming the fields (never their values).
|
|
163
|
+
|
|
164
|
+
## Configuration
|
|
165
|
+
|
|
166
|
+
| Argument | Environment | Default |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `api_key` | `PROOFAGE_API_KEY` | required |
|
|
169
|
+
| `secret_key` | `PROOFAGE_SECRET_KEY` | required |
|
|
170
|
+
| `base_url` | `PROOFAGE_BASE_URL` | `https://api.proofage.xyz` |
|
|
171
|
+
| `version` | `PROOFAGE_VERSION` | `v1` |
|
|
172
|
+
| `timeout` | `PROOFAGE_TIMEOUT` (seconds) | `30.0` |
|
|
173
|
+
| `retry_attempts` | `PROOFAGE_RETRY_ATTEMPTS` | `3` |
|
|
174
|
+
| `retry_delay` | `PROOFAGE_RETRY_DELAY` (**milliseconds**) | `1.0` seconds |
|
|
175
|
+
| `download_retry_attempts` | `PROOFAGE_DOWNLOAD_RETRY_ATTEMPTS` | `1` |
|
|
176
|
+
| `http_client` | — | an owned `httpx` client |
|
|
177
|
+
|
|
178
|
+
The environment variables use the same names and units as the ProofAge Laravel and PHP SDKs, so
|
|
179
|
+
one `.env` serves all of them (the Node SDK reads `PROOFAGE_TIMEOUT` in milliseconds). Constructor
|
|
180
|
+
arguments are seconds.
|
|
181
|
+
|
|
182
|
+
`timeout` limits each network operation (connecting, each read, each write), not the whole
|
|
183
|
+
request. Pass your own `httpx.Client` / `httpx.AsyncClient` as `http_client` for proxies or custom
|
|
184
|
+
TLS; the SDK never closes a client you pass in.
|
|
185
|
+
|
|
186
|
+
## Retries
|
|
187
|
+
|
|
188
|
+
- A GET is retried on 408, 429, 5xx, timeouts and connection failures.
|
|
189
|
+
- A POST is retried only on 429 and when the connection never opened. It is never retried on a
|
|
190
|
+
5xx or once sending began, because the server may already have created the verification.
|
|
191
|
+
- A 429 waits for its `Retry-After`, up to 60 seconds; a longer `Retry-After` raises `RateLimitError`
|
|
192
|
+
at once (with `retry_after` set) instead of blocking your thread. Otherwise the wait grows by
|
|
193
|
+
`retry_delay` per attempt.
|
|
194
|
+
- Media downloads never retry an HTTP status; run them from a queue and let its backoff wait.
|
|
195
|
+
|
|
196
|
+
## Media
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
document = client.verifications.document(verification_id)
|
|
200
|
+
for item in document.media:
|
|
201
|
+
if item.url is not None: # None once purged or past retention
|
|
202
|
+
client.verifications.download_media_to(verification_id, item.id, f"{item.id}.jpg")
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Hosted flow first
|
|
206
|
+
|
|
207
|
+
`accept_consent`, `upload_media` and `submit` exist for custom capture flows. Most integrations
|
|
208
|
+
only create a session, send the person to its `url`, and read the result.
|
|
209
|
+
|
|
210
|
+
## For packages that wrap this SDK
|
|
211
|
+
|
|
212
|
+
A plugin or bot template built on this SDK can name itself in the `X-ProofAge-Sdk` header, which
|
|
213
|
+
helps ProofAge support tell integrations apart:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
ProofAge(sdk_tokens=["telegram-bot/1.2.0"]) # X-ProofAge-Sdk: telegram-bot/1.2.0 python/0.1.0
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`user_agent=` replaces the default `ProofAge-Python/<version> (Python <x.y.z>)` User-Agent.
|
|
220
|
+
|
|
221
|
+
## Supported versions
|
|
222
|
+
|
|
223
|
+
A Python version stays supported for 12 months after its upstream end of life, or until httpx or
|
|
224
|
+
Pydantic stop supporting it, whichever comes first. A version leaves only in a minor release,
|
|
225
|
+
announced one release ahead in the changelog.
|
|
226
|
+
|
|
227
|
+
| Python | Upstream end of life | Supported by `proofage` until |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| 3.10 | 2026-10 | 2027-10 |
|
|
230
|
+
| 3.11 | 2027-10 | 2028-10 |
|
|
231
|
+
| 3.12 | 2028-10 | 2029-10 |
|
|
232
|
+
| 3.13 | 2029-10 | 2030-10 |
|
|
233
|
+
| 3.14 | 2030-10 | 2031-10 |
|
|
234
|
+
|
|
235
|
+
## AI agents
|
|
236
|
+
|
|
237
|
+
The package ships `AGENTS.md` next to its code: the full request and response contract of every
|
|
238
|
+
method, written for coding agents. Agents that speak MCP can also use the ProofAge MCP server
|
|
239
|
+
directly.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|