API Reference
Add network and device checks to your signup, login or checkout. Your backend gets the signals; you decide how to respond.
Last updated August 2026 · every API change lands on the changelog · additive-only stability policy
sk_test_sandbox key — no account required.Attack-specific playbooks: account takeover, card testing, bonus abuse, and more →
Quick Start
From zero to working integration in 5 minutes. Create a free key at /signup — 1,000 requests/hour, no card.
Using an AI coding assistant? Paste this prompt and it wires Maskbreak into your app end-to-end — frontend script, backend check, env var, and a test. It follows the machine-readable guide at maskbreak.com/integrate.md.
Fetch https://maskbreak.com/integrate.md and follow it to add Maskbreak fraud protection to this app — protect signup, login, and checkout. My API key is sk_live_YOUR_API_KEY; put it in a SENTINEL_KEY env var, never in client-side code. Then show me how to test it.
Loading…
// STEP 1 — HTML. Load the collector before your form handler. <script defer src="https://maskbreak.com/assets/sentinel.js"></script> <form class="monocle-enriched" id="login-form"> <input type="email" name="email" required> <button type="submit">Continue</button> </form> // STEP 2 — Your frontend script, after the SDK loads. document.getElementById('login-form').addEventListener('submit', async (event) => { event.preventDefault(); const { token, fingerprintEventId, tz } = await window.Sentinel.collect(); const response = await fetch('/your-login-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: event.target.email.value, token, fingerprintEventId, tz }) }); // Handle response.status and the next step in your existing login UI. }); // STEP 3 — Node 22 / Express backend. // express, app and existingLoginHandler belong to your application. // This check is NOT authentication and never creates a login session. app.use(express.json({ limit: '16kb' })); app.post('/your-login-endpoint', async (req, res, next) => { try { const { token, fingerprintEventId, tz, email } = req.body; const response = await fetch('https://maskbreak.com/v1/evaluate', { method: 'POST', signal: AbortSignal.timeout(2000), // example budget; measure your own headers: { 'Authorization': 'Bearer ' + process.env.SENTINEL_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ token, fingerprintEventId, tz, email }) }); if (!response.ok) throw new Error('Visitor check unavailable'); const result = await response.json(); // Production gate: never authorize from a fixture or personal test key. // Use a live key in SENTINEL_KEY; test the integration separately. if (result.test || result.sandbox || result.sample) { throw new Error('Synthetic visitor check'); } if (!['allow', 'review', 'block'].includes(result.decision)) { throw new Error('Unexpected verdict'); } if (result.decision === 'block') { return res.status(403).json({ error: 'Action refused by policy' }); } if (result.decision === 'review' || result.degraded) { // Implement an actual step-up flow; never mint a session here. return res.status(409).json({ next: 'step_up' }); } req.fraudCheck = result; return next(); // normal credential + authorization checks still run } catch { // Example temporary hold. Choose a fallback for each business action. return res.status(503).json({ error: 'Check unavailable; try again shortly' }); } }, existingLoginHandler);
Before you enforce a verdict
Test normal decisions, missing evidence and customer recovery separately. These guides cover the application-side choices that an API response cannot make for you.
Try It Live
Hit the sandbox endpoint with one click. No signup, no API key — just real /v1/evaluate response shapes you can copy-paste into your code.
Production calls use POST /v1/evaluate with a token from the SDK and an Authorization: Bearer sk_live_... header. The sample endpoint returns the same response shape so you can wire your parsing logic before signing up.
How It Works
Maskbreak uses a 3-step Client → Your Backend → Maskbreak API flow to keep your secret key off the browser.
The Maskbreak SDK runs invisibly in your user's browser, collecting telemetry. It injects an encrypted token into your forms.
Your frontend submits the token to your own backend server along with the rest of the form data.
Your backend calls POST /v1/evaluate with your secret key. Maskbreak returns a threat intelligence report instantly.
Authentication
All requests to /v1/evaluate must include your secret API key as a Bearer token. Find your key in the Dashboard.
sk_live_… in your HTML, JavaScript, or any client-side code. Only use it in your backend server environment.Rotating your key
Rotate from the console (Integration → Rotate, or Settings → API Key; your password is asked again). The new key works at once and the previous key keeps working for 24 hours, so a deploy can switch over without a gap. If a key has leaked, Revoke old key now in Settings ends that grace immediately. During the grace each key answers /v1/usage with its own hourly allowance.
Every account has a personal sk_test_… key. It runs the live pipeline and records test events, without increasing live usage or sending live webhooks; it has its own hourly allowance and bypasses the IP allowlist. The public sk_test_sandbox key only answers deterministic test tokens, with no account or stored events. A live-key IP allowlist restricts /v1/evaluate and /v1/lookup, not /v1/usage.
OAuth 2.0 Client Credentials New
Exchange your account email and API key for a short-lived bearer token at the standard client_credentials grant. The token carries your account id, never the key itself. It lives for 60 minutes and keeps working through a key rotation (it resolves to whatever your current key is), so rotating does not revoke tokens already issued — stop issuing new ones and let the old ones expire. A token minted with your sk_test_ key stays a test credential. API keys keep working exactly as before; this is additive.
POST /oauth/token
/.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource.curl -X POST https://maskbreak.com/oauth/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"your@email.com","client_secret":"sk_live_..."}'Use the returned access_token anywhere an API key is accepted in the Authorization: Bearer header. Tokens expire; refresh by repeating the exchange. The key itself never leaves your server.
Embed the SDK
Add the Maskbreak SDK to every page where you want to evaluate users. One script loads both detection layers — network intelligence (VPN, proxy, datacenter) and device intelligence (antidetect browsers, bots, tampering) — and injects both tokens into your forms.
<!-- Maskbreak SDK — network + device, paste inside <head> --> <script async src="loading..."></script> <!-- Add class="monocle-enriched" to any form you want evaluated --> <form class="monocle-enriched" id="my-form"> <!-- The SDK injects both automatically: --> <input type="hidden" name="monocle" value="eyJ..." /> // network <input type="hidden" name="sentinel_fp" value="a1b2..." /> // device </form>
The SDK takes ~1–2 seconds to load and generate the tokens. If the device layer can’t run (e.g. a hardened browser blocks it), evaluation continues on the network layer alone.
Collect the Tokens
On submit, call Sentinel.collect() — it waits for the device layer to settle and returns { token, fingerprintEventId }. Forward both to your backend. (Or read the monocle and sentinel_fp hidden inputs directly.)
document.getElementById('my-form').addEventListener('submit', async (e) => { e.preventDefault(); // Both layers: network token + device event id const { token, fingerprintEventId } = await window.Sentinel.collect(); if (!token) { // Network SDK still loading — ask user to try again return showError('Security check loading. Please try again.'); } // Forward both to your backend with the rest of the form data const res = await fetch('/your-backend-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: e.target.email.value, token, fingerprintEventId // ← both layers to your backend }) }); });
Call the Evaluate Endpoint
loading…
Call this from your backend server — never from the browser. Pass the token from the client and your secret API key.
Request Body
Sentinel.collect() or input[name="monocle"]. Send it for full network intelligence. Raw HTTP accepts a missing or empty token as a degraded evaluation; a non-string or provider-rejected token returns 400. The current Node, Python and PHP server SDKs require a non-empty token locally, so use raw HTTP for this fallback.sentinel_fp; forward it here (either name is accepted) to unlock the device.* signals (antidetect, automation, emulator, …). Omit it and you get a network-only verdict.fingerprintEventId, Maskbreak counts how many distinct accounts this device has been linked to under your API key and returns device.linked_accounts / device.multi_account — multi-accounting detection with zero extra integration. Stored as a one-way hash only.email.disposable to the response — checked against a continuously refreshed feed of thousands of burner/disposable domains. A disposable hit adds the disposable_email reason, raises risk_score, and escalates allow to review. The address is checked transiently and never stored or logged.Europe/Tallinn. Our client SDK sends it automatically as sentinel_tz, which this endpoint also accepts; on your own you can read it with Intl.DateTimeFormat().resolvedOptions().timeZone — no permission prompt and no geolocation involved. Adds a timezone block to the response. When the connection looks ordinary but the browser's clock belongs to a different country, that is the shape of a residential proxy the network layer did not catch: it adds the timezone_mismatch reason, raises risk_score, and escalates allow to review. Deliberately not raised on a VPN, proxy, Tor or datacenter exit — disagreeing with the clock is what those do — and never enough to block on its own, because travellers and expatriates land here legitimately. Forward the tz returned by Sentinel.collect() in your raw HTTP request. The current server SDK helpers do not serialize tz; use raw HTTP when you need this field.Testing without a browser: send a deterministic test token — test_clean, test_vpn, test_proxy, test_datacenter, or test_tor — to exercise your allow / review / block handling end-to-end. Test calls are authenticated and rate-limited like real ones but never billed, stored, or webhooked, and the response carries "test": true.
CI & staging without an account: the public sandbox key sk_test_sandbox accepts the same test_* tokens and returns the same deterministic shapes — no signup, nothing billed, nothing stored, never touches a real quota. It only answers test tokens (live traffic still needs your real key) and is rate-limited per IP. curl -X POST https://maskbreak.com/v1/evaluate -H "Authorization: Bearer sk_test_sandbox" -H "Content-Type: application/json" -d '{"token":"test_vpn"}'
Your own test key (full pipeline, zero footprint): every account also has a personal sk_test_… key (Settings → API Key), and unlike the sandbox it runs the complete live pipeline — real tokens, real device intelligence, your rules and exception pins included. Events it creates are badged as test in the console, excluded from usage and stats, never fire webhooks or limit emails, and the response carries "test": true. It has its own hourly bucket and is deliberately exempt from the IP allowlist, so CI and laptops can exercise the pipeline without punching holes in your production restriction.
const response = await fetch('https://maskbreak.com/v1/evaluate', { method: 'POST', headers: { 'Authorization': 'Bearer sk_live_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ token: req.body.token, // as sent by your frontend (see Quick Start Step 2) fingerprintEventId: req.body.fingerprintEventId, tz: req.body.tz }) }); const data = await response.json(); // data.decision → 'allow' | 'review' | 'block' (covers VPN, proxy, // datacenter, Tor, antidetect, automation) — route on this // data.reasons → machine-readable why if (data.decision === 'block') { return res.status(403).json({ error: 'Suspicious connection.' }); }
# pip install sentinelsup from sentinel import Sentinel s = Sentinel(api_key='sk_live_YOUR_API_KEY') # or omit — reads SENTINEL_KEY env var # request_body is the JSON body parsed by your web framework. # This SDK requires a non-empty token; use raw HTTP for degraded fallback or tz. result = s.evaluate( token=request_body.get('token', ''), fingerprint_event_id=request_body.get('fingerprintEventId') ) # result.decision → 'allow' | 'review' | 'block' — route on this # result.reasons → machine-readable why if result.is_blocked: return '403 Suspicious connection', 403
curl -X POST 'https://maskbreak.com/v1/evaluate' \ -H 'Authorization: Bearer sk_live_YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"token": "YOUR_BROWSER_TOKEN", "fingerprintEventId": "YOUR_DEVICE_EVENT", "tz": "Europe/Tallinn"}'
// composer require sentinelsup/sdk $sentinel = new \Sentinel\Client(); // or pass the key — reads SENTINEL_KEY env var // The frontend sends JSON, not a URL-encoded form. // This SDK requires a non-empty token; use raw HTTP for degraded fallback or tz. $input = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR); $result = $sentinel->evaluate([ 'token' => $input['token'] ?? '', 'fingerprintEventId' => $input['fingerprintEventId'] ?? null, ]); // $result->decision → 'allow' | 'review' | 'block' — route on this // $result->reasons → machine-readable why if ($result->isBlocked()) { http_response_code(403); exit(); }
Response Object
A successful call returns 200 OK with this JSON structure.
| Field | Type | Description |
|---|---|---|
| decision | string | Recommended action: "allow", "review", or "block". Advisory only — you set the policy. The reasons array explains every decision so you can log it or show an adverse-action reason to your own end user. If you configure custom rules in your dashboard, this field returns your action for a matching signal (most-severe rule wins). |
| engine_decision | string | decision_source is returned whenever your policy matched this event: "rules" (with rule_matched listing the signals that triggered a rule) or "exception" (with exception_matched naming the per-IP/visitor pin from your dashboard's Exceptions list — explicit pins outrank signal rules). engine_decision additionally appears when the match actually changed the decision, preserving the engine's own risk-score verdict. |
| risk_score | integer | Composite risk score 0–100 across all network and device signals. Not altered by custom rules. |
| isSuspicious | boolean | Legacy convenience flag. true when VPN, proxy, antidetect, automation, or emulator fired. Tor and datacenter signals raise risk_score and drive decision (Tor blocks) but do not set this flag — route on decision for full coverage. |
| ip | string | The unmasked, real IP address of the user. Mirrored at details.ip. |
| country | string | ISO 3166-1 country code (e.g. "US", "EE"). Mirrored at details.cc. |
| network. |
boolean | Commercial VPN detected (NordVPN, Proton, ExpressVPN, etc.). Mirrored at details.vpn. |
| network. |
boolean | Residential or SOCKS/HTTP proxy detected. Mirrored at details.proxied. |
| network. |
boolean | Traffic from a known datacenter / cloud provider ASN. Mirrored at details.dch. |
| network. |
boolean | Tor exit node detected. Mirrored at details.tor. |
| network. |
boolean | Any anonymization layer detected. Mirrored at details.anon. |
| network. |
boolean | true when neither datacenter nor proxy fired (looks like a real home IP). |
| network. |
string | Identified provider when known (e.g. "PROTON_VPN", "BRIGHT_DATA"). Mirrored at details.service. |
| device. |
boolean | Antidetect / fingerprint-spoofing browser (Multilogin, Kameleo, GoLogin, etc.). Only present when fingerprintEventId was sent. |
| device. |
boolean | Automated browser detected (Puppeteer, Playwright, Selenium). |
| device. |
boolean | Mobile emulator. |
| device. |
boolean | Virtual machine. |
| device. |
boolean | Private / incognito browsing mode detected. |
| device. |
boolean | Enhanced privacy settings active (e.g. Brave Shields, Firefox resist-fingerprinting). |
| device. |
boolean | The visitor IP appears on email-spam or attack-source blocklists. |
| device. |
boolean | This device is being identified unusually often (High-Activity Device). Informational — frequent visits alone aren't fraud, but combined with other signals they often indicate automation or farming. Adds reason code high_activity_device. |
| device. |
string | Stable device fingerprint hash. Same device returns the same id across sessions even after cookies clear — useful for ATO defense and device clustering. |
| device. |
number | 0–1 tampering score. Above 0.6 strongly suggests an antidetect browser; 0.3–0.6 means soft inconsistencies; below 0.3 is normal. |
| device. |
integer | Cumulative sightings of the one-way device hash while its history record exists. It is not a count of your customers, not scoped to your key, and not a rolling 90-day count. Records are eligible for pruning after 90 days without a sighting. A value of 1 is limited evidence of novelty, not proof of fraud. |
| device. |
string | ISO 8601 time of the first sighting in the current device-history record. This history is not scoped to your API key and can restart after inactivity pruning. Use your own account history for customer age. The separate linked_accounts field remains customer-scoped. |
| device. |
boolean | True when times_seen > 1 — this device has been evaluated before. |
| device. |
integer | How many distinct accounts this device has been seen with under your API key in the last 90 days. Only present when you pass your own accountId in the request alongside fingerprintEventId. Both identifiers are stored as one-way hashes only. |
| device. |
boolean | True when linked_accounts > 1 — the core multi-accounting signal (bonus abuse, trial farming, duplicate signups). Adds reason code multi_account_device. |
| email. |
boolean | Present only when the request included the optional email parameter. true when the address uses a known burner/disposable domain — adds the disposable_email reason and escalates allow to review. |
| timezone. |
string | Present only when the request included a recognised tz. The IANA zone the browser reported, echoed back. |
| timezone. |
string | ISO 3166-1 alpha-2 country that zone belongs to. |
| timezone. |
boolean | true when the zone's country equals country (the exit IP's). Build your own rule on this if you want a policy stricter than ours. |
| timezone. |
boolean | false on a VPN, proxy, Tor or datacenter exit, where a mismatch is expected and we raise nothing. When false, read matches_ip as information rather than as a verdict. |
| reasons | string[] | Machine-readable codes for which signals fired (e.g. "vpn_detected", "datacenter_asn", "antidetect_browser"). |
| evaluated_in_ms | integer | Server-side processing time for this request, in milliseconds. |
{
"status": "success",
"isSuspicious": true,
"decision": "review",
"risk_score": 65,
"ip": "185.107.80.12",
"country": "EE",
"network": {
"vpn": true,
"proxy": false,
"datacenter": true,
"tor": false,
"anonymous": true,
"residential": false,
"service": "PROTON_VPN"
},
"reasons": ["vpn_detected", "datacenter_asn"],
"evaluated_in_ms": 126,
"details": {
"ip": "185.107.80.12",
"cc": "EE",
"vpn": true,
"proxied": false,
"anon": true,
"dch": true,
"service": "PROTON_VPN"
}
}{
"status": "success",
"isSuspicious": false,
"decision": "allow",
"risk_score": 0,
"ip": "82.131.45.9",
"country": "DE",
"network": {
"vpn": false,
"proxy": false,
"datacenter": false,
"tor": false,
"anonymous": false,
"residential": true,
"service": null
},
"reasons": [],
"evaluated_in_ms": 118
}How the decision is made
The engine returns block for a proxy, Tor exit, detected bot, emulator or browser tampering. A VPN alone returns review. Other signals and your policy can affect the final answer. A cloud-server or anonymous-network flag alone is not a hard block. Your rules and exceptions can override the decision; engine_decision is included when an override changes it, not on every response.
Degraded answers. Missing network evidence produces degraded: true, ip: "unknown" and false network flags. Those false flags mean unavailable evidence, not a clean connection. Device or email signals, rules and exceptions can still produce review or block. Only the bare non-suspicious allow fallback is omitted from evaluation totals and Events; meaningful verdicts follow normal event and usage handling. Authenticated attempts still use the hourly request allowance.
Extra flags. test: true on any test-token or test-key answer, sandbox: true when the public sk_test_sandbox key was used, sample: true on /v1/evaluate/sample. /v1/lookup also carries an additive network.cloud object naming the provider when the address sits in a published cloud range.
Reason codes
Every value reasons[] can carry, and what it means. New codes are only ever added.
anonymous_network— an anonymising network with no operator namedantidetect_browser— a fake browser (antidetect tooling or a tampered environment)automation_detected— a script or headless browserdatacenter_asn— a datacenter or cloud addressdisposable_email— a throwaway email addressemulator_detected— an emulatorhigh_activity_device— a device seen unusually often (soft signal)ip_blocklisted— an address on a blocklistmulti_account_device— a device already behind several of your accountsprivate_browsing— an incognito or private windowproxy_detected— a residential or cloud proxytimezone_mismatch— browser time zone disagrees with the address (only when checked)tor_exit_node— a Tor exitvirtual_machine— a virtual machinevpn_detected— a VPN service (network.service names it when known)
Test tokens
Send one of these as token with your live key, your test key or sk_test_sandbox. With your own keys they obey your Rules and exception pins (so test_vpn is a one-line way to verify a rule); they are never billed. Calls made from the console are stored as test rows so the Events log has something to show.
| Token | Pretend visitor | Signals | Decision |
|---|---|---|---|
test_clean | 203.0.113.42 · US | nothing | allow |
test_vpn | 198.51.100.18 · NL | VPN, datacenter (PROTON_VPN) | review |
test_proxy | 203.0.113.9 · DE | proxy, datacenter (BRIGHT_DATA) | block |
test_datacenter | 203.0.113.7 · DE | datacenter (AWS) | allow |
test_tor | 203.0.113.99 · null | Tor (TOR) | block |
IP Lookup
A verdict for a bare IP address — no browser token needed. Same key, same hourly bucket as /v1/evaluate.
GET /v1/lookup/{ip} — for server-side screening where no client runs: allowlist checks, batch scoring, enriching your own logs. Network signals only (there is no device to fingerprint), so use /v1/evaluate whenever a browser is involved. On a bare IP the Tor-exit and cloud-range checks fire; VPN and proxy tunnels — and the service name in network.service — are detected on a live visit via /v1/evaluate, so signals.vpn and signals.proxied come back false here.
curl https://maskbreak.com/v1/lookup/185.220.101.34 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
{
"ip": "185.220.101.34",
"known": true,
"verdict": "block", // allow | review | block
"risk_score": 90,
"signals": { "vpn": false, "proxied": false, "tor": true, "dch": false, "anon": true },
"network": { "asn": 205100, "org": "F3 Netze", "country": "DE", "city": null },
"latency_ms": 121
}
known: false means no intelligence source had an opinion — the verdict is then allow with risk_score: 0, which is “nothing found,” not a clean guarantee. Exception pins you configure in the dashboard apply here too (verdict_source: "exception" with engine_verdict preserved, additively). Signal spellings (proxied, dch) are frozen — safe to parse.
Rate Limits
Maskbreak is in open beta — all access is free. Two limits run in parallel; whichever you hit first returns 429.
| Scope | Limit |
|---|---|
| Per API key (Free) | 1,000 requests / hour |
| Per API key (public interest) | No hourly cap |
| Per source IP | 50,000 requests / hour (approved public-interest keys pass through it) |
No monthly cap during beta. The per-IP ceiling is a backstop against runaway abuse from a single machine. Retry-After is included on every 429 response.
Keyed /v1/evaluate and /v1/lookup responses also carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds) so you can back off before the 429 — and an X-Request-Id header you can quote to support when something looks wrong.
Keys on the public-interest program (hospitals, public health, government, elections, emergency services, universities, registered non-profits) have no hourly cap: the three X-RateLimit-* headers are omitted rather than filled with a sentinel, and /v1/usage reports hourly_limit: null with uncapped: true.
Reliability & fail-open behaviour
Maskbreak returns the best available evidence when an intelligence layer is unavailable. degraded: true specifically reports missing network intelligence; a device-provider failure alone does not set it. A missing device object is not proof that browser checks passed. With a network outage or missing token, only the bare non-suspicious allow fallback is excluded from evaluation totals and Events. Independent signals or policy overrides may still yield a stored review or block; personal test keys retain test-event semantics. Attempts still consume the hourly request allowance. A provider-rejected or non-string token instead returns 400 Invalid token.. Choose an explicit endpoint-specific fallback for missing evidence; do not equate unavailable checks with a safe visitor.
Error Codes
All error responses from the API include an error string field. One exception, noted under 403: a block at the edge returns plain text, not JSON.
-
400
Bad Request — Invalid
token(rejected by the provider, or not a string). A missing or empty token is not an error: it is answered fail-open withdegraded: true. - 401 Unauthorized — Invalid or missing API key.
-
403
Forbidden — Account suspended (contact support@maskbreak.com), or the caller's IP is not on your key's IP allowlist (Settings → API Key Security; the
hintfield names the calling address). Not retryable from the same address.
Third cause, and the confusing one: a bareerror code: 1010intext/plainwith no JSON body is not us — that is our edge rejecting your HTTP client's default User-Agent.Python-urllibis the common one. Set any realUser-Agentheader and it clears; every official SDK already does. -
429
Rate Limited — the normal evaluation allowance is 1,000/hour per key with a separate per-address protection limit. Approved public-interest accounts have no per-key hourly cap, but are not exempt from every safety control:
/v1/usage, sample responses, OAuth and invalid-key protection have independent limits. HonorRetry-Afterwhen returned. - 500 Server Error — unexpected internal error. Use bounded backoff within your action's deadline. A retry may repeat a completed evaluation; never retry a payment or other business mutation blindly.
Usage
Your key’s quota position as an endpoint — poll it instead of scraping rate-limit headers off your last response.
GET /v1/usage — authenticate with the API key itself (Authorization: Bearer …). Works with both key types — sk_live_ and sk_test_ each report their own hourly bucket, named in key_type — and the call is free: it never consumes quota, so a monitor polling it can’t eat into your limit.
curl https://maskbreak.com/v1/usage \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"
{
"key_type": "live", // or "test"
"hourly_limit": 1000,
"used_this_hour": 412,
"remaining": 588,
"resets_at": "2026-07-19T11:00:00.000Z", // end of the rolling 60 minutes that began with your first call; null if nothing used yet
"total_evaluations": 183204,
"limit_hits": 3
}
used_this_hour and remaining are advisory: the counters are in-memory and per-process, so they reset on a deploy — treat them as a live gauge, not an audit record (enforcement of the actual limit is unaffected). total_evaluations and limit_hits are durable account totals.
Limits and notices. The hourly window starts with your first call. Usage warnings are sent to the owner at 80% and at the cap, at most once per threshold per 24 hours for a live key. Test keys do not send these warnings. /v1/usage consumes no evaluation quota but is limited to 120 reads/minute per address, including public-interest accounts (429 with Retry-After). The endpoint does not enforce the key's IP allowlist; suspension still returns 403. Repeated invalid credentials can trigger a 15-minute address lockout (429, Retry-After: 900).
Webhooks
Threat alerts pushed to Slack, Discord, or your own endpoint — configured on the console’s Integration tab, under Alerts.
When they fire. On engine-detected threats (VPN, proxy, Tor, datacenter with tampering, bots, antidetect browsers) and on any evaluation your own custom rules decided to block. Slack and Discord URLs get a channel-ready message; any other HTTPS endpoint receives a JSON event (event: "threat.detected" with IP, threat type, and the signal details). The dashboard's Send test button delivers the same shape with event: "test" — if your receiver switches strictly on the event type, handle both, or the test button will look like a silent failure.
Verifying authenticity. Every delivery is signed with your account's signing secret (shown next to the webhook URL in the dashboard). Compute HMAC_SHA256(secret, timestamp + "." + rawBody) using the X-Maskbreak-Timestamp header and compare it (constant-time) to the hex digest in X-Maskbreak-Signature (after the sha256= prefix). Reject anything unsigned, mismatched, or older than 5 minutes.
Copy-paste receivers. The two classic mistakes are verifying against the re-serialized parsed body instead of the raw bytes, and comparing with ==. These receivers do it right:
const crypto = require('crypto'); const express = require('express'); const app = express(); // Raw body required — verify the exact bytes Maskbreak signed, // never a re-serialized JSON.parse() of them. app.post('/webhooks/sentinel', express.raw({ type: 'application/json' }), (req, res) => { const secret = process.env.SENTINEL_WEBHOOK_SECRET; // shown next to the URL in the dashboard const ts = req.get('X-Maskbreak-Timestamp') || ''; const sig = req.get('X-Maskbreak-Signature') || ''; // Freshness: reject anything older than 5 minutes (replay defense) const age = Math.abs(Date.now() / 1000 - Number(ts)); if (!ts || !Number.isFinite(age) || age > 300) return res.status(400).end(); // "sha256=" + HMAC_SHA256(secret, timestamp + "." + rawBody), hex digest const expected = 'sha256=' + crypto.createHmac('sha256', secret) .update(ts + '.').update(req.body).digest('hex'); // Constant-time compare — never === const a = Buffer.from(sig), b = Buffer.from(expected); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).end(); } const event = JSON.parse(req.body); // { event: "threat.detected", event_id, ip, threat_type, details } // ... handle it (queue it, alert, block the IP upstream) res.status(200).end(); // answer 2xx fast — deliveries time out after 10 s });
import hashlib, hmac, os, time from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["SENTINEL_WEBHOOK_SECRET"].encode() # shown next to the URL in the dashboard @app.post("/webhooks/sentinel") def sentinel_webhook(): ts = request.headers.get("X-Maskbreak-Timestamp", "") sig = request.headers.get("X-Maskbreak-Signature", "") # Freshness: reject anything older than 5 minutes (replay defense) try: fresh = abs(time.time() - int(ts)) <= 300 except ValueError: fresh = False if not fresh: abort(400) # "sha256=" + HMAC_SHA256(secret, timestamp + "." + raw_body), hex digest. # Sign request.get_data() — the exact raw bytes, not request.json. expected = "sha256=" + hmac.new( SECRET, ts.encode() + b"." + request.get_data(), hashlib.sha256 ).hexdigest() # Constant-time compare — never == if not hmac.compare_digest(sig, expected): abort(401) event = request.get_json(force=True) # {"event": "threat.detected", "event_id": ..., "ip": ..., "details": ...} # ... handle it (queue it, alert, block the IP upstream) return "", 200
Delivery & retries. Each event is attempted up to 3 times: a failed delivery is retried after roughly 1 minute, then roughly 8 minutes. Retries are best-effort and in-process — a deploy mid-retry drops the remaining attempts — so treat the dashboard Events log as the source of truth and the webhook as a best-effort push. The 25 most recent deliveries (event, outcome, error) are listed in the dashboard Tools drawer, retained 30 days; the header chip shows current health, and if 5 consecutive deliveries fail we email you — a dead webhook must never read as “no threats.” Deliveries time out after 10 s and never follow redirects.
Dedupe on event_id. Every payload carries an event_id — 32 hex chars, unique per event, present on test events too — and every delivery also carries it in the X-Maskbreak-Event-Id header. Retried deliveries reuse the same event_id, so key your processing on it and each event is handled exactly once no matter how many attempts reach you.
Generic-endpoint payload. What your receiver actually gets (Slack/Discord URLs get a formatted message instead):
{
"event": "threat.detected",
"event_id": "9f2ce7a4c1d84b7fa3d2c05e8b6f4a19", // unique per event; same on retries
"timestamp": "2026-07-19T09:14:03.512Z",
"ip": "185.220.101.34",
"threat_type": "Tor", // or "Custom rule (…)" / "Customer exception (…)"
"details": {
"ip": "185.220.101.34", "cc": "DE",
"vpn": false, "proxied": false, "tor": true, "dch": false,
"bot": false, "tampering": false, "antidetect": false,
"decision": "block",
"rule_matched": null, // signals, when your rule caused the block
"exception_matched": null // pins, when your exception caused it
}
}
MCP for AI Agents
A hosted Model Context Protocol server — point an AI agent at it and it can screen IPs with real Maskbreak verdicts.
Endpoint: POST https://maskbreak.com/mcp (streamable HTTP, stateless). Tools: lookup_ip — the same verdict as GET /v1/lookup — and service_status. Authenticate with either a classic API key Authorization: Bearer sk_live_… or an OAuth access_token from POST /oauth/token. Anonymous use shares the free tool's tight per-IP limits; authenticated calls run on your 1,000/hour quota (your IP allowlist applies here too).
{
"mcpServers": {
"sentinel": {
"type": "http",
"url": "https://maskbreak.com/mcp",
"headers": { "Authorization": "Bearer sk_live_YOUR_API_KEY" }
}
}
}
API Stability & Versioning
What you can build on without worrying about the ground moving.
Versioning. The API is versioned in the path (/v1/). Within a major version we make only additive changes: new response fields, new optional request parameters, new signal reasons. Your integration must tolerate unknown fields in responses — that is the only forward-compatibility requirement we place on you.
Breaking changes. Renaming or removing response fields, changing types or semantics, or retiring an endpoint only happens in a new major version (/v2/). When that day comes, /v1/ keeps working for at least 12 months after the announcement.
Deprecation notice. Any deprecation is announced at least 90 days in advance via the changelog and email to affected API keys, with a documented migration path. Enterprise agreements can pin longer support windows — support@maskbreak.com.