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.

Install

pip install secondfactor      # or: uv add secondfactor

Until the first release is on PyPI, install it from this repository:

pip install git+https://github.com/lambda-payments/secondfactor-python

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.

Headless verification

Draw the screens yourself. Your server creates the session and gives its token to your frontend, which calls our session endpoints directly (see the @secondfactor/js client). No proxy is needed, and your API key stays on your server.

created = sf.create_session("+9779841000001", mode="headless")
store_for_this_user(created["sid"])
return {"client_token": created["client_token"]}   # to your frontend

# When your frontend says the user is done:
result = sf.verify_session(stored_sid)             # raises unless VERIFIED

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.2.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.2.0
File Size Uploaded
secondfactor-0.2.0.tar.gz 7.4 kB Details

Built distribution (wheel)

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

Total release size: 16.3 kB

Release files / secondfactor-0.2.0.tar.gz

Download URL secondfactor-0.2.0.tar.gz
Size 7.4 kB
Tags Source
SHA-256 checksum
How to use checksums
6484447858236a17beb63fc545f14d5ce5194a118b6f2e69039ab8148eed0fe7
BLAKE2b-256 checksum
How to use checksums
75b96009770a67c80e700f063be9c19732316e911f8fdc63f85e1aee62c5b8cf
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 8, 2026.

Transparency log

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

Download URL secondfactor-0.2.0-py3-none-any.whl
Size 8.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6712bda0dbbbda30dac613e20b0011fb89fd565cda6a4c73a50a9fc9226206de
BLAKE2b-256 checksum
How to use checksums
1a59c7c98718b4f2a6f48244084f87c82c37fca822e8158fef280f8823c74a45
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 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

0.2.1

2 release files

This release

0.2.0 This release

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