Overview
By default, one MFA verification begins a session that authorizes every protected operation until the session expires. Request-scoped verification replaces the window with a challenge binding. Each approval authorizes exactly one request. CDP’s API associates a specific request with a challenge, the user approves that specific request, and resubmission of that initial request consumes the approval.Request-scoped verification is opt-in per project. Projects that do not opt in keep session-based verification with no change.
Project configuration
Enable theRequest verification scope in the CDP Portal.
Resetting the scope back to Session restores the default behavior.
How it works
UnderRequest scope, a protected operation that lacks an approval is rejected with 403 mfa_required. The rejection carries an X-Mfa-Challenge-Id response header that identifies a pending challenge for that exact request.
1
Register an MFA listener
Register a listener before any protected call. Under
Request scope the challenge only exists once the server has rejected a call, so the SDK cannot prompt ahead of time. A protected call with no listener registered fails with the LISTENER_REQUIRED MFA error instead.2
Call the protected operation
Call a protected operation such as
sendEvmTransaction. The request shape is unchanged.3
Receive the challenge
The server rejects the call with
403 mfa_required and an X-Mfa-Challenge-Id header. The SDK holds that challenge and invokes your listener with it as challengeId on the listener context.4
Verify
For TOTP or SMS, call
initiateMfaVerification and then submitMfaVerification with the user’s code. Both accept the challengeId from the listener context, and both fall back to the held challenge when you omit it.Passkey works differently. verifyPasskey runs the full WebAuthn ceremony in one call, and the assertion is cryptographically bound to the approved request. Do not pass a passkey assertion to submitMfaVerification.5
Let the SDK replay the request
A successful verification releases the original call, which the SDK replays unchanged. The CDP API consumes the approval and the call proceeds. Do not retry it yourself.
TOTP and SMS
- React hooks
- Core SDK
useRegisterMfaListener receives the challenge, and the protected action’s own promise resolves once verification completes. There is no mfa_required error to catch and no request to resubmit.mfaMethod: "sms" to both calls. The initiate call sends the code.A wrong code is terminal under Request scope. It consumes the challenge server-side, rejects submitMfaVerification, and rejects the sendEvmTransaction promise that was waiting on it. Handle both rejections, clear your local challengeId, and send the user back to a fresh protected call. Resubmitting against the spent challenge fails with mfa_flow_expired.Passkey
Passkey verification needs a user gesture, so the listener records the challenge and renders a button rather than starting the ceremony itself.- React hooks
- Core SDK
Await
verifyPasskeyAsync, not verifyPasskey. The latter resolves even on a cancelled prompt, which would clear your verification UI while the original call is still waiting.Security properties
One approval, one request
An approval is bound to the digest of the approved request and is consumed on use. There is no window and no reuse:- Approving a
signEvmTransactioncall does not approve asendEvmTransactioncall, even with an identical body. - Approving a transfer of 25 USDC does not approve a transfer of 25,000 USDC.
- Retrying after a consumed approval starts a new challenge and requires a new verification.
A failed code burns the challenge
Each challenge accepts one verification attempt. A wrong TOTP or SMS code deletes the challenge, so the next attempt must start over from the 403. Slow online guessing is further limited by the challenge lifetime.Passkey binds cryptographically; TOTP and SMS bind server-side
The binding strength depends on the method:- Passkey: the WebAuthn challenge is derived from the approved request digest and the challenge ID. That value travels in
clientDataJSON, which the credential signature covers, so the assertion is cryptographically bound to one request. A verifier holding the request and the challenge ID can recompute the value and check the signature against it, without trusting CDP’s record of the association. - TOTP and SMS: the code proves possession of the factor, and the server ties the verification to the request digest. The binding exists only in server state, so verifying it means trusting that state.
Request scope. Require passkey when you need evidence, checkable after the fact, that a user authorized one specific request.
Sessions are revoked when you harden
SettingverificationScope from session to request deletes every outstanding verified session for the project at the moment of the change. A session verified before the change cannot be spent after it.
Troubleshooting
mfa_flow_expired on submit
mfa_flow_expired on submit
The challenge is unknown, expired, or already spent. Challenges expire after 5 minutes and are deleted after a failed code. Restart the flow from the original protected call to receive a fresh
X-Mfa-Challenge-Id.403 mfa_required immediately after a successful verification
403 mfa_required immediately after a successful verification
The retried request does not match the approved request. The digest covers the method, path, query string, and body, so any change, including reordered or reformatted fields that change the semantic body, misses the approval. Retry with the identical request.
initiateMfaVerification rejects with invalid_request
initiateMfaVerification rejects with invalid_request
Under
Request scope, initiate and submit must carry the challenge from the 403 response. Confirm the SDK version supports request-scoped verification and that your listener passes its challengeId through, or leaves it out so the SDK supplies the held challenge.What to read next
Overview
How MFA works and how sessions behave by default
MFA Prompts
Handle verification prompts with components, hooks, or the Core SDK
Passkeys
Enable passkey MFA for cryptographic per-request binding
Customizing Triggers
Which operations require MFA and how to customize prompts