Python SDK
The Python SDK (xident) provides both synchronous and asynchronous clients for server-side verification. It uses httpx under the hood with automatic retries and exponential backoff. Works with Flask, Django, FastAPI, and any Python 3.10+ project.
Server-side only. This SDK uses your secret API key (sk_live_* or sk_test_*). Never expose it in client-side code. For browser integration, use the JavaScript SDK with your public key.
Installation
pip install xident Quick Start
Synchronous
from xident import Xident
client = Xident(api_key="sk_live_your_secret_key")
# 1. Create an init token (redirect user to result.verify_url)
init = client.verification.init(
callback_url="https://yoursite.com/verified",
min_age=18,
)
print(init.token) # 'xit_...'
print(init.verify_url) # 'https://verify.xident.io/...'
# 2. After callback, verify the token server-side
result = client.verification.get_result(init.token)
if result.is_verified():
print("Age bracket:", result.age_bracket()) # 18
print("Method:", result.method()) # 'ml_fast' Asynchronous
from xident import AsyncXident
async_client = AsyncXident(api_key="sk_live_your_secret_key")
# Same API, just await the calls
init = await async_client.verification.init(
callback_url="https://yoursite.com/verified",
min_age=18,
)
result = await async_client.verification.get_result(init.token)
if result.is_verified():
print("Verified!", result.age_bracket())
# Clean up when done
await async_client.aclose() Configuration
from xident import Xident
# All configuration options
client = Xident(
api_key="sk_live_your_secret_key", # Required
base_url="https://api.xident.io", # Default (SDK appends /verify/v1)
timeout=30, # Request timeout in seconds (default: 30)
max_retries=3, # Retries on 5xx errors (default: 3)
headers={"X-Custom": "value"}, # Extra headers on every request
)
# Access config
print(client.config.api_url) # 'https://api.xident.io/verify/v1'
print(Xident.version()) # '1.0.0' | Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | — | Required. Secret API key (sk_live_* or sk_test_*). |
base_url | str | http://localhost:9000 | API base URL. The SDK appends /verify/v1 automatically. |
timeout | int | 30 | Request timeout in seconds. |
max_retries | int | 3 | Max retries on 5xx server errors. Exponential backoff with jitter. |
headers | dict[str, str] | None | Extra headers sent with every request. |
Context Manager
Both Xident and AsyncXident support context managers for automatic cleanup:
# Context manager auto-closes the HTTP client
# Sync
with Xident(api_key="sk_live_xxx") as client:
result = client.verification.init(callback_url="https://example.com/cb")
# client.close() called automatically
# Async
async with AsyncXident(api_key="sk_live_xxx") as client:
result = await client.verification.init(callback_url="https://example.com/cb")
# await client.aclose() called automatically Verification
client.verification.init(**kwargs)
Create an init token for starting a verification session. All parameters are keyword-only. Returns an InitResult with token and verify_url. The token is valid for 10 minutes.
result = client.verification.init(
# Required (keyword-only arguments)
callback_url="https://yoursite.com/verified",
# Optional
min_age=18, # Age threshold (12, 15, 18, 21, 25)
success_url="https://yoursite.com/success", # Override redirect on pass
failed_url="https://yoursite.com/failed", # Override redirect on fail
user_id="user-123", # Your internal user ID
theme="dark", # Widget theme: 'light', 'dark', 'auto'
locale="de", # Widget locale: 'en', 'de', 'fr', etc.
metadata="order-456", # Opaque string stored with session
liveness_difficulty="medium", # Challenge: 'easy', 'medium', 'hard'
purpose="age-gate", # Verification purpose for display
)
# InitResult (frozen dataclass)
print(result.token) # 'xit_...' (short-lived, 10-minute TTL)
print(result.verify_url) # Full URL to redirect the user to | Parameter | Type | Required | Description |
|---|---|---|---|
callback_url | str | Yes | URL where user is redirected after verification. |
min_age | int | No | Minimum age threshold (12, 15, 18, 21, 25). |
success_url | str | No | Override redirect URL on success. |
failed_url | str | No | Override redirect URL on failure. |
user_id | str | No | Your internal user ID for correlation. |
theme | str | No | Widget theme: 'light', 'dark', 'auto'. |
locale | str | No | Widget locale: 'en', 'de', 'fr', etc. |
metadata | str | No | Opaque metadata string stored with the session. |
liveness_difficulty | str | No | Challenge difficulty: 'easy', 'medium', 'hard'. |
purpose | str | No | Verification purpose displayed to the user. |
client.verification.get_result(token)
Get the verification result for a token. Call this after the user returns from the verification widget. Never trust URL parameters alone.
# After the user returns from verification, verify the token server-side.
# NEVER trust URL parameters alone.
result = client.verification.get_result("xit_abc123")
# Status helpers
result.is_verified() # True if status == 'completed'
result.is_failed() # True if status == 'failed'
result.is_pending() # True if status in ('pending', 'in_progress')
result.is_terminal() # True if completed, failed, canceled, or claimed
# Verification details
result.age_bracket() # 12, 15, 18, 21, 25, or None
result.method() # 'ml_fast', 'ocr', 'self_declaration', or None
# Session data (frozen dataclass attributes)
result.id # Session UUID
result.status # SessionStatus enum
result.min_age # Requested age threshold
result.country_code # ISO 3166-1 alpha-2 (e.g. 'DE')
result.external_user_id # Your user_id from init()
result.required_methods # ['liveness', 'age'] etc.
result.remaining_attempts # Number of retries left
result.created_at # ISO 8601 timestamp
result.started_at # ISO 8601 or None
result.completed_at # ISO 8601 or None
result.expires_at # ISO 8601 or None
# Sub-results (raw dicts)
result.liveness_result # Liveness check details or None
result.age_result # Age verification details or None
result.ocr_result # Document OCR details or None
result.face_match_result # Face matching details or None Webhook Verification
Xident sends webhook events to your callback URL when verification sessions complete, fail, or expire. Events are signed with HMAC-SHA256 using a Stripe-style signature header. The webhooks resource is stateless -- it does not use the HTTP client.
# Xident sends webhook events via HTTP POST with an HMAC-SHA256 signature.
# Header: X-Xident-Signature: t=1710345600,v1=5257a869abcdef...
# construct_event() verifies the signature AND parses the event in one call.
event = client.webhooks.construct_event(
payload=raw_body, # Raw JSON body (str or bytes)
signature=signature, # Value of X-Xident-Signature header
secret=webhook_secret, # Webhook secret from dashboard (whsec_xxx)
tolerance=300, # Max age in seconds (default: 300 = 5 minutes)
)
# Returns a dict with keys: type, data, id, created
print(event["type"]) # 'verification.completed', 'verification.failed', etc.
print(event["data"]) # Event payload dict
print(event["id"]) # Event ID or None
print(event["created"]) # Unix timestamp or None
# Or verify the signature separately:
client.webhooks.verify_signature(payload, signature, secret, tolerance=300)
# Returns True or raises ValueError
# Parse without verifying:
from xident.resources.webhooks import Webhooks
event = Webhooks.parse_event(payload) # Static method | Method | Returns | Description |
|---|---|---|
construct_event(payload, signature, secret, *, tolerance=300) | dict | Verify signature + parse event. Raises ValueError on failure. |
verify_signature(payload, signature, secret, *, tolerance=300) | bool | Verify HMAC-SHA256 signature only. Returns True or raises ValueError. |
parse_event(payload) | dict | Parse a webhook payload without verifying the signature. Static method. |
Important: Use the raw request body for signature verification. In Flask use request.get_data(as_text=True), in Django use request.body.decode("utf-8"), in FastAPI use await request.body() (accepts both str and bytes).
Error Handling
The SDK has a clear exception hierarchy. APIError carries status_code, error_code, and request_id for debugging.
from xident import (
Xident,
XidentError, # Base exception
APIError, # Base for HTTP errors (has status_code, error_code, request_id)
AuthenticationError, # 401/403 -- invalid or missing API key
ValidationError, # 400 -- bad request params
NotFoundError, # 404 -- token/resource not found
RateLimitError, # 429 -- rate limited (has retry_after)
ServerError, # 5xx -- server error (auto-retried)
NetworkError, # DNS, timeout, connection refused
)
try:
result = client.verification.get_result(token)
except AuthenticationError as e:
print(f"Invalid API key: {e.error_code}")
print(f"Request ID: {e.request_id}") # Include in support tickets
print(f"HTTP status: {e.status_code}") # 401 or 403
except NotFoundError:
print("Token not found or expired")
except RateLimitError as e:
print(f"Retry after: {e.retry_after} seconds")
except ValidationError as e:
print(f"Bad request: {e.message}")
except ServerError:
# Auto-retried up to max_retries times
print("Server error after retries")
except NetworkError:
print("Network failure (DNS, timeout, etc.)")
except XidentError as e:
# Catch-all for any SDK error
print(f"SDK error: {e.message}") | Exception | HTTP Status | When Raised | Key Attributes |
|---|---|---|---|
XidentError | — | Base exception for all SDK errors | message |
APIError | any | Base for HTTP errors | status_code, error_code, request_id |
AuthenticationError | 401/403 | Invalid, expired, or missing API key | inherits APIError |
ValidationError | 400 | Invalid request parameters | inherits APIError |
NotFoundError | 404 | Token or resource not found | inherits APIError |
RateLimitError | 429 | Rate limit exceeded | retry_after (int or None) |
ServerError | 5xx | Server error (auto-retried with backoff) | inherits APIError |
NetworkError | — | DNS, timeout, SSL, connection refused | message |
Note: Webhook verification raises ValueError (not XidentError) to match the Stripe SDK convention. Catch ValueError in webhook handlers.
Framework Examples
Flask
import os
from flask import Flask, jsonify, redirect, request
from xident import Xident, XidentError
app = Flask(__name__)
client = Xident(api_key=os.environ["XIDENT_SECRET_KEY"])
@app.route("/verify")
def start_verification():
"""Redirect user to Xident verification widget."""
try:
result = client.verification.init(
callback_url=request.url_root.rstrip("/") + "/verify/callback",
min_age=18,
theme="auto",
)
return redirect(result.verify_url)
except XidentError as e:
return jsonify({"error": str(e)}), 500
@app.route("/verify/callback")
def verification_callback():
"""Verify the token server-side after user returns."""
token = request.args.get("token")
if not token:
return jsonify({"error": "Missing token"}), 400
try:
session = client.verification.get_result(token)
if session.is_verified():
return jsonify({
"status": "verified",
"age_bracket": session.age_bracket(),
})
elif session.is_failed():
return jsonify({"status": "failed"}), 403
else:
return jsonify({"status": "in_progress"}), 202
except XidentError as e:
return jsonify({"error": str(e)}), 500
@app.route("/webhook", methods=["POST"])
def webhook():
"""Handle Xident webhook events."""
payload = request.get_data(as_text=True)
signature = request.headers.get("X-Xident-Signature", "")
try:
event = client.webhooks.construct_event(
payload, signature, os.environ["XIDENT_WEBHOOK_SECRET"]
)
print(f"Webhook: {event['type']}")
return jsonify({"status": "ok"})
except ValueError as e:
return jsonify({"error": str(e)}), 400 Django
# views.py
import os
from django.conf import settings
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.views.decorators.http import require_GET, require_POST
from xident import Xident, XidentError
client = Xident(
api_key=getattr(settings, "XIDENT_SECRET_KEY", os.environ["XIDENT_SECRET_KEY"])
)
@require_GET
def start_verification(request: HttpRequest) -> HttpResponse:
"""Redirect user to Xident verification widget."""
try:
result = client.verification.init(
callback_url=request.build_absolute_uri("/verify/callback/"),
min_age=18,
user_id=str(request.user.pk) if request.user.is_authenticated else None,
theme="auto",
)
return redirect(result.verify_url)
except XidentError:
return JsonResponse({"error": "Failed to start verification"}, status=500)
@require_GET
def verification_callback(request: HttpRequest) -> HttpResponse:
"""Verify the token server-side after user returns."""
token = request.GET.get("token")
if not token:
return JsonResponse({"error": "Missing token"}, status=400)
try:
session = client.verification.get_result(token)
if session.is_verified():
if request.user.is_authenticated:
request.user.age_verified = True
request.user.age_bracket = session.age_bracket()
request.user.save()
return redirect("/verify/success/")
elif session.is_failed():
return redirect("/verify/failed/")
else:
return JsonResponse({"status": "in_progress"}, status=202)
except XidentError:
return JsonResponse({"error": "Verification check failed"}, status=500)
@require_POST
def webhook(request: HttpRequest) -> HttpResponse:
"""Handle Xident webhook events (exempt from CSRF via decorator)."""
payload = request.body.decode("utf-8")
signature = request.META.get("HTTP_X_XIDENT_SIGNATURE", "")
try:
event = client.webhooks.construct_event(
payload, signature,
getattr(settings, "XIDENT_WEBHOOK_SECRET", ""),
)
if event["type"] == "verification.completed":
pass # Process completed verification
return JsonResponse({"status": "ok"})
except ValueError:
return JsonResponse({"error": "Invalid signature"}, status=400) FastAPI (Async)
Use AsyncXident with FastAPI for non-blocking I/O:
import os
from fastapi import FastAPI, Header, Request
from fastapi.responses import RedirectResponse
from xident import AsyncXident, XidentError
app = FastAPI()
# Use AsyncXident for non-blocking I/O
client = AsyncXident(api_key=os.environ["XIDENT_SECRET_KEY"])
@app.on_event("shutdown")
async def shutdown():
await client.aclose()
@app.get("/verify")
async def start_verification(request: Request):
"""Redirect user to Xident verification widget."""
try:
result = await client.verification.init(
callback_url=str(request.url_for("verification_callback")),
min_age=18,
theme="auto",
)
return RedirectResponse(url=result.verify_url)
except XidentError as e:
return {"error": str(e)}
@app.get("/verify/callback")
async def verification_callback(token: str):
"""Verify the token server-side after user returns."""
try:
session = await client.verification.get_result(token)
if session.is_verified():
return {
"status": "verified",
"age_bracket": session.age_bracket(),
"method": session.method(),
"country": session.country_code,
}
elif session.is_failed():
return {"status": "failed"}
else:
return {"status": "pending"}
except XidentError as e:
return {"error": str(e)}
@app.post("/webhook")
async def webhook(
request: Request,
x_xident_signature: str = Header(""),
):
"""Handle Xident webhook events."""
payload = await request.body() # bytes -- construct_event accepts both
try:
event = client.webhooks.construct_event(
payload, x_xident_signature,
os.environ.get("XIDENT_WEBHOOK_SECRET", ""),
)
match event["type"]:
case "verification.completed":
pass # Process completed verification
case "verification.failed":
pass # Handle failure
return {"status": "ok"}
except ValueError as e:
return {"error": f"Invalid signature: {e}"} Response Types
InitResult
Returned by client.verification.init(). Frozen dataclass.
| Attribute | Type | Description |
|---|---|---|
token | str | Short-lived init token (xit_ prefixed, 10-minute TTL). |
verify_url | str | Full verification URL. Redirect the user here. |
SessionResult
Returned by client.verification.get_result(). Frozen dataclass with helper methods.
| Attribute / Method | Type | Description |
|---|---|---|
id | str | Session UUID. |
status | SessionStatus | Current session status (enum). |
is_verified() | bool | True if status == COMPLETED. |
is_completed() | bool | Alias for is_verified(). |
is_failed() | bool | True if status == FAILED. |
is_pending() | bool | True if PENDING or IN_PROGRESS. |
is_terminal() | bool | True if completed, failed, canceled, or claimed. |
age_bracket() | int | None | Verified age bracket (12, 15, 18, 21, 25) or None. |
method() | str | None | Verification method ('ml_fast', 'ocr', etc.) or None. |
min_age | int | None | Requested age threshold. |
country_code | str | None | ISO 3166-1 alpha-2 country code. |
external_user_id | str | None | Your user_id from init(). |
required_methods | list[str] | None | Required verification methods. |
remaining_attempts | int | None | Retry attempts left. |
liveness_result | dict | None | Liveness check details. |
age_result | dict | None | Age verification details. |
ocr_result | dict | None | Document OCR details. |
face_match_result | dict | None | Face matching details. |
created_at | str | ISO 8601 creation timestamp. |
started_at | str | None | ISO 8601 start timestamp. |
completed_at | str | None | ISO 8601 completion timestamp. |
expires_at | str | None | ISO 8601 expiry timestamp. |
Session Statuses
from xident._types import SessionStatus
# SessionStatus is a str enum
SessionStatus.PENDING # 'pending' -- Session created
SessionStatus.IN_PROGRESS # 'in_progress' -- Verification in progress
SessionStatus.COMPLETED # 'completed' -- Passed verification
SessionStatus.FAILED # 'failed' -- Failed verification
SessionStatus.CANCELED # 'canceled' -- Canceled
SessionStatus.CLAIMED # 'claimed' -- Token claimed/used
# Check if terminal (no more changes possible)
status = SessionStatus.COMPLETED
status.is_terminal # True (property, not method) Environment Variables
| Variable | Description |
|---|---|
XIDENT_SECRET_KEY | Your secret API key (sk_live_* or sk_test_*). Never commit to version control. |
XIDENT_WEBHOOK_SECRET | Webhook signing secret (whsec_*). Found in your dashboard webhook settings. |
Related
- JavaScript SDK — Client-side browser integration
- Node.js SDK — Server-side Node.js SDK with TypeScript
- All SDKs — Overview of all Xident SDKs
- API Reference — Full REST API documentation