# Maskbreak Integration Guide

Reviewed against the HTTP contract and server SDKs on 2026-09-23.
This guide is for developers and AI coding assistants adding a fraud check to
an existing application. Keep the application's authentication, authorization,
CSRF protection and rate limits in place.

- [API reference](https://maskbreak.com/api)
- [OpenAPI specification](https://maskbreak.com/openapi.json)
- [SDKs and Cloudflare Workers](https://maskbreak.com/integrations)
- [Product capabilities and limitations](https://maskbreak.com/llms.txt)

Get an account at [signup](https://maskbreak.com/signup). Configure the API key
in your server's environment or secret store as `MASKBREAK_API_KEY`. Never put it
in frontend code, commit it, or paste it into a public AI conversation.
If your app already uses another variable name, keep that name consistent in
your deployment settings and server code. The dashboard and examples below use
`MASKBREAK_API_KEY`.

## 1. Collect browser evidence

Load the unified SDK on the page containing the protected form:

```html
<script async src="https://maskbreak.com/assets/sentinel.js"></script>

<form class="monocle-enriched" method="POST" action="/your-login-endpoint">
  <!-- Your existing login inputs stay here.
       The SDK adds monocle (network) and sentinel_fp (device). -->
</form>
```

Ordinary form submissions forward the injected `monocle` and `sentinel_fp`
fields. For custom fetch/XHR submissions, call this after the SDK has loaded
and include its result alongside your existing form data:

```javascript
const { token, fingerprintEventId, tz } = await window.Sentinel.collect();
```

The script keeps one hidden form of its own on the page (`#_sntl_net`), so a
client-rendered app (React, Next.js, Vue, Svelte) gets the network token without
any extra markup, also for forms it renders after the page has loaded, and
`collect()` returns it. That form is never submitted and is not one of your
forms; it carries `data-sentinel-net`, so if your own tests select a bare `form`,
use `form:not([data-sentinel-net])`.

`collect()` waits at most 5 seconds for the device check, then returns what it
has: `fingerprintEventId` is `null` when the check has not finished (it keeps
running and still fills your forms when it does), and `token` is `null` until
the network script has written it. Pass `collect({ timeout: 2000 })` to change
the wait (milliseconds); `window.Sentinel.ready()` takes the same option.

### Content Security Policy

If your site sends a `Content-Security-Policy` header, add these sources to the
directives you already have (keep `'self'` and your own hosts). All of them are
Maskbreak's own hosts:

```
script-src  https://maskbreak.com https://mcl.maskbreak.com https://*.mcl.maskbreak.com https://mesu.maskbreak.com 'wasm-unsafe-eval'
connect-src https://mcl.maskbreak.com https://*.mcl.maskbreak.com wss://mcl.maskbreak.com wss://*.mcl.maskbreak.com https://mesu.maskbreak.com
worker-src  https://mesu.maskbreak.com blob:
```

`maskbreak.com` serves `sentinel.js` and the network script's loader, which
continues on `mcl.maskbreak.com` and its measurement hosts; `mesu.maskbreak.com`
serves the device check, which compiles WebAssembly and starts a worker. A
policy without one of them breaks nothing on your page: the browser blocks that
part, and the evaluation goes on without it (no `token`, or
`fingerprintEventId: null`). Check your browser console for CSP reports after
adding the script.

Send evidence to your own backend, not to the Maskbreak API with a secret key
from the browser. Treat a missing collector, token or device event as unavailable
evidence. Do not fabricate a token or replace it with a sample.

## 2. Evaluate before the protected action

Roll out in two steps with one switch, `MASKBREAK_MODE`. The same handler
serves both, so switching needs no code change.

1. **Watch (the default).** Deploy with `MASKBREAK_MODE` unset or `watch`.
   Every request is evaluated and one log line records the decision, reasons,
   `risk_score`, the `X-Request-Id` response header and what enforce mode
   would have done. The existing handler always runs, including when browser
   evidence is missing, the check fails or Maskbreak is unavailable, so a
   mistake in the integration cannot lock real users out.
2. **Enforce.** After a few days, compare those log lines with the
   dashboard's Events. When they look right, set `MASKBREAK_MODE=enforce` and
   redeploy. The handler then stops `block`, holds `review`, missing or
   degraded evidence for your verification flow, rejects test, sample and
   sandbox responses, and holds the action when the check is unavailable.

This Node.js/Express example uses the HTTP API so it can forward all three
browser fields, including the optional timezone. The application must already
define `existingLoginHandler`; it performs the actual credential verification,
session creation and authorization. Configure your normal body-size limits.

In enforce mode the example holds incomplete or review results instead of
automatically allowing them. Connect that response to your own
verification/review flow. For a lower-risk action, you may choose another
documented fallback policy.

```javascript
// Server-side only. Node.js with fetch and AbortSignal.timeout.
// 'watch' (default): log what Maskbreak would do, always continue.
// 'enforce': stop block; hold review, missing evidence and errors.
const MASKBREAK_MODE = process.env.MASKBREAK_MODE || 'watch';

app.use(express.json({ limit: '16kb' }));
app.use(express.urlencoded({ extended: false, limit: '16kb' }));

app.post('/your-login-endpoint', async (req, res, next) => {
  const body = req.body || {};
  const token = body.token || body.monocle;
  const fingerprintEventId = body.fingerprintEventId || body.sentinel_fp;
  let result = null, requestId = null, hold = null;
  if (typeof token !== 'string' || !token ||
      typeof fingerprintEventId !== 'string' || !fingerprintEventId) {
    hold = [409, 'Verification required.'];
  } else {
    try {
      const response = await fetch('https://maskbreak.com/v1/evaluate', {
        method: 'POST',
        signal: AbortSignal.timeout(5000),
        headers: {
          Authorization: 'Bearer ' + process.env.MASKBREAK_API_KEY,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ token, fingerprintEventId, tz: body.tz || body.sentinel_tz })
      });
      requestId = response.headers.get('x-request-id');
      if (!response.ok) throw new Error('Evaluation unavailable');
      result = await response.json();
      if (!result || !['allow', 'review', 'block'].includes(result.decision) ||
          result.test || result.sample || result.sandbox) {
        throw new Error('No production verdict');
      }
      if (result.decision === 'block') {
        hold = [403, 'Request declined.'];
      } else if (result.decision === 'review' || result.degraded || !result.device) {
        hold = [409, 'Verification required.'];
      }
    } catch {
      hold = [503, 'Verification unavailable. Try again later.'];
    }
  }
  // One line per check. Never log the key, browser tokens or form data.
  console.log('[maskbreak]', JSON.stringify({
    mode: MASKBREAK_MODE,
    decision: result?.decision ?? null,
    reasons: result?.reasons ?? null,
    risk_score: result?.risk_score ?? null,
    request_id: requestId,
    enforce_would: hold ? 'return ' + hold[0] : 'continue'
  }));
  if (MASKBREAK_MODE === 'enforce' && hold) {
    return res.status(hold[0]).json({ error: hold[1] });
  }
  return next(); // Watch, or a passed gate; existing authentication still runs.
}, existingLoginHandler);
```

Watch mode is for rollout, not permanent protection: it records what would
have happened and blocks nothing. Enforce mode is the fraud gate; keep its
checks intact when you adapt the example.

For signup or checkout, keep the same separation: a fraud gate followed by the
existing action handler. Do not derive a trusted `accountId` from arbitrary
client input; if linking accounts, use the application's server-validated identifier.

### Official server SDKs

Node: `npm install @sentinelsup/sdk`; Python: `pip install sentinelsup`;
PHP: `composer require sentinelsup/sdk`. Their evaluation helpers accept
`token`, `fingerprintEventId`, optional `accountId` and `email`.
They currently require a non-empty token and do not forward `tz`.
Use raw HTTP when you need timezone input or deliberate degraded evaluation.

### Raw HTTP / Python

Forward both evidence fields in every language. This function performs only
the evaluation. Wrap it like the handler above: read `MASKBREAK_MODE`
(default `watch`), log one line and continue in watch mode, and apply the same
decision, test-mode and unavailable-evidence guards in enforce mode before
taking a protected action.

```python
import os
import requests

def evaluate(token: str, fingerprint_event_id: str, tz: str | None = None) -> dict:
    response = requests.post(
        "https://maskbreak.com/v1/evaluate",
        headers={
            "Authorization": f"Bearer {os.environ['MASKBREAK_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={"token": token, "fingerprintEventId": fingerprint_event_id, "tz": tz},
        timeout=5,
    )
    response.raise_for_status()
    return response.json()
```

Python 3.10+; `requests` uses connect/read timeouts, not a hard total wall-clock
deadline. Choose a bounded timeout strategy appropriate to your server.

## 3. Read the response

Selected fields from the public synthetic VPN sample:

```json
{
  "sample": true,
  "decision": "review",
  "risk_score": 65,
  "ip": "198.51.100.18",
  "country": "NL",
  "network": {
    "vpn": true,
    "proxy": false,
    "datacenter": true,
    "anonymous": true,
    "tor": false,
    "residential": false,
    "service": "PROTON_VPN"
  },
  "reasons": ["vpn_detected", "datacenter_asn"],
  "evaluated_in_ms": 28
}
```

- Route on `decision`: `allow`, `review` or `block`. The base policy reviews
  VPN-only traffic and, since 2026-09-29, a browser inside a virtual machine
  (`virtual_machine`); customer rules and exception pins can change the final result.
- `risk_score` is a signal score, not a probability of fraud or an accuracy measure.
- `isSuspicious` is a legacy engine flag, not the final policy decision. It
  never changes with email, timezone, custom rules or exception pins.
- `device` appears when device evidence resolves successfully; sending an event
  identifier does not guarantee a device block. The VPN sample has no device
  evidence. The `bot` sample includes synthetic device signals.
- Live evaluations also say how usable the evidence was, without changing
  `decision`: `device_evidence` is `"ok"`, `"unavailable"` (an event id was sent
  but did not resolve) or `"not_sent"`; `network.complete` is false for a
  partial network reading and `network.sdk_blocked` is true when the network
  check could not measure the visitor; `evidence.network_age_s`,
  `evidence.device_age_s` and `evidence.device_replayed` say how fresh it was.
  In enforce mode the handler above already holds every result without
  `device`, which covers a `device_evidence` other than `"ok"`. For a sensitive
  action (signup, payout, password reset) you may also hold when
  `network.sdk_blocked` is true, and ask for a fresh check when the evidence is
  old or replayed. Test tokens, the sandbox and samples do not carry these fields.
- `network.service` names the VPN/proxy service when known, otherwise it may be null.
- `evaluated_in_ms` is server processing time; the sample's 28 is a fixture,
  not a latency measurement. End-to-end timing includes collection and transport.
- Optional `email` adds disposable-domain screening; it is checked transiently.
  Optional `accountId` supports per-customer linked-account counts.
- With account credentials, missing network tokens can produce `degraded: true`
  over raw HTTP. Unavailable evidence must not silently count as a clean check.

See [OpenAPI](https://maskbreak.com/openapi.json) for the complete response,
optional fields and reason codes. Do not reject future additive fields.

## Confirm your website is connected

In the dashboard, open **Integration → Check it works**, enter the exact website
address hosting your form and select **Start website check**. Reload that form
and submit a fresh visit within 30 minutes. Both network and device evidence
must be verified with your live key; the device event's site address must match.
The dashboard then shows a dated website confirmation. Samples, dashboard
browser tests, old events and incomplete checks cannot complete this step.
This confirms evidence reached Maskbreak from that website, not domain ownership
or enforcement of the decision. In watch mode, check that each submission wrote
one `[maskbreak]` log line. Before switching to enforce, separately test that
your backend in enforce mode continues an allow, pauses a review and stops a
block without removing its existing security.

## Test checklist

These are reproducible contract tests, not a real-world detection benchmark.
Run them in staging with protected actions stubbed out. In enforce mode the
handler above intentionally rejects test/sample responses; do not remove that
guard on a live action to make a fixture pass.

### A. Exercise each base decision without an account

```bash
curl --fail-with-body 'https://maskbreak.com/v1/evaluate/sample?scenario=vpn'
curl --fail-with-body https://maskbreak.com/v1/evaluate \
  -H 'Authorization: Bearer sk_test_sandbox' \
  -H 'Content-Type: application/json' \
  -d '{"token":"test_vpn"}'
```

| Public sample scenario | Sandbox token | Base decision |
|---|---|---|
| clean | test_clean | allow |
| vpn | test_vpn | review |
| datacenter | test_datacenter | allow |
| proxy | test_proxy | block |
| tor | test_tor | block |
| bot | Not a deterministic test token | block |

The cloud-server fixture has a signal without a base block. The bot sample
contains synthetic browser-tampering and automation evidence. VPN does not
prove the block path; use proxy or Tor fixtures for that branch.

Samples and sandbox calls do not contact live intelligence providers.
Deterministic tokens are unbilled and not webhooked. Account/console test
events may be retained as test events; do not assume all testing is unstored.
Public samples and sandbox calls are rate-limited.

### B. Prove your application's failure paths

Stub the evaluation response locally and run every case in both modes.

In watch mode, confirm that every case below, including block, missing
evidence and an outage, writes one log line and reaches the existing handler.

In enforce mode, confirm that:

- review pauses the action for your verification flow;
- block does not reach the protected handler;
- timeout, invalid JSON, missing decision, 401, 403, 429 and 503 follow your outage policy;
- missing token, missing device event, missing device result and degraded evidence
  cannot silently become trusted sessions;
- sample/sandbox/test responses cannot authorize a real action;
- only an acceptable live allow result reaches the existing action handler;
- that handler still checks credentials, permissions, CSRF and rate limits.

Never flood production or send real signup emails to run this checklist.

### C. Evaluate real detection separately

Use a small, consented set of your own browsers and connections. Record the
test date, browser/version, scenario, expected outcome, available signals and
actual decision. Keep identifiers and raw evidence private.

Measure false flags as well as missed cases. Compare VPN-only, ordinary home
connections, cloud-hosted clients and permitted automation separately. Report
sample size, uncertainty, collection failures and the effect of custom rules.
Fixture success is not evidence of detection accuracy.

Measure collection time and end-to-end p50/p95/p99 separately from server
processing time; include failures, retries, regions and warm/cold conditions.
No guaranteed global latency or controlled accuracy benchmark is provided here.

Start in watch mode before enforcing on real users. See
[rollout guidance](https://maskbreak.com/blog/fraud-detection-rollout) and
[timeout policies](https://maskbreak.com/blog/fraud-api-timeouts).

## Error handling and operating scope

- 400: invalid request/token; fix the input, do not blindly retry.
- 401: missing or invalid account credential.
- 403: credential/account restrictions or an edge rejection; inspect the JSON
  error and request ID when available. Do not bypass an allowlist.
- 429: honor `Retry-After`; queue, back off or use your chosen fallback. With
  `code: "monthly_quota_exceeded"` the calendar month's allowance is used up
  and `Retry-After` counts down to 00:00 UTC on the 1st: apply your outage
  policy (or upgrade the plan) rather than retrying in a loop.
- 503 with `code: "storage_unavailable"`: the key or account could not be
  checked at that moment. It is not an invalid key; honor `Retry-After` and use
  your outage policy meanwhile. Never treat it as an allow.
- Timeout/5xx: bounded retries only where safe. Never label an unavailable
  evaluation as a successful check or retry the protected action blindly.
- Keep diagnostics free of secret keys, browser tokens and unnecessary personal data.

Account test keys run the full live detection, send no webhooks and are stored
as test events; their checks count toward the monthly allowance (the fixed test
tokens do not). They are not the same as deterministic fixtures and must remain
secret. IP lookups have their own monthly allowance, ten times the checks;
inspect `/v1/usage` instead of hardcoding a customer's limit.

Bare-IP screening uses `GET /v1/lookup/{ip}`. Its current production coverage
is Tor exits and cloud ranges, not the live visitor's VPN/proxy or device
checks. An unknown lookup result is not evidence that a session is safe.

Review [privacy](https://maskbreak.com/privacy), the
[DPA](https://maskbreak.com/dpa) and
[security limitations](https://maskbreak.com/security-whitepaper) for your
deployment. The SDK does not by itself establish a lawful basis or compliance.
