[{"data":1,"prerenderedAt":1302},["ShallowReactive",2],{"docs-\u002Fdocs\u002Fpython-quickstart":3,"docs-nav":1288},{"id":4,"title":5,"body":6,"description":1280,"extension":1281,"meta":1282,"navigation":154,"order":170,"path":1283,"section":1284,"seo":1285,"stem":1286,"__hash__":1287},"docs\u002Fdocs\u002Fpython-quickstart.md","Python",{"type":7,"value":8,"toc":1261},"minimark",[9,14,18,21,33,37,65,69,96,100,107,112,180,183,187,236,240,286,289,292,299,308,312,315,340,355,393,402,406,409,412,421,424,437,440,449,456,460,467,476,483,498,514,518,521,531,540,547,550,554,557,560,636,642,646,661,675,681,688,703,707,714,781,854,895,898,923,935,939,942,1195,1201,1204,1213,1224,1228,1257],[10,11,13],"h2",{"id":12},"overview","Overview",[15,16,17],"p",{},"This guide adds BlockBots to a Python web app. You will install one package and add one middleware — detection then runs on every request before your views do.",[15,19,20],{},"It takes about five minutes. One dependency, no database, no build step.",[22,23,24],"blockquote",{},[15,25,26,27,32],{},"This guide covers server-side protection. If your pages are rendered entirely in the browser, see the ",[28,29,31],"a",{"href":30},"\u002Fdocs\u002Fjavascript-quickstart","JavaScript guide",". The two work together.",[10,34,36],{"id":35},"prerequisites","Prerequisites",[38,39,40,49,62],"ul",{},[41,42,43,44,48],"li",{},"Python ",[45,46,47],"strong",{},"3.9"," or higher",[41,50,51,52,58,59],{},"An API key — create one at ",[28,53,57],{"href":54,"rel":55},"https:\u002F\u002Fpanel.blockbots.org",[56],"nofollow","panel.blockbots.org"," under ",[45,60,61],{},"Settings → API Keys",[41,63,64],{},"The domain registered on that key, exactly as visitors reach it",[10,66,68],{"id":67},"step-1-install","Step 1 — Install",[70,71,77],"pre",{"className":72,"code":73,"filename":74,"language":75,"meta":76,"style":76},"language-bash shiki shiki-themes github-dark-dimmed","pip install blockbots\n","terminal","bash","",[78,79,80],"code",{"__ignoreMap":76},[81,82,85,89,93],"span",{"class":83,"line":84},"line",1,[81,86,88],{"style":87},"--shiki-default:#F69D50","pip",[81,90,92],{"style":91},"--shiki-default:#96D0FF"," install",[81,94,95],{"style":91}," blockbots\n",[10,97,99],{"id":98},"step-2-add-the-middleware","Step 2 — Add the middleware",[15,101,102,103,106],{},"Pick the one that matches your framework. In all three cases it must run ",[45,104,105],{},"before"," your own code: a check that runs after your view has already answered protects nothing.",[108,109,111],"h3",{"id":110},"django","Django",[70,113,118],{"className":114,"code":115,"filename":116,"language":117,"meta":76,"style":76},"language-python shiki shiki-themes github-dark-dimmed","MIDDLEWARE = [\n    \"blockbots.django_middleware.BlockbotsMiddleware\",   # first\n    \"django.middleware.security.SecurityMiddleware\",\n    # ... the rest of yours\n]\n\nBLOCKBOTS = {\n    \"api_key\": \"YOUR_API_KEY\",\n    \"exclude\": [\"\u002Fadmin\"],\n}\n","settings.py","python",[78,119,120,125,131,137,143,149,156,162,168,174],{"__ignoreMap":76},[81,121,122],{"class":83,"line":84},[81,123,124],{},"MIDDLEWARE = [\n",[81,126,128],{"class":83,"line":127},2,[81,129,130],{},"    \"blockbots.django_middleware.BlockbotsMiddleware\",   # first\n",[81,132,134],{"class":83,"line":133},3,[81,135,136],{},"    \"django.middleware.security.SecurityMiddleware\",\n",[81,138,140],{"class":83,"line":139},4,[81,141,142],{},"    # ... the rest of yours\n",[81,144,146],{"class":83,"line":145},5,[81,147,148],{},"]\n",[81,150,152],{"class":83,"line":151},6,[81,153,155],{"emptyLinePlaceholder":154},true,"\n",[81,157,159],{"class":83,"line":158},7,[81,160,161],{},"BLOCKBOTS = {\n",[81,163,165],{"class":83,"line":164},8,[81,166,167],{},"    \"api_key\": \"YOUR_API_KEY\",\n",[81,169,171],{"class":83,"line":170},9,[81,172,173],{},"    \"exclude\": [\"\u002Fadmin\"],\n",[81,175,177],{"class":83,"line":176},10,[81,178,179],{},"}\n",[15,181,182],{},"Put it first in the list. Django runs middleware top-down on the way in, so anything above it answers before the check does.",[108,184,186],{"id":185},"flask-bottle-pyramid-any-wsgi-app","Flask, Bottle, Pyramid — any WSGI app",[70,188,191],{"className":114,"code":189,"filename":190,"language":117,"meta":76,"style":76},"from flask import Flask\nfrom blockbots.wsgi import BlockbotsMiddleware\n\napp = Flask(__name__)\napp.wsgi_app = BlockbotsMiddleware(app.wsgi_app, api_key=\"YOUR_API_KEY\")\n\n@app.get(\"\u002F\")\ndef index():\n    return \"Welcome\"\n","app.py",[78,192,193,198,203,207,212,217,221,226,231],{"__ignoreMap":76},[81,194,195],{"class":83,"line":84},[81,196,197],{},"from flask import Flask\n",[81,199,200],{"class":83,"line":127},[81,201,202],{},"from blockbots.wsgi import BlockbotsMiddleware\n",[81,204,205],{"class":83,"line":133},[81,206,155],{"emptyLinePlaceholder":154},[81,208,209],{"class":83,"line":139},[81,210,211],{},"app = Flask(__name__)\n",[81,213,214],{"class":83,"line":145},[81,215,216],{},"app.wsgi_app = BlockbotsMiddleware(app.wsgi_app, api_key=\"YOUR_API_KEY\")\n",[81,218,219],{"class":83,"line":151},[81,220,155],{"emptyLinePlaceholder":154},[81,222,223],{"class":83,"line":158},[81,224,225],{},"@app.get(\"\u002F\")\n",[81,227,228],{"class":83,"line":164},[81,229,230],{},"def index():\n",[81,232,233],{"class":83,"line":170},[81,234,235],{},"    return \"Welcome\"\n",[108,237,239],{"id":238},"fastapi-starlette-litestar-any-asgi-app","FastAPI, Starlette, Litestar — any ASGI app",[70,241,244],{"className":114,"code":242,"filename":243,"language":117,"meta":76,"style":76},"from fastapi import FastAPI\nfrom blockbots.asgi import BlockbotsMiddleware\n\napp = FastAPI()\napp.add_middleware(BlockbotsMiddleware, api_key=\"YOUR_API_KEY\")\n\n@app.get(\"\u002F\")\ndef index():\n    return \"Welcome\"\n","main.py",[78,245,246,251,256,260,265,270,274,278,282],{"__ignoreMap":76},[81,247,248],{"class":83,"line":84},[81,249,250],{},"from fastapi import FastAPI\n",[81,252,253],{"class":83,"line":127},[81,254,255],{},"from blockbots.asgi import BlockbotsMiddleware\n",[81,257,258],{"class":83,"line":133},[81,259,155],{"emptyLinePlaceholder":154},[81,261,262],{"class":83,"line":139},[81,263,264],{},"app = FastAPI()\n",[81,266,267],{"class":83,"line":145},[81,268,269],{},"app.add_middleware(BlockbotsMiddleware, api_key=\"YOUR_API_KEY\")\n",[81,271,272],{"class":83,"line":151},[81,273,155],{"emptyLinePlaceholder":154},[81,275,276],{"class":83,"line":158},[81,277,225],{},[81,279,280],{"class":83,"line":164},[81,281,230],{},[81,283,284],{"class":83,"line":170},[81,285,235],{},[15,287,288],{},"The verify-bot call is blocking, so the ASGI middleware runs it in a worker thread — your event loop is never held for the length of a network round trip.",[15,290,291],{},"There is nothing to call. The middleware decides on its own and answers the requests it stops.",[15,293,294,295,298],{},"Read the key from the environment rather than hard-coding it — ",[78,296,297],{},"BLOCKBOTS_API_KEY"," is picked up automatically, so you can leave the argument out entirely:",[70,300,302],{"className":114,"code":301,"filename":190,"language":117,"meta":76,"style":76},"app.wsgi_app = BlockbotsMiddleware(app.wsgi_app)\n",[78,303,304],{"__ignoreMap":76},[81,305,306],{"class":83,"line":84},[81,307,301],{},[10,309,311],{"id":310},"step-3-exclude-what-must-not-be-checked","Step 3 — Exclude what must not be checked",[15,313,314],{},"Health checks, webhooks, your admin area and your own API routes should go straight through:",[70,316,318],{"className":114,"code":317,"filename":190,"language":117,"meta":76,"style":76},"BlockbotsMiddleware(\n    app.wsgi_app,\n    exclude=[\"\u002Fhealthz\", \"\u002Fwebhooks\", \"\u002Fapi\", \"\u002Fadmin\"],\n)\n",[78,319,320,325,330,335],{"__ignoreMap":76},[81,321,322],{"class":83,"line":84},[81,323,324],{},"BlockbotsMiddleware(\n",[81,326,327],{"class":83,"line":127},[81,328,329],{},"    app.wsgi_app,\n",[81,331,332],{"class":83,"line":133},[81,333,334],{},"    exclude=[\"\u002Fhealthz\", \"\u002Fwebhooks\", \"\u002Fapi\", \"\u002Fadmin\"],\n",[81,336,337],{"class":83,"line":139},[81,338,339],{},")\n",[15,341,342,343,346,347,350,351,354],{},"A string matches that path and everything under it — ",[78,344,345],{},"\u002Fapi"," covers ",[78,348,349],{},"\u002Fapi\u002Fusers",", but not ",[78,352,353],{},"\u002Fapi-docs",". Compiled regexes and callables work too:",[70,356,358],{"className":114,"code":357,"filename":190,"language":117,"meta":76,"style":76},"import re\n\nexclude = [\n    \"\u002Fwebhooks\",                                           # path and everything under it\n    re.compile(r\"^\u002Finternal\u002F\"),                            # regular expression\n    lambda environ: environ.get(\"HTTP_X_INTERNAL\") == \"1\", # your own rule\n]\n",[78,359,360,365,369,374,379,384,389],{"__ignoreMap":76},[81,361,362],{"class":83,"line":84},[81,363,364],{},"import re\n",[81,366,367],{"class":83,"line":127},[81,368,155],{"emptyLinePlaceholder":154},[81,370,371],{"class":83,"line":133},[81,372,373],{},"exclude = [\n",[81,375,376],{"class":83,"line":139},[81,377,378],{},"    \"\u002Fwebhooks\",                                           # path and everything under it\n",[81,380,381],{"class":83,"line":145},[81,382,383],{},"    re.compile(r\"^\u002Finternal\u002F\"),                            # regular expression\n",[81,385,386],{"class":83,"line":151},[81,387,388],{},"    lambda environ: environ.get(\"HTTP_X_INTERNAL\") == \"1\", # your own rule\n",[81,390,391],{"class":83,"line":158},[81,392,148],{},[22,394,395],{},[15,396,397,398,401],{},"On Django, add ",[78,399,400],{},"\"\u002Fadmin\""," unless you want staff hitting the verification page.",[10,403,405],{"id":404},"step-4-check-it-works","Step 4 — Check it works",[15,407,408],{},"Open your site in a private window. You should see a verification screen for a second, then your own page.",[15,410,411],{},"To confirm what is going on under the hood, switch on the self-test:",[70,413,415],{"className":114,"code":414,"filename":190,"language":117,"meta":76,"style":76},"BlockbotsMiddleware(app.wsgi_app, selftest=\"pick-a-long-secret\")\n",[78,416,417],{"__ignoreMap":76},[81,418,419],{"class":83,"line":84},[81,420,414],{},[15,422,423],{},"Then request any page with that secret:",[70,425,427],{"className":72,"code":426,"filename":74,"language":75,"meta":76,"style":76},"curl \"https:\u002F\u002Fyour-site.com\u002F?bb-selftest=pick-a-long-secret\"\n",[78,428,429],{"__ignoreMap":76},[81,430,431,434],{"class":83,"line":84},[81,432,433],{"style":87},"curl",[81,435,436],{"style":91}," \"https:\u002F\u002Fyour-site.com\u002F?bb-selftest=pick-a-long-secret\"\n",[15,438,439],{},"A healthy install answers like this:",[70,441,447],{"className":442,"code":444,"filename":445,"language":446,"meta":76},[443],"language-text","Blockbots self-test\n\nenvironment\n  python                 3.12.3\n  platform               generic (has a socket)\n  cryptography           yes\n  library active         yes\n  enabled option         true\n  service state          ok\n\nthis request\n  socket address         203.0.113.9\n  trusted proxy          NO\n  resolved client ip     203.0.113.9\n  ip came from           socket\n  looks like a browser   yes\n  navigation             yes\n  host                   your-site.com\n  path excluded          NO\n\ntoken\n  __bbjwt cookie         absent - visitor has not been verified yet\n\ncache\n  store                  memory\n  entries                14\n  ttl                    3600s\n\napi\n  url                    https:\u002F\u002Fapi.blockbots.org\u002Fv1\u002Fverify-bot\n  round trip             86 ms\n  reachable              yes\n  answered               not_bot\n","output","text",[78,448,444],{"__ignoreMap":76},[15,450,451,452,455],{},"Nothing secret is printed — not your API key, not the visitor's token. Remove the ",[78,453,454],{},"selftest"," option when you are done; without it the page does not exist.",[10,457,459],{"id":458},"step-5-if-you-use-a-cdn-or-reverse-proxy","Step 5 — If you use a CDN or reverse proxy",[15,461,462,463,466],{},"Cloudflare is recognised automatically, and so are nginx, Docker and a load balancer on your own network. ",[45,464,465],{},"Any other CDN needs one line",", otherwise every visitor arrives as the same address and nobody can pass verification:",[70,468,470],{"className":114,"code":469,"filename":190,"language":117,"meta":76,"style":76},"BlockbotsMiddleware(app.wsgi_app, trusted_proxies=\"203.0.113.0\u002F24,198.51.100.7\")\n",[78,471,472],{"__ignoreMap":76},[81,473,474],{"class":83,"line":84},[81,475,469],{},[15,477,478,479,482],{},"Use the address ranges your CDN connects from — your provider publishes them. ",[78,480,481],{},"X-Forwarded-For"," is read right to left, skipping your own hops.",[15,484,485,486,489,490,493,494,497],{},"In the self-test, ",[78,487,488],{},"trusted proxy"," should read ",[78,491,492],{},"yes"," and ",[78,495,496],{},"resolved client ip"," should be the visitor's real address, not your CDN's.",[22,499,500],{},[15,501,502,503,493,506,509,510,513],{},"Django's ",[78,504,505],{},"SECURE_PROXY_SSL_HEADER",[78,507,508],{},"USE_X_FORWARDED_HOST"," are Django's own settings and do not affect BlockBots. Use ",[78,511,512],{},"trusted_proxies",".",[10,515,517],{"id":516},"step-6-serverless","Step 6 — Serverless",[15,519,520],{},"Vercel, Netlify and Deno Deploy are recognised from their own environment and need nothing.",[15,522,523,526,527,530],{},[45,524,525],{},"AWS Lambda and Google Cloud Run"," usually work too, because Mangum, Zappa and the Serverless framework all fill in the client address from the event. If the self-test shows ",[78,528,529],{},"ip came from  unresolved",", name the header your platform sets:",[70,532,534],{"className":114,"code":533,"filename":190,"language":117,"meta":76,"style":76},"BlockbotsMiddleware(app.wsgi_app, client_ip=\"x-real-ip\")\n",[78,535,536],{"__ignoreMap":76},[81,537,538],{"class":83,"line":84},[81,539,533],{},[15,541,542,543,546],{},"Why it is not guessed: with no connection to check a header against, trusting one would let any visitor choose their own address with a single ",[78,544,545],{},"curl -H"," flag, which defeats the verdict cache and the cookie's IP binding at once.",[15,548,549],{},"Without an address, protection switches itself off for those requests and says so in your log — deliberately, because a cookie with no address to bind to would send every visitor back to the verification page forever.",[10,551,553],{"id":552},"step-7-behind-gunicorn-or-uvicorn-workers","Step 7 — Behind gunicorn or uvicorn workers",[15,555,556],{},"Nothing to do, but worth knowing: the verdict cache lives in the process, and each worker has its own. A crawler verdict is learned once per worker rather than once per site, which costs a few extra cloud calls and nothing else.",[15,558,559],{},"To share them, hand over a store. Any object with two methods works:",[70,561,564],{"className":114,"code":562,"filename":563,"language":117,"meta":76,"style":76},"import redis\nfrom blockbots import Blockbots\n\nclient = redis.Redis(decode_responses=True)\n\nclass RedisStore:\n    def get(self, key):\n        return client.get(key)\n\n    def set(self, key, value, ttl):\n        client.set(key, value, ex=ttl)\n\nbb = Blockbots(api_key=..., store=RedisStore())\napp.wsgi_app = BlockbotsMiddleware(app.wsgi_app, client=bb)\n","store.py",[78,565,566,571,576,580,585,589,594,599,604,608,613,619,624,630],{"__ignoreMap":76},[81,567,568],{"class":83,"line":84},[81,569,570],{},"import redis\n",[81,572,573],{"class":83,"line":127},[81,574,575],{},"from blockbots import Blockbots\n",[81,577,578],{"class":83,"line":133},[81,579,155],{"emptyLinePlaceholder":154},[81,581,582],{"class":83,"line":139},[81,583,584],{},"client = redis.Redis(decode_responses=True)\n",[81,586,587],{"class":83,"line":145},[81,588,155],{"emptyLinePlaceholder":154},[81,590,591],{"class":83,"line":151},[81,592,593],{},"class RedisStore:\n",[81,595,596],{"class":83,"line":158},[81,597,598],{},"    def get(self, key):\n",[81,600,601],{"class":83,"line":164},[81,602,603],{},"        return client.get(key)\n",[81,605,606],{"class":83,"line":170},[81,607,155],{"emptyLinePlaceholder":154},[81,609,610],{"class":83,"line":176},[81,611,612],{},"    def set(self, key, value, ttl):\n",[81,614,616],{"class":83,"line":615},11,[81,617,618],{},"        client.set(key, value, ex=ttl)\n",[81,620,622],{"class":83,"line":621},12,[81,623,155],{"emptyLinePlaceholder":154},[81,625,627],{"class":83,"line":626},13,[81,628,629],{},"bb = Blockbots(api_key=..., store=RedisStore())\n",[81,631,633],{"class":83,"line":632},14,[81,634,635],{},"app.wsgi_app = BlockbotsMiddleware(app.wsgi_app, client=bb)\n",[15,637,638,641],{},[78,639,640],{},"decode_responses=True"," matters — the store deals in strings. A store that raises is logged and treated as a miss, so a Redis outage costs cloud calls, not requests. Keys are shared with the PHP and Node clients, so one Redis can serve a mixed estate.",[10,643,645],{"id":644},"how-it-works","How it works",[647,648,649,652,658],"ol",{},[41,650,651],{},"A visitor with a valid cookie passes through instantly — no network call.",[41,653,654,655,513],{},"Anything that does not look like a browser is checked against the cloud. Crawlers you allow, such as Googlebot, get your content; impostors using their name get ",[78,656,657],{},"403",[41,659,660],{},"Everyone else sees the verification page, gets a signed cookie, and the page reloads.",[15,662,663,664,493,667,670,671,674],{},"Verdicts are cached, so a crawler costs one cloud call per hour rather than one per page. ",[78,665,666],{},"fetch",[78,668,669],{},"XHR"," requests are never interrupted, and ",[78,672,673],{},"POST"," bodies are never swallowed. If the cloud is unreachable, the middleware steps aside and your site keeps serving.",[15,676,677,680],{},[45,678,679],{},"A dead or expired key never reaches your visitors."," Before the verification page is served for the first time, BlockBots asks whether the cloud is serving your key at all. A rejection switches protection off for five minutes rather than handing out a page whose script would fail the same way and never reload — so a lapsed subscription leaves your site unprotected, not broken. The answer is cached, so this costs one request an hour, not one per visitor, and renewing a subscription takes effect on its own with no restart.",[15,682,683,684,687],{},"Everything goes to the ",[78,685,686],{},"blockbots"," logger — nothing is printed and nothing is raised:",[70,689,691],{"className":114,"code":690,"language":117,"meta":76,"style":76},"import logging\nlogging.getLogger(\"blockbots\").setLevel(logging.INFO)\n",[78,692,693,698],{"__ignoreMap":76},[81,694,695],{"class":83,"line":84},[81,696,697],{},"import logging\n",[81,699,700],{"class":83,"line":127},[81,701,702],{},"logging.getLogger(\"blockbots\").setLevel(logging.INFO)\n",[10,704,706],{"id":705},"making-the-decision-yourself","Making the decision yourself",[15,708,709,710,713],{},"For anything the three adapters do not cover — or when you would rather log than block — ",[78,711,712],{},"check()"," reads a request and returns a verdict without touching the response:",[70,715,718],{"className":114,"code":716,"filename":717,"language":117,"meta":76,"style":76},"from blockbots import Blockbots\nfrom blockbots.wsgi import request_from_environ\n\nbb = Blockbots(api_key=...)\n\ndecision = bb.check(request_from_environ(environ))\n# Decision(action='challenge', allow=False, reason='unverified',\n#          ip='203.0.113.9', user_agent='…', response=Response(...))\n\nif not decision.allow:\n    return send(decision.response.status,\n                decision.response.headers,\n                decision.response.body)\n","middleware.py",[78,719,720,724,729,733,738,742,747,752,757,761,766,771,776],{"__ignoreMap":76},[81,721,722],{"class":83,"line":84},[81,723,575],{},[81,725,726],{"class":83,"line":127},[81,727,728],{},"from blockbots.wsgi import request_from_environ\n",[81,730,731],{"class":83,"line":133},[81,732,155],{"emptyLinePlaceholder":154},[81,734,735],{"class":83,"line":139},[81,736,737],{},"bb = Blockbots(api_key=...)\n",[81,739,740],{"class":83,"line":145},[81,741,155],{"emptyLinePlaceholder":154},[81,743,744],{"class":83,"line":151},[81,745,746],{},"decision = bb.check(request_from_environ(environ))\n",[81,748,749],{"class":83,"line":158},[81,750,751],{},"# Decision(action='challenge', allow=False, reason='unverified',\n",[81,753,754],{"class":83,"line":164},[81,755,756],{},"#          ip='203.0.113.9', user_agent='…', response=Response(...))\n",[81,758,759],{"class":83,"line":170},[81,760,155],{"emptyLinePlaceholder":154},[81,762,763],{"class":83,"line":176},[81,764,765],{},"if not decision.allow:\n",[81,767,768],{"class":83,"line":615},[81,769,770],{},"    return send(decision.response.status,\n",[81,772,773],{"class":83,"line":621},[81,774,775],{},"                decision.response.headers,\n",[81,777,778],{"class":83,"line":626},[81,779,780],{},"                decision.response.body)\n",[782,783,784,802],"table",{},[785,786,787],"thead",{},[788,789,790,796,799],"tr",{},[791,792,793],"th",{},[78,794,795],{},"action",[791,797,798],{},"Meaning",[791,800,801],{},"What to do",[803,804,805,819,839],"tbody",{},[788,806,807,813,816],{},[808,809,810],"td",{},[78,811,812],{},"allow",[808,814,815],{},"Let it through",[808,817,818],{},"Call your view",[788,820,821,826,829],{},[808,822,823],{},[78,824,825],{},"block",[808,827,828],{},"A bot failed its check",[808,830,831,832,835,836,838],{},"Send ",[78,833,834],{},"response"," — a ",[78,837,657],{}," page",[788,840,841,846,849],{},[808,842,843],{},[78,844,845],{},"challenge",[808,847,848],{},"Unverified visitor",[808,850,831,851,853],{},[78,852,834],{}," — the verification page, which reloads once passed",[15,855,856,859,860,863,864,863,867,863,870,863,873,863,876,863,879,863,882,863,885,863,888,891,892,513],{},[78,857,858],{},"reason"," is for your logs: ",[78,861,862],{},"token_valid",", ",[78,865,866],{},"verified_bot",[78,868,869],{},"cached_allowed",[78,871,872],{},"not_a_navigation",[78,874,875],{},"excluded",[78,877,878],{},"unverified",[78,880,881],{},"api_request_failed",[78,883,884],{},"ip_unresolved",[78,886,887],{},"unauthorized",[78,889,890],{},"disabled",", or the cloud's own reason for blocking such as ",[78,893,894],{},"fake_googlebot",[15,896,897],{},"Build the request from whatever you have:",[70,899,901],{"className":114,"code":900,"language":117,"meta":76,"style":76},"from blockbots.wsgi import request_from_environ             # a WSGI environ\nfrom blockbots.asgi import request_from_scope               # an ASGI scope\nfrom blockbots.django_middleware import request_from_django # a Django HttpRequest\nfrom blockbots import build_request_from_url                # a URL plus headers\n",[78,902,903,908,913,918],{"__ignoreMap":76},[81,904,905],{"class":83,"line":84},[81,906,907],{},"from blockbots.wsgi import request_from_environ             # a WSGI environ\n",[81,909,910],{"class":83,"line":127},[81,911,912],{},"from blockbots.asgi import request_from_scope               # an ASGI scope\n",[81,914,915],{"class":83,"line":133},[81,916,917],{},"from blockbots.django_middleware import request_from_django # a Django HttpRequest\n",[81,919,920],{"class":83,"line":139},[81,921,922],{},"from blockbots import build_request_from_url                # a URL plus headers\n",[15,924,925,926,863,929,863,932,513],{},"Each adapter also leaves the decision where a view can read it without redoing the work: ",[78,927,928],{},"environ[\"blockbots.decision\"]",[78,930,931],{},"scope[\"blockbots.decision\"]",[78,933,934],{},"request.blockbots",[10,936,938],{"id":937},"configuration","Configuration",[15,940,941],{},"Everything except the key is optional.",[782,943,944,957],{},[785,945,946],{},[788,947,948,951,954],{},[791,949,950],{},"Option",[791,952,953],{},"Default",[791,955,956],{},"What it does",[803,958,959,977,989,1001,1016,1027,1045,1060,1075,1090,1105,1120,1134,1147,1166,1181],{},[788,960,961,966,971],{},[808,962,963],{},[78,964,965],{},"api_key",[808,967,968],{},[78,969,970],{},"$BLOCKBOTS_API_KEY",[808,972,973,976],{},[45,974,975],{},"Required."," Your API key",[788,978,979,983,986],{},[808,980,981],{},[78,982,512],{},[808,984,985],{},"—",[808,987,988],{},"Your CDN or load balancer addresses, comma separated or a list",[788,990,991,996,998],{},[808,992,993],{},[78,994,995],{},"client_ip",[808,997,985],{},[808,999,1000],{},"Header name or callable, for hosts with no connection to read",[788,1002,1003,1008,1013],{},[808,1004,1005],{},[78,1006,1007],{},"exclude",[808,1009,1010],{},[78,1011,1012],{},"()",[808,1014,1015],{},"Paths left unchecked: string prefix, compiled regex, or callable",[788,1017,1018,1022,1024],{},[808,1019,1020],{},[78,1021,454],{},[808,1023,985],{},[808,1025,1026],{},"Secret that enables the self-test page",[788,1028,1029,1034,1039],{},[808,1030,1031],{},[78,1032,1033],{},"enabled",[808,1035,1036],{},[78,1037,1038],{},"True",[808,1040,1041,1044],{},[78,1042,1043],{},"False"," turns protection off entirely",[788,1046,1047,1052,1057],{},[808,1048,1049],{},[78,1050,1051],{},"retries",[808,1053,1054],{},[78,1055,1056],{},"1",[808,1058,1059],{},"Verification attempts in the browser",[788,1061,1062,1067,1072],{},[808,1063,1064],{},[78,1065,1066],{},"delay",[808,1068,1069],{},[78,1070,1071],{},"1000",[808,1073,1074],{},"Milliseconds between attempts",[788,1076,1077,1082,1087],{},[808,1078,1079],{},[78,1080,1081],{},"timeout",[808,1083,1084],{},[78,1085,1086],{},"1.5",[808,1088,1089],{},"Seconds to wait for the cloud before letting the request through",[788,1091,1092,1097,1102],{},[808,1093,1094],{},[78,1095,1096],{},"cache_ttl",[808,1098,1099],{},[78,1100,1101],{},"3600",[808,1103,1104],{},"How long a crawler verdict is cached, in seconds",[788,1106,1107,1112,1117],{},[808,1108,1109],{},[78,1110,1111],{},"cache_max",[808,1113,1114],{},[78,1115,1116],{},"10000",[808,1118,1119],{},"Verdicts held per worker before the oldest is dropped",[788,1121,1122,1127,1131],{},[808,1123,1124],{},[78,1125,1126],{},"key_check_ttl",[808,1128,1129],{},[78,1130,1101],{},[808,1132,1133],{},"How long a confirmed-good API key is trusted, in seconds",[788,1135,1136,1141,1144],{},[808,1137,1138],{},[78,1139,1140],{},"store",[808,1142,1143],{},"in-process",[808,1145,1146],{},"Shared cache, for several workers or hosts",[788,1148,1149,1154,1159],{},[808,1150,1151],{},[78,1152,1153],{},"challenge_status",[808,1155,1156],{},[78,1157,1158],{},"200",[808,1160,1161,1162,1165],{},"Use ",[78,1163,1164],{},"503"," to have search engines treat the verification page as a temporary hold",[788,1167,1168,1173,1178],{},[808,1169,1170],{},[78,1171,1172],{},"cookie_name",[808,1174,1175],{},[78,1176,1177],{},"__bbjwt",[808,1179,1180],{},"Cookie the browser SDK sets",[788,1182,1183,1188,1192],{},[808,1184,1185],{},[78,1186,1187],{},"debug",[808,1189,1190],{},[78,1191,1043],{},[808,1193,1194],{},"Skip the IP check on the cookie — for local development",[15,1196,1197,1200],{},[78,1198,1199],{},"cryptography"," is the only dependency, and it is not optional: every decision is anchored in the Ed25519 signature of the session cookie, and the standard library cannot verify one. Without it the library disables itself rather than skipping verification.",[15,1202,1203],{},"For local development the cleanest switch is to turn it off:",[70,1205,1207],{"className":114,"code":1206,"filename":116,"language":117,"meta":76,"style":76},"BLOCKBOTS = {\"api_key\": ..., \"enabled\": os.environ.get(\"ENV\") == \"production\"}\n",[78,1208,1209],{"__ignoreMap":76},[81,1210,1211],{"class":83,"line":84},[81,1212,1206],{},[15,1214,1215,1216,1219,1220,1223],{},"Your API key is tied to its registered domain, so the cloud rejects requests from ",[78,1217,1218],{},"localhost"," anyway. ",[78,1221,1222],{},"debug=True"," relaxes the cookie's IP check if you do want to test the real flow; never use it in production.",[10,1225,1227],{"id":1226},"next-steps","Next steps",[38,1229,1230,1236,1243,1250],{},[41,1231,1232,1235],{},[28,1233,1234],{"href":30},"JavaScript"," — add the browser SDK alongside this",[41,1237,1238,1242],{},[28,1239,1241],{"href":1240},"\u002Fdocs\u002Fnodejs-quickstart","Node.js"," — the same protection for an Express app",[41,1244,1245,1249],{},[28,1246,1248],{"href":1247},"\u002Fdocs\u002Fgolang-quickstart","Go"," — the same protection for a net\u002Fhttp server",[41,1251,1252,1256],{},[28,1253,1255],{"href":1254},"\u002Fdocs\u002Fphp-quickstart","PHP"," — the same protection for a PHP site",[1258,1259,1260],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":76,"searchDepth":127,"depth":127,"links":1262},[1263,1264,1265,1266,1271,1272,1273,1274,1275,1276,1277,1278,1279],{"id":12,"depth":127,"text":13},{"id":35,"depth":127,"text":36},{"id":67,"depth":127,"text":68},{"id":98,"depth":127,"text":99,"children":1267},[1268,1269,1270],{"id":110,"depth":133,"text":111},{"id":185,"depth":133,"text":186},{"id":238,"depth":133,"text":239},{"id":310,"depth":127,"text":311},{"id":404,"depth":127,"text":405},{"id":458,"depth":127,"text":459},{"id":516,"depth":127,"text":517},{"id":552,"depth":127,"text":553},{"id":644,"depth":127,"text":645},{"id":705,"depth":127,"text":706},{"id":937,"depth":127,"text":938},{"id":1226,"depth":127,"text":1227},"Protect a Django, Flask or FastAPI site from bots with one middleware — one dependency.","md",{},"\u002Fdocs\u002Fpython-quickstart","Server",{"title":5,"description":1280},"docs\u002Fpython-quickstart","_3XePpjp-_8Dat5Aq4YINASEd878-9Ueq4YszwmnK5g",[1289,1293,1295,1296,1299,1300,1301],{"title":1290,"path":1291,"order":84,"section":1292},"Getting Started","\u002Fdocs\u002Fgetting-started","Getting started",{"title":1234,"path":30,"order":127,"section":1294},"Web",{"title":1255,"path":1254,"order":139,"section":1284},{"title":1297,"path":1298,"order":145,"section":1284},"WordPress Plugin","\u002Fdocs\u002Fwordpress-plugin",{"title":1241,"path":1240,"order":158,"section":1284},{"title":5,"path":1283,"order":170,"section":1284},{"title":1248,"path":1247,"order":176,"section":1284},1794295237814]