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.
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=Truedoes, 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, orrequire_session=Falseto restore the old behavior. passedis stricter. Asyntheticverdict 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
- Docs: certilayer.net/docs
- Issues: GitHub Issues or contact@certilayer.net
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.