Skip to main content

certilayer

Verify human sessions server-side in 3 lines.

The official Python SDK for CertiLayer — an async-first client (with sync twins) for calling CertiLayer's HCS (Human Confidence Score) API after your Web, iOS, Android, or React Native SDK has captured a session. Gate critical actions — login, checkout, signup, password changes — with a single await.

PyPI version License: MIT


Installation

pip install certilayer

Framework integrations are optional extras — install only what you need:

pip install "certilayer[fastapi]"   # FastAPI dependency helper
pip install "certilayer[django]"    # Django middleware (sync + async)

⚠️ Secret key — server-side only

CertiLayerClient takes your secret key (certilayer_live_sk_... or certilayer_test_sk_...). This SDK is for server environments only — never ship this key to a browser, mobile app, or any client-side code. For the client side, use @certilayer/web (or the iOS/Android/React Native SDK), which takes your public key instead.


Quick start

from certilayer import CertiLayerClient, HCSVerdict

client = CertiLayerClient(api_key="certilayer_live_sk_...")

async def login(session_id: str):
    result = await client.verify_session(session_id)
    if result.verdict == HCSVerdict.SYNTHETIC:
        raise HTTPException(status_code=403, detail="bot_detected")

session_id is the session ID produced by whichever client-side SDK (@certilayer/web, iOS, Android, or React Native) is running on the same page/app the user is on.

Use the client as an async context manager, or call aclose() yourself on shutdown:

async with CertiLayerClient(api_key="certilayer_live_sk_...") as client:
    result = await client.verify_session(session_id)

Not async? Every call has a blocking twin that uses its own connection pool:

result = client.verify_session_sync(session_id)
check  = client.quick_check_sync(session_id)

Client configuration

CertiLayerClient(
    api_key: str,
    base_url: str = "https://api.certilayer.net/v1",
    timeout_s: float = 10.0,
    max_retries: int = 2,
)
Parameter Default Notes
api_key — Required. Raises INVALID_API_KEY if empty.
base_url https://api.certilayer.net/v1 Override only for self-hosted deployments.
timeout_s 10.0 Per-request timeout, in seconds.
max_retries 2 Retries 5xx and network errors with exponential backoff. 4xx errors are never retried.

API reference

await client.verify_session(session_id, critical=False) -> VerifyResult

Full verification — returns score, verdict, and session metadata. Sync twin: client.verify_session_sync(...).

@dataclass(frozen=True)
class VerifyResult:
    score: float          # 0.0 – 1.0
    verdict: HCSVerdict   # 'human_verified' | 'human_likely' | 'synthetic'
    session_id: str
    scored_at: str        # UTC ISO-8601

Pass critical=True immediately before a high-stakes action (payment, password change, account mutation). This tells the policy engine to apply stricter critical-action rules, which can escalate a grey-zone score to a step-up challenge or session termination instead of a softer response.

try:
    await client.verify_session(session_id, critical=True)
except CertiLayerError as e:
    if e.code == "STEP_UP_REQUIRED":
        return prompt_webauthn()
    raise

await client.quick_check(session_id, min_score=0.35, critical=False) -> QuickCheckResult

Lighter than verify_session() — just a pass/fail gate decision. Sync twin: client.quick_check_sync(...).

@dataclass(frozen=True)
class QuickCheckResult:
    score: float
    verdict: HCSVerdict
    passed: bool           # True only if score >= min_score AND verdict != synthetic
    degraded: bool = False # True if a guard let the request through during an outage
check = await client.quick_check(session_id)
if not check.passed:
    return JSONResponse({"error": "bot_detected"}, status_code=403)

An unknown verdict from the API is treated as synthetic, so it never passes — even if its score is above min_score.

HCS verdict thresholds

Verdict Score range Recommended action
human_verified ≥ 0.35 Allow — high confidence
human_likely 0.30 – 0.35 Soft friction / step-up auth
synthetic < 0.30 Block or challenge

min_score defaults to 0.35, so only human_verified sessions pass. Use min_score=0.30 to let human_likely through as well. Values far above 0.35 (e.g. 0.90) will block most real users.


Framework integrations

Both integrations fail closed by default: if CertiLayer can't give a verdict, the request is rejected. Opt in to fail_open=True only if you prefer availability over protection during a CertiLayer outage.

FastAPI

from fastapi import Depends
from certilayer import CertiLayerClient

client = CertiLayerClient(api_key=settings.CERTILAYER_KEY)
guard  = client.fastapi_dependency(min_score=0.35)

@app.post("/checkout", dependencies=[Depends(guard)])
async def checkout(): ...
client.fastapi_dependency(
    session_header: str = "x-certilayer-session",
    min_score: float = 0.35,
    fail_open: bool = False,
    require_session: bool = True,
)

Reads the session ID from the given header and raises HTTPException if the check fails. With require_session=False a missing header returns None instead of 403.

Django

# settings.py
client = CertiLayerClient(api_key=settings.CERTILAYER_KEY)
MIDDLEWARE = [
    ...
    client.django_middleware(paths=("/api/",)),
]
client.django_middleware(
    session_header: str = "HTTP_X_CERTILAYER_SESSION",
    min_score: float = 0.35,
    reject_status: int = 403,
    fail_open: bool = False,
    require_session: bool = True,
    paths: Sequence[str] | None = None,
)

The middleware works under both WSGI (sync) and ASGI (async). Set paths to the prefixes you want to protect — with the default None it gates every request, including plain page loads that carry no session header.

Django converts an x-certilayer-session HTTP header into HTTP_X_CERTILAYER_SESSION automatically.

Guard behavior

Situation Default response With fail_open=True
Missing session header 403 missing_session same (unless require_session=False)
Verdict synthetic / score below min_score 403 bot_detected same
SESSION_NOT_FOUND / SESSION_EXPIRED 403 invalid_session / session_expired same
STEP_UP_REQUIRED / SESSION_TERMINATED 403 step_up_required / session_terminated same
INVALID_API_KEY 500 certilayer_misconfigured same
RATE_LIMITED or other unexpected error 503 verification_unavailable same
Network error, timeout, or 5xx from CertiLayer 503 verification_unavailable request allowed (degraded=True)

fail_open only ever covers real outages. A bad key, an unknown session, or a policy decision is never waved through.


Error handling

All SDK errors raise CertiLayerError with a machine-readable .code (and .status_code when the API responded):

from certilayer import CertiLayerError

try:
    result = await client.verify_session(session_id)
except CertiLayerError as e:
    if e.code == "SESSION_NOT_FOUND":
        return JSONResponse({"error": "session_expired"}, status_code=400)
    raise
Code Meaning
INVALID_API_KEY API key missing, empty, or rejected by the server
SESSION_NOT_FOUND No session exists for the given session_id
SESSION_EXPIRED Session exists but has exceeded its TTL
STEP_UP_REQUIRED Policy engine requires additional verification (WebAuthn/OTP)
SESSION_TERMINATED Policy engine has revoked this session as confidently synthetic
RATE_LIMITED Too many requests — back off and retry
NETWORK_ERROR Could not reach the CertiLayer API
TIMEOUT Request exceeded timeout_s
UNEXPECTED_ERROR Unclassified server or SDK error

Upgrading from 1.0.x

1.1.0 changes the guards' defaults to be secure:

  • Fail closed. 1.0.x let requests through on any CertiLayer error. Now only fail_open=True does, and only for outages. To keep the old availability-first behavior: fail_open=True.
  • Missing header is rejected. The Django middleware used to pass requests with no session header straight through. Set paths=(...) to protect only your API routes, or require_session=False to restore the old behavior.
  • passed is stricter. A synthetic verdict never passes, whatever the score.
  • The 403 body no longer includes score.

Requirements

  • Python 3.9+
  • httpx (installed automatically as a dependency)

License

MIT

Support

Metadata

Release files for certilayer 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for certilayer 1.2.0
File Size Uploaded Provenance Actions
certilayer-1.2.0.tar.gz 20.2 kB Without provenance Details for certilayer-1.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for certilayer 1.2.0
File Interpreter ABI Platform Actions
certilayer-1.2.0-py3-none-any.whl Python 3 none any Details for certilayer-1.2.0-py3-none-any.whl

Total release size: 35.5 kB

Release files / certilayer-1.2.0.tar.gz

Download URL certilayer-1.2.0.tar.gz
Size 20.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4cf873beaa317bfa19782c084b7bf6baf39c05f46d3810f1f9daf203b8fdab2d
BLAKE2b-256 checksum
How to use checksums
4bbd9d72a1303447d5c656cc2a59900fb63160faccf53a04f7d79db03ac79279
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / certilayer-1.2.0-py3-none-any.whl

Download URL certilayer-1.2.0-py3-none-any.whl
Size 15.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
93f77f8762a3bd1dfb172dcf2965c663f2db21c1b41237f64981ea9eab278515
BLAKE2b-256 checksum
How to use checksums
4f07cfe2c0f0cb934eff1b2a2c952dd5d2edfb995a59cd1a14572ef7125fed4a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Security for certilayer 1.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page