Edge evidence: Cloudflare and Fingerprint

View as Markdown

Your edge and your fraud tools already reach their own conclusions about each request. You can pass those conclusions to Telltale when you verify a protected action. Telltale keeps each one as a separate, labelled source with a strength, records it with the decision, and returns it to your backend. It never mixes them into its own browser evidence, and none of them can make a session human.

Two sources are accepted:

Edge evidence: Cloudflare and Fingerprint
SourceLabel in TelltaleWhat you send
Cloudflare Bot ManagementCloudflare (customer's edge)Bot score, verified bot and its category, signed agent (Web Bot Auth checked by Cloudflare), JA4, detection IDs, JavaScript detection, static resource
Fingerprint Smart SignalsFingerprint (customer's account)Bot detection, tampering and anomaly score, virtual machine, developer tools, emulator, VPN, proxy, private window

Who may send it

Only your backend or your own edge may send this evidence, and only with the Site's server key on POST /api/v1/verify. The browser SDK, the collector endpoints and the compatibility API's POST /v1/verify do not accept it (/v1/verify answers 400 bad_request to an evidence field).

Telltale cannot check these fields itself: your backend reports them. That is why they can only ever change your own Site's decisions, and why the strongest edge signal is still labelled as something Cloudflare checked, not something Telltale checked. Never let a browser supply them, for example by copying a request header into the body. Keep the server key out of the browser.

Send it with verify

Add an evidence object beside the session token:

JSON
{
  "sessionToken": "…",
  "actionId": "export_data",
  "evidence": {
    "cloudflare": {
      "botScore": 4,
      "verifiedBot": false,
      "signedAgent": false,
      "ja4": "t13d1516h2_8daaf6152771_02713d6af862",
      "detectionIds": [33554817]
    },
    "fingerprint": { "bot": "bad", "botType": "headlessChrome", "virtualMachine": true }
  }
}

Every field is optional, but a source you name must carry at least one field. Unknown fields are refused, so a typo is a 400 that names it, not silently dropped evidence.

Cloudflare fields, with the Workers property they come from:

Send it with verify
FieldFromAccepted values
botScorerequest.cf.botManagement.score0 to 99 (1 automated, 2 to 29 likely automated, 30 to 99 likely human, 0 not scored)
verifiedBotrequest.cf.botManagement.verifiedBottrue or false
verifiedBotCategoryrequest.cf.verifiedBotCategoryCloudflare's category name, up to 64 characters
signedAgentrequest.cf.botManagement.signedAgenttrue or false
signatureAgentthe origin in the Signature-Agent headeran https origin, sent only when signedAgent is true
ja4request.cf.botManagement.ja4a JA4 fingerprint
detectionIdsrequest.cf.botManagement.detectionIdsup to 32 non-negative integers
jsDetectionPassedrequest.cf.botManagement.jsDetection.passedtrue or false
staticResourcerequest.cf.botManagement.staticResourcetrue or false

Fingerprint fields come from the event you read with your Fingerprint secret key on your server: bot (not_detected, good or bad; notDetected is accepted too), botType, tampering, anomalyScore (0 to 1), virtualMachine, developerTools, emulator, vpn, proxy and incognito. Do not send visitorId, linkedId, requestId or IP data: Telltale does not need a device identifier for a verdict, and refuses them.

What comes back

The verify response gains edgeEvidence, which is also stored on the decision:

JSON
{
  "edgeEvidence": {
    "sources": [
      {
        "source": "cloudflare",
        "label": "Cloudflare (customer's edge)",
        "reportedBy": "customer-server",
        "fields": { "botScore": 4, "ja4": "t13d1516h2_8daaf6152771_02713d6af862" },
        "signals": [
          { "signal": "botScore", "value": 4, "strength": "vendor-heuristic", "mayLeadTo": "review", "note": "Cloudflare scored the request as likely automated (4, below 30)." },
          { "signal": "ja4", "value": "t13d…", "strength": "context", "mayLeadTo": "nothing", "note": "TLS client fingerprint. …" }
        ],
        "mayLeadTo": "review"
      }
    ],
    "mayLeadTo": "review",
    "appliedToDecision": false,
    "note": "Reported by your own backend with your server key; Telltale cannot check it. …"
  }
}

How strong each signal is

How strong each signal is
SignalStrengthMost it may lead to
Cloudflare signed agent (signedAgent: true)edge-verified-signature: Cloudflare checked a Web Bot Auth signature from an agent it approveddeny
Cloudflare verified botedge-verified-bot: a known bot Cloudflare verified by address or reverse DNSreview
Cloudflare bot score 1 to 29vendor-heuristicreview
Fingerprint bot badvendor-heuristicreview
Fingerprint bot goodedge-verified-botreview
Cloudflare bot score 30 to 99, or 0contextnothing
Fingerprint bot not_detectedcontextnothing
JA4, detection IDs, JavaScript detection, static resourcecontextnothing
Tampering, virtual machine, developer tools, emulator, VPN, proxy, private windowcontextnothing

mayLeadTo is a ceiling, not an instruction. Edge evidence on its own may lead to deny only when Cloudflare reports a verified signed agent. Every other edge signal may lead to review (a step-up such as a passkey or MFA) at most, because a bot score or a bot-detection result is the provider's model, not proof, and assistive technology and privacy tools trip them too. Context signals never decide anything.

Nothing here is evidence of a person. A high Cloudflare score or Fingerprint's "not detected" is recorded as context only: agents that drive real browsers usually score as human. Unknown stays Unknown.

appliedToDecision says whether the evidence changed the decision. Edge evidence only tightens: it raises Allow to Hold for review, and to Deny only when mayLeadTo is deny (a Cloudflare-verified signed agent), never past the action's AI agent rule. It never lowers a decision and never changes the verdict or the attribution, so it never makes Unknown human, and it is never chargeable on its own. When it raised the decision, appliedToDecision is true and raisedFrom is the decision your rule gave; a hold it caused offers step-up with reason edge_evidence (Server API). The evidence is also kept beside the decision for your own rules and for audit. A Cloudflare signed-agent result also never becomes a Telltale-verified signature or names an agent. To have a signature checked by Telltale itself, forward the request's Web Bot Auth headers in request (the Worker below does this); Telltale then checks it against its reviewed list of signing operators.

The Cloudflare Worker

adapters/cloudflare-worker/worker.js in the Telltale release is a dependency-free Worker that sits in front of your app. On the routes you list it:

  1. reads the session token from the x-telltale-session-token header, or from the SDK's telltaleSessionToken field on a POST form (from a copy of the body; nothing else in the body is read or sent);
  2. calls POST /api/v1/verify with your server key, the Cloudflare fields above as evidence.cloudflare (only values Telltale accepts), and the request's Web Bot Auth headers as request so Telltale checks the signature itself;
  3. passes the request to your origin with Telltale-Decision (allow, review, deny, no-session or unavailable), Telltale-Decision-Id and Telltale-Verdict headers.

With TELLTALE_ENFORCE=on it also applies the decision: allow passes, review answers 403 {"error":"step_up_required","decisionId":…} or redirects to your TELLTALE_STEP_UP_URL, and deny answers 403 {"error":"forbidden","decisionId":…}. The client never sees a verdict, reasons or a score.

Configure it with wrangler variables and one secret:

The Cloudflare Worker
VariableDefaultMeaning
TELLTALE_ENDPOINTrequiredYour Telltale server's https origin
TELLTALE_SERVER_KEYrequired, a secretThe Site's server key: wrangler secret put TELLTALE_SERVER_KEY
TELLTALE_ROUTESrequiredJSON list, e.g. [{"method":"POST","path":"/api/export","actionId":"export_data"}]. A path ending in /* is a prefix; without a method every method except GET, HEAD and OPTIONS is protected
TELLTALE_ENFORCEoffon applies the decision; off only adds the headers
TELLTALE_STEP_UP_URLnoneWhere review sends people, with telltale_decision and return added
TELLTALE_MISSING_TOKENreviewWhen enforcing and there is no valid session token: review, deny or pass
TELLTALE_ON_ERRORpassWhen Telltale cannot be reached, answers late, or refuses the server key: pass (your origin decides, header unavailable) or block (503)
TELLTALE_TIMEOUT_MS3000Deadline for the verify call (100 to 10,000)
TELLTALE_TOKEN_HEADERx-telltale-session-tokenThe header your pages put the session token in

The Site's website addresses (Settings › Channels › Website addresses) must include the hostname the Worker serves, because the forwarded request URL is checked against them.

Trust boundary

Fingerprint from the Worker

createWorker(config, { fingerprint }) takes an optional async function that returns the Smart Signals for a request (for example after reading the event with your Fingerprint secret key, using a request ID the page sent). Whatever it returns is sent as evidence.fingerprint; if it throws, the call goes ahead without it. Never return a visitorId.