Skip to content

Node.js SDK ​

The secondfactor package calls the API from your Node.js server. It has no dependencies, needs Node 18 or later, and includes type declarations.

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
npm install 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.createSession, verifySession
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, verifySession, 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.

js
const { SecondFactor, SecondFactorError } = require("secondfactor");
// or: import { SecondFactor, SecondFactorError } from "secondfactor";

const sf = new SecondFactor({ apiKey: process.env.SECONDFACTOR_API_KEY });

new SecondFactor(options) ​

OptionTypeDefaultDescription
apiKeystring—Required. A key from the dashboard. It is held in a private field, so it never appears when the client is logged or serialized.
serviceSidstringlooked upYour Service SID (VA…). Every organization has exactly one, so when it is omitted the client looks it up once, on first use.
baseUrlstringhttps://api.secondfactor.aiMust be https://. Plain http:// is accepted only for localhost, 127.0.0.1 and [::1], for a local stub.
timeoutMsnumber10000Per request, including reading the answer.
fetchfunctionglobalThis.fetchA fetch to use instead of the global one, for example to add tracing. It must honour redirect: "manual" and signal.

Throws TypeError when apiKey is missing or is not visible ASCII, or when baseUrl is not safe to send the key to. The message never repeats the key.

createSession(params) ​

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

ParamTypeDefaultDescription
tostring—Required. The phone number in E.164, for example +9779841000001.
returnUrlstring—Required. Where we send the user back. Its origin must be listed under Settings → Hosted verification.
clientReferenceIdstring—Your own ID for this sign-in, such as your user's ID. It comes back from verifySession.
templateSidstringyour defaultThe message template to send the code with.

The session resolves with 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.

js
const session = await sf.createSession({
  to: user.phone,
  returnUrl: "https://app.example.com/verified",
  clientReferenceId: user.id,
});
req.session.sfSid = session.sid;
res.redirect(303, session.url);

verifySession(storedSid, returnToken) ​

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

ParamTypeDescription
storedSidstringThe sid you stored when you created the session. Never the sf_session_id from the return URL: anyone can edit a URL.
returnTokenstringThe sf_return_token query parameter from the return URL.

Resolves with:

FieldTypeDescription
phonestringThe number that was proven, in E.164. Use this, not anything the browser sent.
clientReferenceIdstring or nullThe clientReferenceId you passed to createSession.
sessionobjectThe full session object.

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

js
// On https://app.example.com/verified?sf_session_id=…&sf_return_token=…
const storedSid = req.session.sfSid;
delete req.session.sfSid;
try {
  const { phone } = await sf.verifySession(storedSid, req.query.sf_return_token);
  await markPhoneVerified(user, phone);
} catch (error) {
  if (!(error instanceof SecondFactorError)) throw error;
  return startAgain(error.code);
}

retrieveSession(sid) ​

Resolves with 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 verifySession to act on the outcome.

send(to, options?) ​

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

ParamTypeDescription
tostringRequired. The phone number in E.164.
options.idempotencyKeystringNew for each user action. A request retried with the same key is charged at most once.
options.templateSidstringThe message template to send the code with.
options.codestringOnly 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.

js
const crypto = require("node:crypto");

const verification = await sf.send("+9779841000001", { idempotencyKey: crypto.randomUUID() });
req.session.verificationSid = verification.sid;

check(verificationSid, code) ​

Checks the code your user typed. Resolves with the verification object plus verified, which is true only when the code was right. Checking is free.

A wrong code is not an error. It resolves with verified: false and attempts_remaining. A verification that can never succeed throws SecondFactorError with code expired, locked or already_verified; only a new send helps.

The code is trimmed before it is sent, and may be a string or a number.

js
const result = await sf.check(req.session.verificationSid, req.body.code);
if (result.verified) {
  await markPhoneVerified(user, result.to);
} else {
  showWrongCode(result.attempts_remaining);
}

serviceSid() ​

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

SecondFactorError ​

Every refused request throws a SecondFactorError, a subclass of Error.

PropertyTypeDescription
codestring or nullThe stable string to branch on.
statusnumber or nullThe HTTP status of the refusal, including a refused 3xx redirect. null when no answer arrived or a successful answer could not be trusted.
messagestringFor your logs. It is safe to log; never show it to your users.
codeThrown byMeaning
no_return_origin, origin_not_allowedcreateSessionAdd the return URL's origin under Settings → Hosted verification.
missing_return_token, invalid_return_tokenverifySessionThe user did not finish verifying in this browser. Start again.
not_verifiedverifySessionThe session is open, canceled, failed or expired.
already_confirmedverifySessionThe session was confirmed before. Treat it as a replay.
expired, locked, already_verifiedcheckThis verification can never succeed. Send a new code.
unroutablesend, createSessionNot a valid E.164 number.
rate_limited, burstsendToo many codes to this number. Try later.
insufficient_fundssendTop up your balance.
invalid_responseanyA successful answer was not the JSON the API sends.
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. It throws with status set to the redirect's 3xx status.

The session object ​

createSession, retrieveSession and verifySession (as session) return this object.

FieldTypeDescription
sidstringThe session SID. Store it server-side.
statusstringOPEN, VERIFIED, CANCELED, FAILED or EXPIRED.
failure_reasonstring or nullWhy a FAILED session failed: max_sends, max_attempts, or send_refused:<code> when we could not send to the number.
tostringThe phone number being verified, in E.164.
client_reference_idstring or nullYour own ID, as passed to createSession.
verification_sidstring or nullThe newest code the session sent (VE…). Get OTP status answers price and delivery questions about it.
sendsnumberHow many codes the session has sent.
return_urlstringWhere the user is sent back.
confirmedbooleanWhether verifySession has accepted this session.
expires_atstringWhen the session stops working.
date_createdstringWhen the session was created.
date_completedstring or nullWhen the session reached its final status.
urlstringOn creation only: the hosted page to redirect the user to.

A runnable Express app with the hosted flow is in the package repository's examples/express.js.