Skip to content

Fraud Prevention Data API ​

The Data API is the server-side counterpart to the Web SDK. Use it to pull verdicts, aggregate statistics, and export data for reporting and reconciliation — pulling the numbers your own systems agree on.

Base URL ​

https://verify.captcha.la

Authentication ​

All Data API requests are authenticated with your Fraud Prevention application credentials, sent as headers:

X-App-Key:    YOUR_APP_KEY
X-App-Secret: YOUR_APP_SECRET

WARNING

X-App-Secret is server-side only. Never expose it to browsers, mobile apps, or public repositories. The page SDK only ever uses the public appKey.

Endpoints ​

Fetch a verdict ​

Retrieve the verdict for a single visit (e.g. to reconcile a specific visit). Pass exactly one identifier: verdict_id (from the verdict the page received) or cid (the click id, for click-token traffic).

bash
GET /v1/bot/verdict?verdict_id=VERDICT_ID
GET /v1/bot/verdict?cid=CID_OF_THE_VISIT
X-App-Key: YOUR_APP_KEY
X-App-Secret: YOUR_APP_SECRET

found tells you whether a verdict was found. When it is true, data holds the verdict fields described in the Verdict Reference:

json
{
  "found": true,
  "data": {
    "cid": null,
    "verdict_id": "vd_3f9c0a7e5b1d4c2a9e8f7b6a",
    "is_bot": true,
    "score": 87,
    "level": "high",
    "action": "flag",
    "reasons": ["automation", "network"],
    "consistency": { "ok": false },
    "degraded": false,
    "created_at": "2026-06-25 00:00:00"
  }
}

When nothing matches, the response is {"found": false}. A request with neither verdict_id nor cid is rejected with HTTP 400.

TIP

Verdicts are stored a few seconds after they are produced, so a lookup made right after the visit can return found: false. To check a visit in real time, use Verify a verdict token instead.

Verify a verdict token ​

Check a verdict_token that your page received from the Web SDK. Use this to gate content on your server (see Server-side verification). The check is answered immediately and does not depend on stored data.

bash
POST /v1/bot/verdict/verify
X-App-Key: YOUR_APP_KEY
X-App-Secret: YOUR_APP_SECRET
Content-Type: application/json

{ "token": "vt1.eyJ2Ijoi...", "ip": "203.0.113.7", "consume": false }
ParameterRequiredDescription
tokenyesThe verdict_token from the verdict.
ipnoThe visitor's IP address as your server sees it. When given, the token must come from the same network as the visit. Recommended.
consumenotrue = the token can be used only once; a second check returns already_used. Default false: the token can be checked again until it expires.
json
{
  "valid": true,
  "error": null,
  "verdict_id": "vd_3f9c0a7e5b1d4c2a9e8f7b6a",
  "expired": false,
  "expires_at": 1767225600,
  "ip_match": true,
  "verdict": {
    "is_bot": false,
    "level": "low",
    "action": "record_only",
    "score": 12,
    "degraded": false,
    "reasons": [],
    "issued_at": 1767225000
  }
}
FieldDescription
validtrue when the token is authentic, belongs to your app, has not expired, matches ip (if given) and, with consume, has not been used. valid only says the token is genuine. It does not say the visitor is human, so also check verdict.
errornull, or why the token is not valid: malformed, bad_signature, expired, ip_mismatch, already_used, unavailable (the one-time check could not be completed; retry).
expired / expires_atExpiry state and expiry time (Unix seconds).
ip_matchtrue / false when ip was given, otherwise null.
verdictThe signed verdict: is_bot, level, action, score, degraded, reasons, issued_at. null when the token could not be read.

A typical gate serves the content when valid is true and verdict.is_bot is false. Decide yourself how to treat verdict.degraded and requests that have no token at all. If you fail open, real visitors still get through during an outage. If you fail closed, nothing gets through without a valid token.

Tokens expire after 10 minutes by default. Re-run the SDK (for example on the next page load) to get a fresh one.

Aggregated stats ​

Pull counts over a time range (totals, bots, and the breakdown by level and by day) for dashboards and quality reports.

bash
GET /v1/bot/stats?from=2026-06-01&to=2026-06-30
X-App-Key: YOUR_APP_KEY
X-App-Secret: YOUR_APP_SECRET
json
{
  "from": "2026-06-01",
  "to": "2026-06-30",
  "cleaned": true,
  "total": 124500,
  "bots": 18230,
  "by_level": { "low": 100300, "medium": 16900, "high": 6200, "critical": 1100 },
  "by_day": [
    { "date": "2026-06-01", "total": 4100, "bots": 590 }
  ]
}

from / to default to the last 7 days.

Export ​

Export per-visit verdict rows for a time range, for offline reconciliation. Rows are returned newest first, in pages.

bash
GET /v1/bot/export?from=2026-06-01&to=2026-06-30&limit=1000
X-App-Key: YOUR_APP_KEY
X-App-Secret: YOUR_APP_SECRET
ParameterDescription
from / toDate range (YYYY-MM-DD). Optional.
limitRows per page, 1–5000 (default 1000).
cursorPass the previous page's next_cursor to get the next page.
json
{
  "rows": [
    {
      "id": 1042, "cid": null, "verdict_id": "vd_3f9c0a7e5b1d4c2a9e8f7b6a",
      "is_bot": false, "score": 12, "level": "low", "action": "record_only",
      "reasons": [], "consistency": { "ok": true }, "degraded": false,
      "created_at": "2026-06-25 00:00:00", "rescored": false
    }
  ],
  "next_cursor": 1042
}

Each row carries the visit's identifiers, timestamp, and verdict fields, so you can join it back to your own logs. next_cursor is null on the last page.

Cleaned figures

Stats and exports are reported on a cleaned basis by default: traffic that is later identified as invalid is continuously re-evaluated and removed from the numbers. This happens asynchronously, so figures for very recent windows may still settle slightly. Pass ?raw=1 to see the un-cleaned figures; in export, each row also carries a rescored flag.

Per-click reconciliation

For paid-traffic scenarios, a single visit can be tied back to a specific delivered click so two parties can settle on it. That uses a click token and is covered in the Ad fraud guide.

Click token reference (for traffic providers) ​

A traffic provider signs a click token on its own server and adds it to the destination URL, so the verdict for the resulting visit is attributed back to the provider. Signing is offline — no API call.

Get your signing credentials ​

In the dashboard, open your app → Fraud Prevention → Issue signing key. You receive:

  • bot_kid — your public key id (goes into the token as pkid).
  • bot_hmac_secret — your signing secret, shown once. Keep it server-side.

Token format ​

ct.<base64url(payload)>.<base64url(HMAC_SHA256(body, bot_hmac_secret))>

body is the base64url of the JSON payload; the signature is computed over that body string.

Payload fields ​

fieldrequireddescription
pkidyesyour bot_kid; the backend uses it to look up your secret and verify the signature
cidyesunique id for this click; the reconciliation key. Generate a fresh, unique value per click
audnothe target advertiser's app_key. When set, the token is only honored on that advertiser's page. Omit to let any advertiser page accept it
click_tsnounix seconds when the click happened
expnounix seconds expiry; expired tokens are rejected

Append the token as the _ctk query parameter on the advertiser's destination URL:

https://advertiser.example/lp?_ctk=ct.<...>.<...>

The advertiser's SDK reads _ctk automatically (configurable via tokenParam). Each cid is replay-protected: the same visitor reloading the page is fine, but a token reused somewhere else is flagged. See the Security model.

Example (pseudo-code) ​

js
const payload = { pkid, cid, aud: advertiserAppKey, click_ts: now, exp: now + 900 }
const body  = base64url(JSON.stringify(payload))
const sig   = base64url(hmacSha256(body, botHmacSecret))
const token = `ct.${body}.${sig}`
const url   = `${destination}?_ctk=${encodeURIComponent(token)}`

Postback (server-to-server) ​

Instead of polling the Data API, you can have verdicts pushed to your own endpoint as they happen — handy for affiliate trackers and advertiser back-ends that want to record or reconcile each visit in real time.

Enable it ​

In the dashboard, open your app → Fraud Prevention → set Postback URL (bot_postback_url). Once set, every verdict is POSTed to that URL shortly after it is produced. Delivery is asynchronous and isolated — a slow or failing endpoint never affects the verdict already returned to the page SDK.

When it fires ​

On each verify that produces a verdict for the app. Delivery is best-effort with a few automatic retries; treat it as at-least-once and dedupe on verdict_id.

Request ​

POST <your bot_postback_url>
Content-Type: application/json
X-Bot-Signature: t=<unix_ts>,v=<hex>

Body (JSON):

json
{
  "cid": "CID_OF_THE_VISIT",
  "verdict_id": "vd_3f9c0a7e5b1d4c2a9e8f7b6a",
  "is_bot": 1,
  "score": 87,
  "level": "high",
  "action": "flag",
  "advertiser_app_id": 12,
  "provider_app_id": 34,
  "created_at": "2026-06-25 00:00:00"
}
fielddescription
cidreconciliation key for the visit (may be null if no click token was present)
verdict_idid of the verdict; same value the page received
is_bot1 / 0
scorerisk score
levellow / medium / high / critical
actionserver-authoritative action (record_only / flag / challenge / deny)
advertiser_app_idid of the app the verify ran for
provider_app_idtraffic provider id when attributed via a click token, else null
created_atverdict time (UTC)

Verify the signature ​

The X-Bot-Signature header lets you confirm the request really came from us. It is t=<unix_ts>,v=<hex> where:

hex = HMAC_SHA256( "<unix_ts>." + raw_request_body , bot_hmac_secret )

bot_hmac_secret is the same signing secret you issue under Fraud Prevention → Issue signing key. To verify, recompute the HMAC over the raw body you received (do not re-serialize the JSON) and compare in constant time. Optionally reject requests whose t is too far from your clock to bound replay.

js
// Express example
const [tsPart, vPart] = req.get('X-Bot-Signature').split(',')
const ts = tsPart.slice(2)             // "t=" prefix
const v  = vPart.slice(2)              // "v=" prefix
const expected = crypto
  .createHmac('sha256', BOT_HMAC_SECRET)
  .update(ts + '.' + rawBody)          // rawBody is the unparsed request body
  .digest('hex')
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v))

Next steps ​

MIT-licensed examples · CaptchaLa is operated independently