Skip to main content

Multi-factor authentication

Most carriers challenge a credential submission with a one-time code at least some of the time. Across our customer base this is roughly a third of connection attempts, so treat it as a normal path rather than an edge case — an integration that only handles the happy path will fail for a large minority of your members.

While a validation waits on a code, the carrier is holding an authenticated session open. That session has a limited lifetime. This is the one place in the Connect API where a real person has to act inside a bounded window.

The state machine

After you submit credentials you have a task_id. Poll GET /validate-credentials/{policy_holder_id}/{task_id} and drive off state:

POST credentials


┌─────────┐
│ PENDING │◄──────────────┐
└────┬────┘ │
│ │
no MFA ─────┼───── MFA required │
│ │ │
│ ▼ │
│ WAITING_FOR_METHOD_CHOICE │
│ │ │
│ │ PUT {method} │
│ ▼ │
│ TRIGGERING_TWO_FACTOR_AUTH ┤ (carrier is sending the code)
│ │ │
│ ▼ │
│ WAITING_FOR_TWO_FACTOR_CODE│
│ │ │
│ │ PUT {code} │
│ ▼ │
│ ENTERING_CODE ─────────────┘ (wrong code returns here
│ │ with a message)
│ ▼
│ TWO_FACTOR_AUTH_COMPLETE
│ │
▼ ▼
SUCCESS / FAILURE
StateTerminalMeaning
PENDINGNoWorking. Also reported for the underlying STARTED and RETRY states.
WAITING_FOR_METHOD_CHOICENoThe carrier offers several delivery methods. info.method_list holds them.
TRIGGERING_TWO_FACTOR_AUTHNoWe asked the carrier to send the code.
WAITING_FOR_TWO_FACTOR_CODENoThe code is on its way to the member.
ENTERING_CODENoWe are submitting the code you supplied.
TWO_FACTOR_AUTH_COMPLETENoThe code was accepted. SUCCESS follows shortly.
SUCCESSYesFinished. Read credentials_are_valid.
FAILUREYesCould not complete. Read message.

Choosing a delivery method

When you poll into WAITING_FOR_METHOD_CHOICE:

{
"data": {
"id": "3bb088ed-...",
"state": "WAITING_FOR_METHOD_CHOICE",
"info": { "method_list": ["Text to (***) ***-1234", "Email to d***@example.com"] }
}
}

method_list is carrier-authored text. Show the strings as-is; do not try to normalize them into your own categories, and do not assume SMS is always present or always first.

Send back the exact string the member picked:

tpa -X PUT "$TPA_BASE/validate-credentials/630364/3bb088ed-..." -d '{
"user_email": "member@example.com",
"method": "Text to (***) ***-1234"
}'

Then keep polling. You will move through TRIGGERING_TWO_FACTOR_AUTH into WAITING_FOR_TWO_FACTOR_CODE.

Some carriers offer only one method and skip this state entirely, going straight to WAITING_FOR_TWO_FACTOR_CODE. Handle both.

Submitting the code

tpa -X PUT "$TPA_BASE/validate-credentials/630364/3bb088ed-..." -d '{
"user_email": "member@example.com",
"code": "482915"
}'

Same endpoint, different key. Keep polling afterward.

If the carrier rejects the code — wrong digits, expired, mistyped — the state returns to WAITING_FOR_TWO_FACTOR_CODE with a message explaining why. Surface that message and let the member try again. Do not restart the credential submission; the carrier session is still open and a fresh submit will send a second code and confuse the member.

Polling cadence

SituationInterval
Right after credential submit3 seconds
Waiting on the member to pick a method or type a code5 seconds
Anything past 2 minutes on the same state10 seconds

Stop polling once you reach SUCCESS or FAILURE. There is no reason to poll a terminal task, and no new information will arrive.

Designing the member experience

Your front end has to prompt the member and get the code back to your server. The specific transport is up to you — a polled endpoint on your own API, a websocket, a server-sent stream, or a plain form post all work. What matters:

  • Do not block your UI on the whole flow. The member may need a minute to find their phone. Let them do other things; surface the prompt when it arrives.
  • Show carrier text verbatim. Method labels and rejection messages come from the carrier and are usually more accurate than anything you would write.
  • Let them retry the code without starting over. This is the most common recoverable failure in the whole flow.
  • Handle the member walking away. Decide what your UI does when a validation is left hanging, and make sure the member can start a fresh attempt later.

Server-sent events

There is also an event stream:

GET https://app.tpastream.com/v3/sdk/progress/{task_id}/stream?token={task_token}

It pushes the same state transitions as they happen, authenticated by the task_token returned alongside task_id on the credential submit.

We recommend polling instead for server-side integrations. The stream was built for browsers and carries browser-shaped constraints: the task_token is valid for about ten minutes, and the server closes the stream after roughly the same interval with a timeout event. A member who takes longer than that to find their phone will outlive the stream. The polling endpoint has neither limit and is authenticated the same way as every other call.

If a stream does close on you, the underlying validation is unaffected — it keeps running, and polling will show you where it landed.

Failure modes worth handling

SymptomCauseResponse
FAILURE immediately after submitCredentials rejected outright.Show message, let the member re-enter.
Stuck in PENDING past a few minutesCarrier is slow or degraded.Keep polling, but tell the member it's taking a while.
WAITING_FOR_TWO_FACTOR_CODE with a messageThe previous code was rejected.Show the message, accept another code.
SUCCESS with credentials_are_valid: falseThe carrier authenticated but the account has a problem.Check login_problem on the policy holder.
SUCCESS with pending: trueNo verdict yet; we will keep trying in the background.Treat as provisional success, not failure.