Arcjet Python SDK reference
This is the reference guide for the Arcjet Python SDK, available on GitHub and licensed under the Apache 2.0 license.
What is Arcjet?
Arcjet is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.Installation
Section titled “Installation”Install from PyPI with your preferred package manager:
uv add arcjetpip install arcjetRequirements
Section titled “Requirements”- Python 3.10 or later
Quick start
Section titled “Quick start”Check out the quick start guide.
Protect vs. Guard
Section titled “Protect vs. Guard”The Arcjet Python SDK has two entrypoints. Pick the one that matches the surface you need to protect:
- Arcjet Protect —
arcjet(async) andarcjet_sync(sync). Protect HTTP request handlers in FastAPI, Flask, Django, and other Python web frameworks. You pass the frameworkrequestobject toprotect()and get anArcjetDecisionback. This is what you want for route handlers and API endpoints. - Arcjet Guard —
arcjet.guard(withlaunch_arcjet/launch_arcjet_sync). Apply security rules where HTTP middleware can’t reach: AI agent tool calls, MCP servers, queue consumers, and background jobs. There is no request object — you pass inputs directly toguard().
Protect (arcjet / arcjet_sync) | Guard (arcjet.guard) | |
|---|---|---|
| Designed for | HTTP request protection | AI agent tool calls, background jobs |
| Request object | Required (protect(request, ...)) | Not needed |
| Rule binding | Rules configured once, input via protect() kwargs | Rules configured as classes, called with input per invocation |
| Rate limit key | IP or characteristics dict | Explicit key string (SHA-256 hashed before sending) |
| Rate limiting | ✅ | ✅ |
| Prompt injection detection | ✅ | ✅ |
| Sensitive information detection | ✅ | ✅ |
| Bot protection | ✅ | — |
| Shield WAF | ✅ | — |
| Email validation | ✅ | — |
| Request filters | ✅ | — |
| IP analysis | ✅ | — |
| Custom rules | — | ✅ |
Both entrypoints ship in the arcjet package — no extra install is required.
Protect
Section titled “Protect”Use arcjet (async) or arcjet_sync (sync) to protect HTTP route handlers.
Async vs. sync client
Section titled “Async vs. sync client”The SDK ships two clients with an identical API:
arcjet— async client for use with FastAPI and other async frameworks. Callawait aj.protect(...).arcjet_sync— sync client for use with Flask, Django, and other sync frameworks. Callaj.protect(...).
Pick the one that matches your framework. The rest of this section shows both where the API differs.
Configuration
Section titled “Configuration”Create a new Arcjet client with your API key and rules. This should be done at startup, outside of the request handler.
The required fields are:
key(str) — Your Arcjet site key. This can be found in the SDK Installation section for the site in the Arcjet Dashboard.rules— The rules to apply to the request. See the various sections of the docs for how to configure these e.g. shield, rate limiting, bot protection, email validation, prompt injection detection, sensitive information detection, request filters.
The optional fields are:
proxies(list[str]) — A list of one or more trusted proxies. These addresses will be excluded when Arcjet is determining the client IP address. This is useful if you are behind a load balancer or proxy that sets the client IP address in a header. See Load balancers & proxies below for an example.
import os
from arcjet import Mode, arcjet, shield
aj = arcjet( # Get your site key from https://app.arcjet.com and set it as an # environment variable rather than hard coding it. key=os.environ["ARCJET_KEY"], rules=[ # Protect against common attacks with Arcjet Shield shield(mode=Mode.LIVE), # Use Mode.DRY_RUN to log only ],)import os
from arcjet import Mode, arcjet_sync, shield
aj = arcjet_sync( # Get your site key from https://app.arcjet.com and set it as an # environment variable rather than hard coding it. key=os.environ["ARCJET_KEY"], rules=[ # Protect against common attacks with Arcjet Shield shield(mode=Mode.LIVE), # Use Mode.DRY_RUN to log only ],)Single instance
Section titled “Single instance”We recommend creating a single instance of the Arcjet client and reusing it throughout your application. This is because the SDK caches decisions and configuration to improve performance.
# Good — one instance, created once at startupaj = arcjet(key=arcjet_key, rules=[...])
# Bad — new instance per request wastes resources@app.get("/")async def index(request: Request): aj = arcjet(key=arcjet_key, rules=[...]) # don't do thisRule modes
Section titled “Rule modes”Each rule can be configured in either Mode.LIVE or Mode.DRY_RUN. When in
DRY_RUN mode, each rule will return its decision, but the end conclusion will
always be ALLOW.
This allows you to run Arcjet in passive / demo mode to test rules before enabling them.
from arcjet import Mode, detect_bot
detect_bot(mode=Mode.DRY_RUN, allow=[])As the top level conclusion will always be ALLOW in DRY_RUN mode, you can
loop through each rule result to check what would have happened:
for result in decision.results: if result.is_denied(): print("Rule returned deny conclusion", result)Multiple rules
Section titled “Multiple rules”You can combine rules to create a more complex protection strategy. For example, you can combine rate limiting and bot protection rules to protect your API from automated clients.
import os
from arcjet import Mode, arcjet, detect_bot, token_bucket
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[ # Create a token bucket rate limit. Other algorithms are supported token_bucket( mode=Mode.LIVE, # Use Mode.DRY_RUN to log only refill_rate=5, # Refill 5 tokens per interval interval=10, # Refill every 10 seconds capacity=10, # Bucket capacity of 10 tokens ), # Detect automated clients detect_bot( mode=Mode.LIVE, allow=[], # An empty allow list blocks all bots ), ],)Environment variables
Section titled “Environment variables”The Arcjet Python SDK uses several environment variables to configure its
behavior.
See Concepts: Environment variables
for more info.
The ARCJET_KEY environment variable is not read automatically and must be
passed explicitly via the key argument.
Load balancers & proxies
Section titled “Load balancers & proxies”If your application is behind a load balancer, Arcjet will only see the IP address of the load balancer and not the real client IP address.
To fix this, most load balancers will set the X-Forwarded-For header with the
real client IP address plus a list of proxies that the request has passed
through.
The problem is that the X-Forwarded-For header can be spoofed by the client,
so you should only trust it if you are sure that the load balancer is setting
it correctly. See the MDN
docs
for more details.
You can configure Arcjet to trust IP addresses in the X-Forwarded-For header
by setting the proxies field in the configuration. This should be a list of
the IP addresses or the CIDR range of your load balancers to be removed, so
that the last IP address in the list is the real client IP address.
Example
Section titled “Example”For example, if the load balancer is at 100.100.100.100 and the client IP
address is 192.168.1.1, the X-Forwarded-For header will be:
X-Forwarded-For: 192.168.1.1, 100.100.100.100You should set the proxies field to ["100.100.100.100"] so Arcjet will use
192.168.1.1 as the client IP address.
You can also specify CIDR ranges to match multiple IP addresses.
import os
from arcjet import arcjet
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[], proxies=[ "100.100.100.100", # A single IP "100.100.100.0/24", # A CIDR for the range ],)protect()
Section titled “protect()”Arcjet exposes a single protect method that is used to execute your
protection rules. It accepts the framework request object as its first
argument. Rules you add to the SDK may require additional keyword arguments,
such as the validate_email rule requiring an email argument.
The async client returns a coroutine that resolves to an ArcjetDecision
object. The sync client returns the ArcjetDecision directly.
import os
from arcjet import Mode, arcjet, token_bucketfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[ # Create a token bucket rate limit. Other algorithms are supported token_bucket( mode=Mode.LIVE, characteristics=["userId"], # Track requests by a custom user ID refill_rate=5, # Refill 5 tokens per interval interval=10, # Refill every 10 seconds capacity=10, # Bucket capacity of 10 tokens ), ],)
@app.get("/")async def index(request: Request): user_id = "user_123" # Replace with your authenticated user ID
# The "userId" characteristic value is required because it is defined in # the characteristics field of the token_bucket rule. decision = await aj.protect( request, requested=5, # Deduct 5 tokens from the bucket characteristics={"userId": user_id}, )
if decision.is_denied(): return JSONResponse({"error": "Too Many Requests"}, status_code=429)
return {"message": "Hello world"}import os
from arcjet import Mode, arcjet_sync, token_bucketfrom flask import Flask, jsonify, request
app = Flask(__name__)
aj = arcjet_sync( key=os.environ["ARCJET_KEY"], rules=[ token_bucket( mode=Mode.LIVE, characteristics=["userId"], refill_rate=5, interval=10, capacity=10, ), ],)
@app.get("/")def index(): user_id = "user_123" # Replace with your authenticated user ID
decision = aj.protect( request, requested=5, characteristics={"userId": user_id}, )
if decision.is_denied(): return jsonify(error="Too Many Requests"), 429
return jsonify(message="Hello world")Parameters
Section titled “Parameters”All parameters are optional keyword arguments passed alongside the request:
| Parameter | Type | Used by |
|---|---|---|
requested | int | Token bucket rate limit |
characteristics | Mapping[str, Any] | Rate limiting (pass values for keys declared in rule config) |
detect_prompt_injection_message | str | Prompt injection detection |
sensitive_info_value | str | Sensitive info detection |
email | str | Email validation |
filter_local | Mapping[str, str] | Request filters (local.* fields) |
extra | Mapping[str, str] | Forwarded verbatim to the Arcjet Decide API as custom metadata (advanced) |
ip_src | str | Manual IP override (advanced) |
Decision
Section titled “Decision”The protect method returns an ArcjetDecision object. It includes the
following properties:
conclusion("ALLOW" | "DENY" | "CHALLENGE" | "ERROR") — The final conclusion based on evaluating each of the configured rules.reason_v2— A typed reason object describing the conclusion. Usereason_v2.typeas a discriminator ("BOT","RATE_LIMIT","SHIELD","EMAIL","SENSITIVE_INFO","PROMPT_INJECTION","FILTER", or"ERROR") and then access type-specific fields.results— A list of per-rule result objects. There will be one for each configured rule so you can inspect the individual results.ip/ip_details— Objects containing Arcjet’s analysis of the client IP address. See IP analysis below for more information.
Conclusion
Section titled “Conclusion”The ArcjetDecision object has the following methods that should be used to
check the conclusion:
is_allowed()(bool) — The request should be allowed.is_denied()(bool) — The request should be denied.is_error()(bool) — There was an unrecoverable error.
The conclusion will be the highest-severity finding when evaluating the
configured rules. "DENY" is the highest severity, followed by "CHALLENGE",
then "ERROR", and finally "ALLOW" as the lowest severity.
For example, when a bot protection rule returns an error and a validate email
rule returns a deny, the overall conclusion would be deny. To access the error
you would have to iterate over the results property on the decision.
Reason
Section titled “Reason”The reason_v2 property of the ArcjetDecision object describes the
conclusion. It will always reflect the highest-priority rule that produced
that conclusion; to inspect other rules, iterate over the results property
on the decision.
Switch on reason_v2.type to map each rule kind to a response. Only branch on
reasons that produce a different response — a branch that returns 403 for
SHIELD when the default already returns 403 is dead code.
if decision.is_denied(): if decision.reason_v2.type == "RATE_LIMIT": return JSONResponse({"error": "Too many requests"}, status_code=429) if decision.reason_v2.type in ("EMAIL", "SENSITIVE_INFO", "PROMPT_INJECTION"): return JSONResponse({"error": "Bad request"}, status_code=400) # BOT, SHIELD, FILTER, and anything else return JSONResponse({"error": "Forbidden"}, status_code=403)Recommended HTTP status mapping:
reason_v2.type | Status |
|---|---|
"RATE_LIMIT" | 429 |
"EMAIL" | 400 |
"SENSITIVE_INFO" | 400 |
"PROMPT_INJECTION" | 400 |
"BOT", "SHIELD", "FILTER", fallback | 403 |
Each variant exposes type-specific fields:
reason_v2.type | Fields |
|---|---|
"BOT" | allowed, denied, spoofed (bool), verified (bool) |
"RATE_LIMIT" | max, remaining, reset_time, reset, window |
"SHIELD" | shield_triggered (bool) |
"EMAIL" | email_types (e.g. ["DISPOSABLE", "NO_MX_RECORDS"]) |
"SENSITIVE_INFO" | allowed, denied (each a list of IdentifiedEntity) |
"PROMPT_INJECTION" | injection_detected (bool) |
"FILTER" | matched_expressions, undetermined_expressions |
"ERROR" | message (str) |
Results
Section titled “Results”The results property contains a list of per-rule result objects. There will
be one for each configured rule so you can inspect the individual results.
for result in decision.results: print("Rule Result", result)Each result includes:
conclusion— The conclusion of the rule ("ALLOW","DENY","CHALLENGE", or"ERROR").reason_v2— A typed reason for this rule’s conclusion (same set of types as on the top-level decision).is_denied()/is_allowed()/is_error()— convenience methods.
For bot results, the SDK also exposes a top-level helper to check whether the request claimed to be a well-known crawler (e.g. Googlebot) but did not match the verified IP ranges:
from arcjet import is_spoofed_bot
if any(is_spoofed_bot(r) for r in decision.results): return jsonify(error="Spoofed bot"), 403See the shield, bot protection, rate limiting, and email validation docs for what each rule’s reason fields mean.
IP analysis
Section titled “IP analysis”Arcjet returns IP metadata with every decision — no extra API calls needed.
# High-level helpers on decision.ipif decision.ip.is_hosting(): # likely a cloud / hosting provider — often suspicious for bots pass
if decision.ip.is_vpn() or decision.ip.is_proxy() or decision.ip.is_tor(): # apply your policy for anonymized traffic pass
if decision.ip.is_abuser(): # IP is associated with known abuse pass
# Typed field access via decision.ip_detailsip = decision.ip_detailsif ip: print(ip.city, ip.country_name) # geolocation print(ip.asn, ip.asn_name) # ASN / network print(ip.is_vpn, ip.is_hosting) # reputationdecision.ip exposes boolean helpers: is_hosting(), is_vpn(),
is_proxy(), is_tor(), is_abuser().
decision.ip_details is an IpDetails dataclass (or None) with these
fields:
- Geolocation:
latitude,longitude,accuracy_radius,timezone,postal_code,city,region,country,country_name,continent,continent_name. - Network (ASN):
asn,asn_name,asn_domain,asn_type(one ofisp,hosting,business,education),asn_country. - Reputation:
is_vpn,is_proxy,is_tor,is_hosting,is_relay,is_abuser,service(e.g."Apple Private Relay").
The IP fields may be missing — decision.ip_details itself may be None,
and individual fields may be None. Geolocation accuracy varies; country is
usually reliable, but city and region can be very inaccurate. Use these fields
for convenience (e.g. suggesting a user location) but do not rely on them
alone.
IP location example
Section titled “IP location example”import os
from arcjet import Mode, arcjet, shieldfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[ shield(mode=Mode.LIVE), ],)
@app.get("/")async def index(request: Request): decision = await aj.protect(request)
if decision.is_denied(): return JSONResponse({"error": "Forbidden"}, status_code=403)
ip = decision.ip_details if ip and ip.country: return { "message": f"Hello {ip.country_name}!", "ip": { "country": ip.country, "country_name": ip.country_name, "continent": ip.continent, "continent_name": ip.continent_name, "asn": ip.asn, "asn_name": ip.asn_name, "asn_domain": ip.asn_domain, }, }
return {"message": "Hello world"}For the IP address 8.8.8.8 you might get the following response. Only the
fields we have data for will be returned:
{ "message": "Hello United States!", "ip": { "country": "US", "country_name": "United States", "continent": "NA", "continent_name": "North America", "asn": "AS15169", "asn_name": "Google LLC", "asn_domain": "google.com" }}Arcjet will automatically detect the IP address of the client making the
request based on the context provided by your framework. In development
(see ARCJET_ENV) we
allow private and internal addresses so that the SDK works correctly locally.
Error handling
Section titled “Error handling”Arcjet is designed to fail open so that a service issue or misconfiguration
does not block all requests. If there is an error condition when processing a
rule, Arcjet will return an ERROR result for that rule and you can check
result.reason_v2.message for more information.
If all other rules that were run returned an ALLOW result, then the final
Arcjet conclusion will be ERROR.
import loggingimport os
from arcjet import Mode, arcjet, sliding_windowfrom fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
logger = logging.getLogger(__name__)
aj = arcjet( key=os.environ["ARCJET_KEY"], rules=[ sliding_window(mode=Mode.LIVE, interval=3600, max=60), ],)
@app.get("/")async def index(request: Request): decision = await aj.protect(request)
for result in decision.results: if result.reason_v2.type == "ERROR": # Fail open by logging the error and continuing logger.warning("Arcjet error: %s", result.reason_v2.message) # You could also fail closed here for very sensitive routes # return JSONResponse({"error": "Service unavailable"}, status_code=503)
if decision.is_denied(): return JSONResponse({"error": "Forbidden"}, status_code=403)
return {"message": "Hello world"}You can check for errors at the top level too:
decision = await aj.protect(request)
if decision.is_error(): # Arcjet service error — fail open or apply a fallback policy passelif decision.is_denied(): return JSONResponse({"error": "Denied"}, status_code=403)arcjet.guard is a lower-level API designed for AI agent tool calls,
MCP servers, and background tasks where there is no HTTP request object. It
gives you fine-grained, per-call control over rate limiting, prompt injection
detection, sensitive information detection, and custom rules. See the
Guards documentation for the full guide.
Use launch_arcjet for async frameworks and launch_arcjet_sync for sync
frameworks. Create a single client at startup and reuse it. Configure each
rule once, then bind input and call guard() per invocation.
import osimport time
from arcjet.guard import DetectPromptInjection, TokenBucket, launch_arcjet
# Create a single guard client at startup and reuse itaj = launch_arcjet(key=os.environ["ARCJET_KEY"])
# Configure rules once at module scope so per-rule result accessors workuser_limit = TokenBucket( refill_rate=100, interval_seconds=60, max_tokens=1000, bucket="user-tools", # name this per use case to avoid collisions)prompt_scan = DetectPromptInjection()
async def handle_tool_call(user_id: str, message: str) -> str: # Bind input and call guard() for each invocation. Hardcode `label` # as a string literal so it stays greppable and groups in the dashboard. decision = await aj.guard( label="tools.weather", rules=[ user_limit(key=user_id, requested=5), prompt_scan(message), ], metadata={"user_id": user_id}, )
if decision.conclusion == "DENY": # Branch on which rule denied to give the caller something actionable rate_limited = user_limit.denied_result(decision) if rate_limited: retry_in = max(0, rate_limited.reset_at_unix_seconds - int(time.time())) raise RuntimeError(f"Rate limited — retry in {retry_in}s") raise RuntimeError("Blocked")
# Safe to proceed with the tool call return "..."Configure each rule once at module scope so you have a stable reference for the
typed per-rule result accessors (e.g. user_limit.denied_result(decision)).
All rules accept keyword-only arguments. Every rule accepts mode ("LIVE"
or "DRY_RUN"), label (an observability label that appears in the
dashboard), and metadata (a dict[str, str] of analytics context).
Rate limiting
Section titled “Rate limiting”from arcjet.guard import TokenBucket, FixedWindow, SlidingWindow
user_limit = TokenBucket( refill_rate=10, interval_seconds=60, max_tokens=100, bucket="user-tools", # name this per use case to avoid collisions)
team_limit = FixedWindow( max_requests=1000, window_seconds=3600, bucket="team-api",)
api_limit = SlidingWindow( max_requests=500, interval_seconds=60, bucket="public-api",)Rate limit state is tracked server-side by the combination of bucket and
other configuration. Set bucket explicitly to avoid collisions between
different rules — two rate limit rules created with the default bucket name
will share counters.
At call time, all three accept key=... (the per-caller identifier — user ID,
session ID, tenant) and requested=N (tokens / requests consumed; default
1).
Prompt injection detection
Section titled “Prompt injection detection”from arcjet.guard import DetectPromptInjection
prompt_scan = DetectPromptInjection()
decision = await aj.guard( label="tools.weather", rules=[prompt_scan(user_message)],)Sensitive information detection
Section titled “Sensitive information detection”Runs locally via WASM — the raw text never leaves the SDK; only a SHA-256
hash is sent alongside the local result. Valid entity types: "EMAIL",
"PHONE_NUMBER", "IP_ADDRESS", "CREDIT_CARD_NUMBER".
from arcjet.guard import LocalDetectSensitiveInfo
sensitive = LocalDetectSensitiveInfo( deny=["EMAIL", "CREDIT_CARD_NUMBER"],)allow and deny are mutually exclusive.
Custom rules
Section titled “Custom rules”Subclass LocalCustomRule and override evaluate (sync) or evaluate_async
(async) to implement custom logic with typed Config / Input / Data
shapes.
guard()
Section titled “guard()”The guard call takes a label identifying the invocation site, a list of
bound rule inputs, and optional metadata:
| Parameter | Type | Description |
|---|---|---|
label | str | Label identifying this guard call (required) |
rules | Sequence[RuleWithInput] | Bound rule inputs (required) |
metadata | dict[str, str] | None | Optional key-value metadata |
Guard decision
Section titled “Guard decision”The guard decision exposes:
conclusion—"ALLOW"or"DENY". Always check before proceeding.has_error()—Trueif something went wrong during rule evaluation (service unreachable, rule execution failure). The SDK fails open — log it but don’t block the caller.results— per-rule outcomes.
For useful error messages, branch on which rule denied — not just on
DENY. Each rule defined at module scope exposes typed result accessors:
rule.result(decision)— the result for this rule, orNone.rule.denied_result(decision)— the result, but only if the rule denied the request. ReturnsNoneotherwise.
import time
if decision.conclusion == "DENY": rate_limited = user_limit.denied_result(decision) if rate_limited: retry_in = max( 0, rate_limited.reset_at_unix_seconds - int(time.time()) ) raise TaskBlocked(f"rate limited — retry in {retry_in}s") raise TaskBlocked("blocked")For token bucket rate limits the denied result also exposes remaining_tokens,
max_tokens, refill_rate, and refill_interval_seconds. Fixed and sliding
window results expose remaining_requests, max_requests, and
reset_at_unix_seconds.
Hardcode the label argument to guard() as a string literal (e.g.
"tools.get_weather", not f"tools.{name}"). Hardcoded labels stay
greppable and the dashboard groups by them. Pass metadata whenever you
have useful auditing context (e.g. {"user_id": ..., "request_id": ...}) —
it shows up in the dashboard and makes debugging much easier later.
Version support
Section titled “Version support”Arcjet supports Python 3.10 and above.
Technical support is provided for the current major version of the Arcjet SDK for all users and for the current and previous major versions for paid users. We will provide security fixes for the current and previous major versions.