Metadata-Version: 2.4
Name: secondfactor
Version: 0.3.0
Summary: secondfactor.ai for Python servers: hosted and headless phone verification, and OTP send and check. No dependencies.
Keywords: otp,2fa,phone-verification,sms,whatsapp,secondfactor
License-Expression: MIT
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Project-URL: Homepage, https://secondfactor.ai
Project-URL: Source, https://github.com/secondfactor/secondfactor-python
Project-URL: Changelog, https://github.com/secondfactor/secondfactor-python/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# secondfactor.ai for Python

Verify phone numbers from your Python server with
[secondfactor.ai](https://secondfactor.ai). One file, standard library only,
Python 3.9 or later.

This library runs on **your server**. It holds your API key, which must never
reach a browser or a mobile app. There are two ways to verify a number with
it: send your user to our hosted verification page, or send and check codes
directly and draw your own screens. For the hosted page, the browser or React
Native side can use
[`@secondfactor/js`](https://github.com/secondfactor/secondfactor-js), which
opens the page and reads the result when the user returns; it never calls the
API itself.

## Install

```bash
pip install secondfactor      # or: uv add secondfactor
```

## Set up

From the secondfactor.ai dashboard you need an **API key** (API keys page). For
the hosted page you also need a **return origin**, such as
`https://app.example.com`, under **Settings → Hosted verification**.

```python
import os
from secondfactor import SecondFactor, SecondFactorError

sf = SecondFactor(api_key=os.environ["SECONDFACTOR_API_KEY"])
```

## Hosted verification

Redirect the user to our page; we send the code, check it, and send them back.

```python
# 1. When the user reaches the step that needs a verified number.
created = sf.create_session(
    "+9779841000001",
    return_url="https://app.example.com/verified",
    client_reference_id=str(user.id),
)
session["sf_sid"] = created["sid"]          # keep it for this browser
return redirect(created["url"], code=303)

# 2. On https://app.example.com/verified?sf_session_id=…&sf_return_token=…
try:
    result = sf.verify_session(session.pop("sf_sid"), request.args.get("sf_return_token"))
except SecondFactorError as error:
    return start_again(reason=error.code)
mark_phone_verified(user, result["phone"])
```

**Always confirm the `sid` you stored**, never the `sf_session_id` from the URL:
anyone can edit a URL. `verify_session` succeeds once. A second call raises
`already_confirmed`, so a replayed return URL cannot sign anyone in.

## Direct sends

Send a code and check what the user typed in your own form.

```python
verification = sf.send("+9779841000001", idempotency_key=str(uuid.uuid4()))
result = sf.check(verification["sid"], user_input)
if result["verified"]:
    ...
else:
    show_wrong_code(result["attempts_remaining"])
```

Sending again to the same number is a new verification with a new `sid` and is
charged again. Pass a fresh `idempotency_key` per user action so a retried
request is never charged twice.

## Errors

Every refused request raises `SecondFactorError` with:

- `code`: the string to branch on;
- `status`: the HTTP status, or `None` if no answer arrived;
- a message for your logs. Never show the message to your users.

| `code` | Raised by | Meaning |
|---|---|---|
| `no_return_origin`, `origin_not_allowed` | `create_session` | Add the return URL's origin under Settings → Hosted verification. |
| `missing_return_token`, `invalid_return_token` | `verify_session` | The user did not finish verifying on this browser. Start again. |
| `not_verified` | `verify_session` | The session is open, cancelled, failed or expired. |
| `already_confirmed` | `verify_session` | Confirmed before; treat as a replay. |
| `expired`, `locked`, `already_verified` | `check` | This verification can never succeed. Send a new code. |
| `unroutable` | `send`, `create_session` | Not a valid E.164 number. |
| `rate_limited`, `burst` | `send` | Too many codes to this number. Try later. |
| `insufficient_funds` | `send` | Top up your balance. |
| `network_error` | any | secondfactor.ai could not be reached. |

The full list is in the [API reference](https://secondfactor.ai/docs).

## Example

[`examples/flask_app.py`](examples/flask_app.py) is a runnable Flask app with
both flows.

## Development

The project is managed with [uv](https://docs.astral.sh/uv/). The library
lives in `src/secondfactor/` and its tests in `tests/`.

```bash
uv run python -m unittest discover -s tests -v
uv build          # the sdist and wheel, into dist/
```

The tests run against a local stub of the API and need no network or key.

## Upgrading from the dashboard download (0.1.0)

- `start()` is now `send()`. `start()` still works and warns.
- `check()` adds `verified`, and **raises** for a verification that can never
  succeed (`expired`, `locked`, `already_verified`) instead of returning it.
- `service_sid` is optional.
- Requests are sent as JSON.

## License

MIT
