Skip to main content

secondfactor.ai for Python

Verify phone numbers from your Python server with 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, which opens the page and reads the result when the user returns; it never calls the API itself.

Install

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.

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.

# 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.

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.

Example

examples/flask_app.py is a runnable Flask app with both flows.

Development

The project is managed with uv. The library lives in src/secondfactor/ and its tests in tests/.

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

Metadata

Release files for secondfactor 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for secondfactor 0.3.0
File Size Uploaded
secondfactor-0.3.0.tar.gz 7.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for secondfactor 0.3.0
File Interpreter ABI Platform
secondfactor-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 15.9 kB

Release files / secondfactor-0.3.0.tar.gz

Download URL secondfactor-0.3.0.tar.gz
Size 7.2 kB
Tags Source
SHA-256 checksum
How to use checksums
b2d48dfd46caed2f1ee642d03437031af44c679d935391ec0e524ca542aff547
BLAKE2b-256 checksum
How to use checksums
e3ad8d17568128a2f98e0b7ddd3b8c9673095fead66d6c872e6fb61ca5070ab2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / secondfactor-0.3.0-py3-none-any.whl

Download URL secondfactor-0.3.0-py3-none-any.whl
Size 8.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9fd8c033b372e1aab9fbbd4658f6a9a6e77efdaa4be15149c1589212f4e4f37b
BLAKE2b-256 checksum
How to use checksums
0f260face3de1652b04df10971d7751ac74ed109ffd7c1387b7e1efdeded8b1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page