Skip to content

Python SDK ​

The secondfactor package calls the API from your Python server. It is one module with no dependencies beyond the standard library, and needs Python 3.9 or later.

It runs on your server and holds your API key, which must never reach a browser or a mobile app. For the other side of a hosted session, the browser SDK opens the page in a browser and reads the result, and the iOS, Android, Flutter and React Native SDKs do the same in a mobile app.

bash
pip install secondfactor      # or: uv add secondfactor

You need an API key from the dashboard's API keys page. For the hosted page you also need a return origin, such as https://app.example.com, listed under Settings → Hosted verification.

Choosing a flow ​

FlowWho draws the screensMethods
Hosted sessionWe do. You redirect the user to our page and get them back on your return URL.create_session, verify_session
Direct sendsYou do, and every code goes through your server.send, check

There is no headless flow in which your frontend calls our API. If you want your own screens, use direct sends from your server.

Sessions are the recommended way in. They give you one call at the end, verify_session, that says whether the number was proven, and it succeeds only once, so a replayed request cannot sign anyone in.

SecondFactor ​

The client for one organization's API key. Create one and reuse it. Every method is synchronous and returns the API's JSON answer as a dict.

python
import os

from secondfactor import SecondFactor, SecondFactorError

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

SecondFactor(api_key, service_sid=None, base_url=..., timeout=10) ​

ArgumentTypeDefaultDescription
api_keystr—Required. A key from the dashboard.
service_sidstrlooked upYour Service SID (VA…). Every organization has exactly one, so when it is omitted the client looks it up once, on first use.
base_urlstr"https://api.secondfactor.ai"Must be https://. Plain http:// is accepted only for localhost, 127.0.0.1 and ::1, for a local stub.
timeoutfloat10Seconds per request.

Raises ValueError when api_key is missing or base_url is not safe to send the key to.

create_session(to, return_url, client_reference_id=None, template_sid=None) ​

Starts verifying a phone number and returns the session object. Creating a session is free; each code it sends is charged.

ArgumentTypeDescription
tostrRequired. The phone number in E.164, for example "+9779841000001".
return_urlstrRequired. Where we send the user back. Its origin must be listed under Settings → Hosted verification.
client_reference_idstrYour own ID for this sign-in, such as your user's ID. It comes back from verify_session.
template_sidstrThe message template to send the code with. Your default when omitted.

The session returns url, the hosted page: redirect the user there. It is returned only this once.

Store the returned sid server-side against the user's pending sign-in, for example in their session. It is what you confirm later.

python
created = sf.create_session(
    user.phone,
    return_url="https://app.example.com/verified",
    client_reference_id=str(user.id),
)
session["sf_sid"] = created["sid"]
return redirect(created["url"], code=303)

verify_session(stored_sid, return_token) ​

Accepts the outcome of a session, exactly once, and returns the number that was proven.

ArgumentTypeDescription
stored_sidstrThe sid you stored when you created the session. Never the sf_session_id from the return URL: anyone can edit a URL.
return_tokenstr or NoneThe sf_return_token query parameter from the return URL. When it is missing, pass None and the call raises missing_return_token.

Returns a dict:

KeyTypeDescription
phonestrThe number that was proven, in E.164. Use this, not anything the browser sent.
client_reference_idstr or NoneThe client_reference_id you passed to create_session.
sessiondictThe full session object.

Raises SecondFactorError unless the session is VERIFIED and has not been confirmed before. The usual codes are missing_return_token, invalid_return_token, not_verified, already_confirmed and not_found.

python
# 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"])

retrieve_session(sid) ​

Returns the session object as it stands now. Reading a session proves nothing about who verified it, and it does not use up the session; use verify_session to act on the outcome.

send(to, code=None, template_sid=None, idempotency_key=None) ​

Sends a code directly, without a session, and returns the verification object. Keep its sid server-side for the check. Each call is charged.

ArgumentTypeDescription
tostrRequired. The phone number in E.164.
idempotency_keystrNew for each user action. A request retried with the same key is charged at most once.
template_sidstrThe message template to send the code with.
codestrOnly if you generate codes yourself. You then check them yourself too.

There is no resend method. Sending to the same number again is a new verification, with a new sid, and is charged again.

python
import uuid

verification = sf.send("+9779841000001", idempotency_key=str(uuid.uuid4()))
session["verification_sid"] = verification["sid"]

check(verification_sid, code) ​

Checks the code your user typed. Returns the verification object with verified added, which is True only when the code was right. Checking is free.

A wrong code is not an error. It returns verified as False with attempts_remaining. A verification that can never succeed raises SecondFactorError with code expired, locked or already_verified; only a new send helps.

The code is converted to a string and stripped before it is sent.

python
result = sf.check(session["verification_sid"], request.form["code"])
if result["verified"]:
    mark_phone_verified(user, result["to"])
else:
    show_wrong_code(result["attempts_remaining"])

service_sid() ​

Returns your Service SID (VA…): the one passed to the constructor, or the one looked up from the API on first use.

start(to, code=None, template_sid=None) ​

Deprecated: use send. It still works, and emits a DeprecationWarning.

SecondFactorError ​

Every refused request raises a SecondFactorError, a subclass of Exception.

AttributeTypeDescription
codestr or NoneThe stable string to branch on.
statusint or NoneThe HTTP status of the refusal, including a refused 3xx redirect. None when no answer arrived.
str(error)strA message for your logs. It is safe to log; never show it to your users.
codeRaised byMeaning
no_return_origin, origin_not_allowedcreate_sessionAdd the return URL's origin under Settings → Hosted verification.
missing_return_token, invalid_return_tokenverify_sessionThe user did not finish verifying in this browser. Start again.
not_verifiedverify_sessionThe session is open, canceled, failed or expired.
already_confirmedverify_sessionThe session was confirmed before. Treat it as a replay.
expired, locked, already_verifiedcheckThis verification can never succeed. Send a new code.
unroutablesend, create_sessionNot a valid E.164 number.
rate_limited, burstsendToo many codes to this number. Try later.
insufficient_fundssendTop up your balance.
network_erroranyThe API could not be reached, or the request timed out.

Other codes come straight from the API; see Errors. New codes may appear, so keep a default branch.

The API never redirects, so the client refuses a redirect rather than following it, which could send your API key to another host.

The session object ​

create_session, retrieve_session and verify_session (as "session") return this object as a dict.

KeyTypeDescription
sidstrThe session SID. Store it server-side.
statusstrOPEN, VERIFIED, CANCELED, FAILED or EXPIRED.
failure_reasonstr or NoneWhy a FAILED session failed: max_sends, max_attempts, or send_refused:<code> when we could not send to the number.
tostrThe phone number being verified, in E.164.
client_reference_idstr or NoneYour own ID, as passed to create_session.
verification_sidstr or NoneThe newest code the session sent (VE…). Get OTP status answers price and delivery questions about it.
sendsintHow many codes the session has sent.
return_urlstrWhere the user is sent back.
confirmedboolWhether verify_session has accepted this session.
expires_atstrWhen the session stops working.
date_createdstrWhen the session was created.
date_completedstr or NoneWhen the session reached its final status.
urlstrOn creation only: the hosted page to redirect the user to.

A runnable Flask app with the hosted flow is in the package repository's examples/flask_app.py.