Challenges

A challenge decision comes with challengeId, challengeUrl, and challengeOptions.
  • Light (score 30-49): email_verify, captcha. The hosted page runs an invisible Turnstile check first and offers a magic link.
  • Strong (50-70): require_card. The hosted page collects a card with Stripe Elements on your Stripe account (nothing is charged). The card fingerprint is re-scored immediately: a card already tied to a consumed trial escalates to deny.
Hosted page copy is neutral ("Quick verification to activate your trial"), WCAG 2.1 AA, mobile-first, and never shows a score or reason.

Headless challenges

Skip the hosted page and run your own UI:
  • GET /api/c/{challengeId} (no auth) returns { status, options, workspaceName }, nothing sensitive.
  • Complete from your backend: POST /api/v1/challenges/{challengeId}/complete {"method": "email_verify" | "captcha" | "sms_verify" | "require_card", "outcome": "passed" | "failed", "cardFingerprint"?: "..."}.
  • require_card with a fingerprint re-scores: a repeat card escalates to deny and the response carries both scores.

Card fingerprints

Card fingerprints are scoped to a Stripe account, so cards must be collected on your account. Two options:
  • Connect (recommended): Settings -> Connect Stripe. Hosted card challenges create the SetupIntent on your account; the fingerprint is harvested and the decision re-scored automatically.
  • Push: keep your own Stripe flow and forward fingerprints from your setup_intent.succeeded / payment_method.attached webhook: POST /api/v1/signals/card {"decisionId": "...", "cardFingerprint": "<payment_method.card.fingerprint>"} If the decision has a pending challenge this completes it (repeat card -> deny). Otherwise the fingerprint is attached for future matching.
Card numbers never reach OneTrial.