/trace call returns a verdict. There are exactly three. Your code (and the Cloudflare Worker) switches on this value to enforce the outcome.
The three verdicts
allow is the default. It means every rule in your policies evaluated the signals and nothing triggered. There is no block value. The correct term is deny.
allow
No rule matched. The user passed every condition in every policy. Proceed normally: issue a session, complete the transaction, send the OTP.challenge
A rule matched and its action ischallenge. Add friction, but don’t hard-block. Use this for situations where you want the user to prove they’re human before you let them continue: a new device on a known account, a VPN login, a slightly suspicious email domain.
challenge means “make them work for it.” The specific mechanism is up to you: MFA, CAPTCHA, email OTP, SMS verification.
Handling challenge in your app
The canonical pattern is arequires_challenge flag in your response body that your frontend interprets:
TypeScript (Node)
requires_challenge and redirects to your MFA or CAPTCHA flow. After the user completes step-up, call /trace again with the same signals plus status: "succeeded" if they passed. This second trace records the real outcome and re-evaluates against your policies.
Challenge types
MFA (authenticator app, hardware key)
MFA (authenticator app, hardware key)
Best for account-takeover signals (new device, impossible travel, new country). The user already has an authenticator enrolled. Redirect to your existing MFA flow.
CAPTCHA
CAPTCHA
Best for bot/automation signals such as
device.is_headless, device.automation_detected, and high velocity. A CAPTCHA distinguishes a real human from a script. Use any provider (hCaptcha, reCAPTCHA, Turnstile).Email or SMS OTP
Email or SMS OTP
Best for new-device or low-trust scenarios where the user doesn’t have MFA enrolled. Sends a one-time code to the address or phone number on the account.
deny
A rule matched and its action isdeny. Hard block: don’t issue a session, don’t process the transaction, don’t send the OTP. Return an error.
Edge behavior: what the Worker does
The Cloudflare Worker enforces verdicts differently from your backend, because it sits in front of your app and can’t render your MFA flow.
The critical difference: at the edge, only
deny blocks. challenge is always forwarded to your origin. The Worker cannot render your MFA page or CAPTCHA (that lives in your app), so challenge enforcement is always the in-app call’s job.
This is intentional and correct. Think of it as division of responsibilities:
- The edge handles
deny(bots, Tor, known-bad IPs, disposable emails): hard blocks before your app sees a single byte. - Your app handles
challenge(step-up friction for real users who look slightly suspicious).
Configuring the deny response at the edge
In Enforce mode, you configure what the Worker returns ondeny. The default is a 403 with an empty body. You can override it per route rule:
When a policy is triggered
Whenverdict is challenge or deny, the response includes a policy object telling you exactly which rule fired:
policy.rule_name for logging. Avoid showing it to the user, as it reveals your detection logic.
When no rule matches
Whenverdict is allow, the policy object is absent from the response. signals_complete: true means all enrichment resolved. If signals_complete: false, some async enrichment was still pending and the verdict was made on partial data. For high-stakes decisions, retry after ~500 ms to get the warm-cache result.
Full verdict handling pattern
TypeScript (Node)
Python
Where to go next
Server-side integration
The full /trace request and response contract: every field, getting the client IP, and handling signals_complete.
Rules and policies
Define the conditions that trigger challenge or deny: fields, operators, logic, and ordering.
