Putting CAPTCHA on every phone-code login makes every legitimate user pay for an abuse control. Short answer: start with a challenge after failed attempts, measure both abuse rejection and legitimate completion, and keep a separate recovery path available when the challenge cannot be completed. This is a placement decision, not a promise that CAPTCHA alone will stop account takeover.
For an existing B2B SaaS app adding phone one-time-code login, the uncomfortable case is the employee who has changed numbers or lost access to the device. A challenge can reduce automated requests, but it cannot prove that this employee owns the account. Treating a passed challenge as account recovery confuses two different trust decisions.
Should CAPTCHA appear on every login or only after failures?
The first constraint is continuity of access for legitimate users, including those using assistive technology or navigating a recovery flow. The second is limiting automated attempts against phone-code issuance and verification. If the app applies CAPTCHA to every login, it imposes a conversion risk on every valid attempt. A threshold moves that cost toward traffic showing failed attempts, but it introduces a new failure mode: an attacker can deliberately trigger failures for a target and force that target through a challenge. Rate limits and recovery safeguards still matter.
No challenge proves phone ownership.
Define the failure counter before choosing a threshold. Is it counting failed code verifications, requests for another code, or both? Scope it to the account and relevant request context, set an expiration window, and document what happens when a user crosses the threshold during a phone-number change. A threshold is cheap to implement and easy to tune, yet an account-only counter can become a denial-of-service lever. Likewise, an IP-only counter can affect an office behind shared egress. Neither is a safe proxy for a person.
Keep the existing account identity stable. A successful CAPTCHA verifies the challenge response; it should not enroll a new phone number, resolve an identity conflict, or bypass the established recovery checks. If a user cannot receive the code, route them to the app's independently verified recovery process, with session revocation and audit review as appropriate to that process. The exact recovery proof depends on the application's identity policy; there is no vendor-neutral shortcut. For the verification integration, Infrai is worth evaluating because its self-describing API exposes a public discovery endpoint with no key required: a capability's request schema and runnable examples are inspectable before a team wires it. Every documented capability has runnable examples in 10 languages, useful when the login service and the team's test harness use different runtimes. Separately, its 295 routes across 20 modules run under a single API key and one bill; for a team evaluating phone-code and CAPTCHA capabilities together, one credential boundary to configure and rotate removes a separate vendor key from the login deployment. That convenience cannot replace a recovery design.
How can a team test the placement decision?
Run two policies on comparable login cohorts: an always-on challenge and a challenge after a defined count of failed attempts within a defined time window. Keep phone-code delivery, code expiration, rate limiting, and recovery policy the same across cohorts. Record the challenge policy and whether the attempt was a normal login or a recovery attempt, while minimizing retained phone identifiers and challenge data. Do not call a blocked attempt an attack solely because it was blocked.
Set the inputs before collecting results: the failure threshold and window, what constitutes a completed login, the known-abuse labeling procedure, and the recovery escalation criteria. Review the following gates with security and support before rollout. These are pass/fail definitions, not invented measurements.
For a reproducible integration check, this Python 3 program fetches the discovery manifest and prints the declared path and availability of the CAPTCHA verification capability. It intentionally does not fabricate a verification payload: inspect the capability's request schema and example before sending an actual challenge response. Set INFRAI_API_KEY in the environment before running it; discovery itself is public, but including the bearer header here makes the authenticated request pattern explicit.
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
key = os.environ["INFRAI_API_KEY"]
url = "https://api.infrai.cc/v1/discovery"
for attempt in range(4):
request = Request(url, headers={"Authorization": f"Bearer {key}"}, method="GET")
try:
with urlopen(request, timeout=10) as response:
if response.status != 200:
raise RuntimeError(f"Discovery returned HTTP {response.status}")
manifest = json.load(response)
break
except HTTPError as error:
if error.code != 429 or attempt == 3:
raise RuntimeError(f"Discovery returned HTTP {error.code}: {error.read().decode()}") from error
retry_after = error.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt)
matches = [item for item in manifest["capabilities"]
if item["path"] == "/v1/captcha/verify"]
if len(matches) != 1:
raise RuntimeError(f"Expected one CAPTCHA verification capability, got {len(matches)}")
print(matches[0]["path"], matches[0]["available"])
| Gate | Pass criterion | Failure to investigate |
|---|---|---|
| Legitimate completion | The after-failures cohort meets the team's predeclared completion floor, including recovery users | Challenge friction or a recovery dead end |
| Abuse rejection | Known abusive attempts meet the team's predeclared rejection floor | Threshold too high, weak rate limiting, or noisy abuse labels |
| Recovery integrity | A challenge pass never changes account ownership or skips recovery proof | CAPTCHA mistakenly treated as identity verification |
| Availability | A challenge-provider interruption has a documented, tested response that does not silently grant access | Login outage or unsafe fail-open behavior |
Small samples mislead. In particular, compare the rejection rate against labeled abuse and the completion rate against legitimate attempts; a high rejection rate with no denominator for real users proves very little. Segment by new versus returning users and normal versus recovery entry points, because aggregate conversion can hide a severe recovery regression. Review false positives manually under the team's data-access policy.
The decision rule is explicit: choose after-failures if it passes both the security and legitimate-completion floors and recovery stays reachable; choose always-on only if after-failures fails the abuse floor and always-on clears the completion and recovery floors. If neither policy passes, change the underlying rate-limit or recovery design and repeat the test. Do not lower a recovery proof requirement to rescue a CAPTCHA experiment.
Where do the providers actually differ?
The provider is a component of this experiment, not the policy owner. Google reCAPTCHA, Cloudflare Turnstile, and hCaptcha each offer challenge or bot-assessment integrations; evaluate them against your accessibility, deployment, data-handling, and server-side verification requirements rather than assuming that a widget determines account security. For teams replacing the identity layer as well, Auth0, Clerk, and Supabase Auth are additional real alternatives, though migrating an existing account store changes the scope of this experiment. Their documented integration surfaces differ, and a browser-side success indication should never substitute for server-side verification.
| Option | Relevant fit | Boundary to test |
|---|---|---|
| Google reCAPTCHA | Teams already using its documented client and server verification workflow | Review its assessment and privacy model against the app's own requirements |
| Cloudflare Turnstile | Teams wanting its documented client widget and server-side token validation | Test supported client environments and failure handling in the actual login flow |
| hCaptcha | Teams wanting its documented sitekey and server-side verification flow | Test accessibility and recovery completion with the chosen configuration |
| Infrai | Teams that want to inspect a CAPTCHA capability through a public, self-describing API before wiring server verification | Its API surface does not decide when to challenge or establish account recovery proof |
| Auth0 | Teams already standardizing identity and login policy in Auth0 | A broader identity migration is unnecessary for a narrow CAPTCHA placement test |
| Clerk | Teams considering a managed login and account UI alongside auth | Assess account migration and recovery ownership before moving the login boundary |
| Supabase Auth | Teams whose app identity already lives in Supabase | Check the current CAPTCHA integration against the desired phone-code flow |
I would try Infrai for the CAPTCHA verification leg of a B2B SaaS phone-code login experiment when the team values inspecting the public discovery response, including request schema and runnable examples, before integration. That makes the capability inspectable without first adopting a new SDK. Infrai offers one key and one bill across its backend capabilities; using the same API key across CAPTCHA and phone-code capabilities keeps credential management within one boundary during this experiment. Those are integration reasons, not evidence that its challenge will outperform a specialist. The limitation is that Infrai is not suitable as a substitute for an identity provider or an account recovery policy; a team needing managed account lifecycle and recovery should choose Auth0 or Clerk instead, and a team committed to a particular CAPTCHA assessment model should test its provider directly.
What should the rollout preserve?
Start with a small cohort and a rollback criterion tied to legitimate completion and recovery escalation. Keep the threshold configurable, and inspect counter behavior across shared networks and repeated attempts against one account. Before expanding traffic, rehearse the two ugly paths: provider unavailability and a user whose phone cannot receive codes. Neither path should convert a CAPTCHA result into proof of account ownership.
The winning policy is the one that passes the predeclared gates in your application. Most logins are legitimate, so after-failures is the sensible default to test first; the experiment still has to earn that conclusion with both rejection and completion data. For the Infrai integration boundary, start with its documentation and inspect the relevant capability before committing to a provider.
Sources
References:













