Python SDK

v1.0.0 Secret Key (sk_live_*) Python 3.10+ Sync + Async httpx-based

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
idstrSession UUID.
statusSessionStatusCurrent session status (enum).
is_verified()boolTrue if status == COMPLETED.
is_completed()boolAlias for is_verified().
is_failed()boolTrue if status == FAILED.
is_pending()boolTrue if PENDING or IN_PROGRESS.
is_terminal()boolTrue if completed, failed, canceled, or claimed.
age_bracket()int | NoneVerified age bracket (12, 15, 18, 21, 25) or None.
method()str | NoneVerification method ('ml_fast', 'ocr', etc.) or None.
min_ageint | NoneRequested age threshold.
country_codestr | NoneISO 3166-1 alpha-2 country code.
external_user_idstr | NoneYour user_id from init().
required_methodslist[str] | NoneRequired verification methods.
remaining_attemptsint | NoneRetry attempts left.
liveness_resultdict | NoneLiveness check details.
age_resultdict | NoneAge verification details.
ocr_resultdict | NoneDocument OCR details.
face_match_resultdict | NoneFace matching details.
created_atstrISO 8601 creation timestamp.
started_atstr | NoneISO 8601 start timestamp.
completed_atstr | NoneISO 8601 completion timestamp.
expires_atstr | NoneISO 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