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_cardwith 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.attachedwebhook: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.