Metadata-Version: 2.5
Name: bot403
Version: 0.1.0
Summary: Bot403 for Python servers: check the proof-of-work token a protected HTML form sends. Standard library only.
Project-URL: Homepage, https://bot403.com
Project-URL: Documentation, https://bot403.com/docs/verify
Project-URL: Source, https://github.com/zaharystoyanovlabs/bot403-python
Author-email: Zahary Stoyanov <hello@bot403.com>
License-Expression: MIT
License-File: LICENSE
Keywords: altcha,bot-protection,bot403,captcha,forms,proof-of-work,spam
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# bot403 for Python

Bot403 is an invisible bot shield for HTML forms: one script tag makes the browser solve a small proof of work, and your server checks it with one call. This package is that call for Python. It uses only the standard library. The free plan covers 5,000 checks a month.

## Install

```sh
pip install bot403
```

## Set up

1. Create a site at [bot403.com](https://bot403.com). You get a public key (`pk_live_…`) and a secret key (`sk_live_…`).
2. Add the script tag to the page with the form, before `</body>`:

   ```html
   <script src="https://bot403.com/v1/bot403.js" data-bot403-key="pk_live_…" defer></script>
   ```

3. Put the secret key in the environment of your server, never in a page or in Git:

   ```sh
   BOT403_SECRET_KEY=sk_live_…
   ```

4. Check the form on submit (Flask):

   ```python
   from bot403 import Bot403, Bot403Error
   from flask import Flask, abort, request

   app = Flask(__name__)
   bot403 = Bot403()  # reads BOT403_SECRET_KEY

   @app.post("/contact")
   def contact():
       try:
           # Post the whole form: Bot403 reads the _bot403 field and the honeypot field.
           result = bot403.verify_form(request.form)
       except Bot403Error as e:
           if e.status != 0:
               raise
           result = None  # Bot403 did not answer: let the form through
       # Refuse when ok is false, except for reason "quota".
       if result and not result.ok and result.reason != "quota":
           abort(400)
       return "Thanks."
   ```

   Django: `bot403.verify_form(request.POST)`. With `fetch` and JSON, send the token alone: `bot403.verify(token)`.

## Answers

`verify` and `verify_form` return a `Result`. A token that fails is not an exception.

| `result.ok` / `result.reason` | Meaning | What to do |
|---|---|---|
| `True` | A browser solved the puzzle for this site. The token was not used before. | Accept. |
| `invalid` | The token is missing, damaged, or wrong. | Reject. |
| `expired` | The challenge is older than the site's expiry (default 10 minutes). | Reject. Ask the visitor to send again. |
| `replay` | The token was used before. | Reject. |
| `wrong_site` | The token belongs to a different site. | Reject. Check the keys. |
| `honeypot` | The hidden honeypot field had a value. | Reject, or show a fake success. |
| `quota` (HTTP 402) | Only in a capped month, after two calendar months over the plan in a row. The token is not used up. | Your choice. The example lets the form through. |

`Bot403Error` means the request failed as a whole. `e.status` is the HTTP status (401 wrong key, 429 too many verify calls a minute, 413 body over 64 KB), or 0 when the network failed (`e.code == "network"`). Decide what your server does when Bot403 does not answer. The example lets the form through.

Options: `Bot403(secret_key=None, base_url="https://bot403.com", timeout=10)`.

All answers and reasons: [bot403.com/docs/verify](https://bot403.com/docs/verify). API reference: [bot403.com/docs/api](https://bot403.com/docs/api).

## Test

The tests run against a local fake of the API. They make no network calls.

```sh
uv run --with pytest --with . python -m pytest -q tests
```

## License

MIT. See [LICENSE](LICENSE).
