Policy API
The Policy API evaluates an encrypted Monocle Assessment against your application's policy and returns a decision: allowed or blocked, with the reason. Use it to enforce your policy on your own backend, without writing the decision logic yourself.
It works like the Decryption API, and adds the policy decision on top of the decrypted assessment.
| Decryption API | Policy API | |
|---|---|---|
| Endpoint | /api/v1/assessment | /api/v1/policy |
| Returns | The decrypted assessment | A decision, its reason and the assessment |
| Decision logic | Yours, from the assessment fields | Your policy, as configured in Monocle |
With an SDK integration (JavaScript, React or Next.js), your backend calls the Policy API and acts on the decision. With an edge integration (Cloudflare, CloudFront or Fastly), Monocle calls it for you on the paths you enforce; see Assess & Enforce.
Request
Send a POST request to https://decrypt.mcl.spur.us/api/v1/policy, with your Secret Key in a TOKEN HTTP header. Find the Secret Key in the Monocle dashboard for your application.
The body is JSON:
| Field | Required | Description |
|---|---|---|
assessment | Yes | The encrypted Monocle Assessment, as your frontend received it |
ip | No | The visitor's public IP address, written the same way as in the assessment: 198.51.100.24, not ::ffff:198.51.100.24. Used by the IP mismatch check, which compares the two exactly. |
cpd | No | The client-provided data you set on the assessment. Used by the CPD mismatch check. |
The API also accepts the same fields as form data, with the Content-Type header set to exactly application/x-www-form-urlencoded. With a parameter added, such as ; charset=UTF-8 (which fetch and axios add when you send URLSearchParams), the API reads the body as JSON and returns 400. If you can't control the header, send JSON.
Call the Policy API from your backend only. Your Secret Key must never reach the browser.
curl -X POST "https://decrypt.mcl.spur.us/api/v1/policy" \-H "Content-Type: application/json" \-H "TOKEN: <MONOCLE SECRET KEY>" \--data '{"assessment": "<ENCRYPTED MONOCLE ASSESSMENT>", "ip": "<VISITOR IP>"}'
Response
A successful evaluation returns 200 with a JSON body:
| Field | Type | Description |
|---|---|---|
allowed | boolean | true if the session passes your policy, false if your policy blocks it |
reason | string | Why, such as Assessment allowed or VPNs blocked. See Decision reasons. |
decisionId | string | A unique ID for this decision. Log it with your own records. |
assessment | object | The decrypted Monocle Assessment |
{"allowed": false,"reason": "VPNs blocked","assessment": {"vpn": true,"proxied": false,"anon": false,"rdp": false,"dch": false,"cc": "US","ip": "198.51.100.24","ts": "2026-10-13T15:04:05Z","complete": true,"id": "7c1e5a92-3f4b-4d8e-9a6c-2b0f8e1d4c37","sid": "example-form","service": "GOOGLE_ONE_VPN","ai_agentic": false,"ai_crawling": false},"decisionId": "0b8f3c2e-6d41-4a7b-9e15-3c9a2f7d8e60"}
Plan differences
- Blocking needs the Team plan or above. On other plans, a session your policy would block comes back with
allowed: trueand the reasonnot entitled to block. - The full assessment needs the Pro plan or above. On other plans,
assessmentholds onlyid,ip,ipv6,tsandcomplete. To read the other fields, use the Decryption API.
See Plan availability on the Policies page.
Decision reasons
Monocle checks your policy's rules in the order below and stops at the first one that decides. A rule only applies if your policy turns it on. Assessment is incomplete and CPD mismatch have no switch on the Policies page: whether they apply comes from your Block Strategy.
reason | allowed | When |
|---|---|---|
Assessment is incomplete | false | The assessment didn't finish collecting before it was sent |
Invalid assessment timestamp | false | The assessment's timestamp can't be read, so its age can't be checked |
Assessment TTL has expired | false | The assessment is older than your policy's Max TTL |
IP mismatch | false | The ip you sent doesn't match the assessment's IPv4 or IPv6 address |
CPD mismatch | false | The cpd you sent doesn't match the assessment's client-provided data |
Service in deny list | false | The session's service is in your policy's Blocked services |
Assessment allowed by Service label | true | The session's service is in your policy's Exempt services. Later rules aren't checked. |
Country in deny list | false | The session's country is on your policy's Geo Blocklist |
Datacenter hosting not allowed | false | Datacenter traffic, with Deny datacenter on |
RDP not allowed | false | Remote Desktop Protocol traffic, with Deny RDP on |
Anonymous tunnels with Service labels blocked | false | Anonymous traffic from a named service, with Deny anonymous named on |
Anonymous connections blocked | false | Anonymous traffic, with Deny anonymous on |
VPNs blocked | false | VPN traffic, with Deny VPNs on |
Proxied connections blocked | false | Proxied traffic, with Deny proxies on |
Country not in allowed list | false | Your policy has a Geo Allowlist, and the session's country isn't on it |
Assessment allowed | true | No rule blocked the session |
not entitled to block | true | Your policy would block the session, but your plan doesn't include blocking. See Plan differences. |
The IP mismatch check only runs when you send ip. The CPD mismatch check only runs when you send cpd and the assessment carries client-provided data.
Errors
Errors return a plain-text body, not JSON. Branch on the status code, not the body: the bodies below end with a newline, and a 429 can come back as an HTML page.
| Status | Body | Cause |
|---|---|---|
400 | Bad request: missing monocle bundle | The request has no assessment |
400 | Bad request | The body can't be read, or the assessment can't be decrypted with your application's key |
401 | Unauthorized | The TOKEN header is missing, or isn't a valid Secret Key. A 401 on a key that was working can also come from a problem on Monocle's side, so retry later before you rotate the key. |
404 | Policy not configured for this deployment | Your application has no policy. Set one on the Policies page. |
429 | Varies | The Policy API is briefly at capacity. There's no per-key request limit. |
500 | Internal server error | A problem on Monocle's side |
A 400 for an assessment that can't be decrypted usually means it was sent to the wrong application's key, or it was altered or made up. Treat it as a failed check, not as an outage.
Handling failures
Decide what your backend does when the Policy API doesn't answer in time or returns an error other than 400:
- Fail open: let the session through, and log that no decision was made. This keeps your application available if Monocle can't be reached.
- Fail closed: block the session, or step it up to extra verification. This is stricter, but an outage then affects your visitors.
Set a timeout on the request either way, so a slow answer doesn't hold up your visitor. Monocle's own edge integrations wait up to five seconds and fail open.
The Node library (@spur.us/monocle-backend) doesn't take a timeout, and its errors don't carry the status code. To do both, call the API with fetch, as in the login example.
A 429 or 5xx is temporary. Handle it the same way as a timeout, and retry with a backoff rather than straight away.
Example: enforce on login
Send the assessment with the login form, call the Policy API from your login handler, and act on the status code and allowed. This example uses fetch (Node 18 or later), so it can set a timeout and read the status code:
async function checkLogin(req) {// A dual-stack server reports IPv4 visitors as ::ffff:198.51.100.24.const ip = (req.ip || "").replace(/^::ffff:/, "");let res;try {res = await fetch("https://decrypt.mcl.spur.us/api/v1/policy", {method: "POST",headers: {"Content-Type": "application/json",TOKEN: process.env.MONOCLE_SECRET_KEY,},body: JSON.stringify({ assessment: req.body.monocle || "", ip }),signal: AbortSignal.timeout(5000),});} catch (error) {// Timeout or network error: no decision. This example fails open and logs it.console.warn("Monocle policy unavailable", error);return "allow";}if (res.status === 400) {// Missing, altered or made-up assessment: treat it as a failed check.return "step-up";}if (!res.ok) {// 429 or 5xx: temporary. 401 or 404: check the Secret Key and policy.// No decision either way: fail open and log it.console.warn("Monocle policy unavailable", res.status);return "allow";}const decision = await res.json();console.info("Monocle decision", decision.decisionId, decision.reason);// Blocked sessions get step-up MFA here. Return "block" to refuse them instead.return decision.allowed ? "allow" : "step-up";}
A 400 means the assessment is missing, altered or made up, so the example treats it as a failed check. Any other error fails open and is logged, so a timeout or outage doesn't lock visitors out.
req.ip is the address Express sees. Behind a load balancer or proxy, set Express's trust proxy setting so it's the visitor's IP rather than the balancer's.
Log decisionId and reason with your own records, so you can match a login to Monocle's decision later.
FAQ
Can I read my policy configuration through the API?
No. The Policy API only evaluates assessments. View and change your policy on the Policies page.
Can I exempt specific users, such as a synthetic monitoring account?
Not in the policy. Policy rules are based on the network a session comes from, not on who the user is. To exempt specific users, check for them in your own backend before or after you call the Policy API.