Errors
Every error body is JSON. This page lists each status you can see, where you see it, and what to do.
In the browser
The collector reports errors by throwing or by rejecting the promise from ready(), settled() or verify():
| Error | Cause | What to do |
|---|---|---|
octet: `challenge` is required. … |
start() or verify() got no challenge. |
Get a challenge on your backend and pass it in. See Get a challenge. |
octet: this challenge was already used. … |
The collector already sent this challenge. ready() reruns the collection 90 seconds after the last one, or after a failed POST, and a second verify() with the same config does the same. |
Get a new challenge from your backend and call start() or verify() with it. |
octet: invalid apiUrl "…" |
apiUrl is not a URL. |
Set apiUrl to your edge's origin. |
octet: apiUrl must be https://… |
apiUrl is not https://. |
Use https://. http:// works only for localhost, 127.0.0.1 and [::1]. |
octet: the collector needs a secure context (https) for crypto.subtle |
The page isn't served over HTTPS, so the browser offers no WebCrypto, which the collector needs to encrypt the collection. | Serve the page over HTTPS. http:// works only for localhost, 127.0.0.1 and [::1]. |
octet: call start() before ready() |
ready() ran before start(). |
Call start() at page load. |
octet: call start() before settled() |
settled() ran before start(). |
Call start() at page load. |
octet: unknown mode "…" |
mode is not 'full', 'lite' or 'passive'. |
Fix the value, or leave mode out to get full. |
signal report failed: <status> |
Your edge answered with an error status. | Look up the status under From your edge. |
TypeError from fetch |
The browser could not complete the POST. Usually CORS, DNS, TLS, or the edge is down. | Check the browser console, ALLOWED_ORIGIN, and the edge's /health. |
AbortError, or your own abort reason |
Your code aborted the collection. | None. If the abort came before the POST reached your edge, no verdict was produced. |
Blocked or failed measurements never cause an error. If the WebSocket to your edge fails, the collector sends what it has and results are weaker. The usual causes are a Content Security Policy without wss:// for your edge, and networks that block WebSockets.
From your edge
These are the responses to POST /v1/signals, which the browser sees:
| Status | Body | Cause | What to do |
|---|---|---|---|
200 |
{"ok":true} |
Octet accepted the session. | None. |
400 |
{"error":"read_failed"} |
The edge could not read the request body. | Retry. |
405 |
{"error":"method_not_allowed"} |
A method other than POST or OPTIONS. |
Use POST. |
502 |
{"error":"octet_unreachable"} |
The edge could not reach Octet. | Check OCTET_URL, outbound TCP 443, and DNS on the edge host. If you set OCTET_CA_FILE, check that Octet's server certificate chains to it. The edge's log has the detail. |
503 |
{"error":"edge_signing_failed"} |
From v1.7.0. The edge couldn't sign the session, for example because its cloud key service was unreachable. It sent nothing to Octet. | Check the key service and the edge's access to it. The edge's log has the detail. See Where to keep the signing key. |
| Octet's status | {"error":"octet_rejected","reason":"<reason>","status":<status>} |
Octet refused the session. The edge passes Octet's status and reason code through. | Look up the reason below. |
When Octet refuses a session, the edge logs one line with the status and reason, with no end-user data. reason is unknown when Octet's reply carries no plain reason code.
Reasons Octet gives your edge
| Status | Reason | Cause | What to do |
|---|---|---|---|
401 |
missing_license |
LICENSE is not set on the edge. |
Set LICENSE and restart. |
401 |
bad_vendor_prefix, malformed_token, bad_footer, bad_payload, bad_signature |
LICENSE is not a valid license token. It may be truncated, or be a read token. |
Copy the license token again from Octet's email. |
401 |
wrong_typ, wrong_issuer, product_not_licensed, platform_not_licensed |
The token is valid but not an Octet Browser license. | Use the license token issued for Octet Browser. |
401 |
expired, not_yet_valid, missing_exp, missing_iat_and_nbf |
The license token is outside its validity period, or the edge host's clock is wrong. | Check the host's clock. If it is right, ask Octet for a new license token. |
401 |
revoked |
Octet revoked the license token. | Deploy the replacement Octet sent you, or contact Octet. |
401 |
unknown, or any reason not listed on this page |
Octet could not check the license. | Retry. If it persists, contact developer@octetproof.com. |
401 |
client_cert_required |
The edge presented no client certificate, or one Octet didn't issue. | Check EDGE_CLIENT_CERT_FILE and EDGE_CLIENT_KEY_FILE, and that the certificate hasn't expired. See Deploy the Edge. |
403 |
client_cert_licence_mismatch |
The client certificate was issued for a different license than the one in LICENSE. |
Use a certificate issued for this license, from its Edge configuration tab in the Octet portal, under Certificates. |
403 |
client_cert_revoked |
The client certificate was revoked in the Octet portal. | Issue a new certificate in the Edge configuration tab, under Certificates, install it and restart the edge. See Credentials. |
401 |
edge_signature_required |
Octet requires a signature from your edge, and the session had none, because EDGE_SIGNING_KEY is unset. |
Set EDGE_SIGNING_KEY, restart the edge and register its key. See Deploy the Edge. |
401 |
edge_key_unknown |
Octet requires a signature from your edge, and the key that signed the session isn't registered for this license, or was revoked. | Register the kid and public key from the edge's startup log in the Edge configuration tab, under Signing keys. See Credentials. |
401 |
edge_signature_stale |
The signature's time is out of range, usually because the edge host's clock is wrong. | Check the edge host's clock, for example that NTP is running. |
401 |
edge_signature_mismatch |
The session Octet received differs from the one your edge signed. Something between your edge and Octet changed the request. | Make sure nothing between the edge and Octet, such as an outbound proxy, rewrites the request. |
401 |
edge_signature_invalid |
The edge's signature doesn't verify, or its key is registered for a different license. | Update the edge to the latest release and restart it. If it persists, contact developer@octetproof.com. |
401 |
challenge_required |
The collection carries no challenge. A collector older than v1.5.0 gets this. | Update the collector, and pass it a challenge from your backend. See Get a challenge. |
401 |
challenge_invalid |
The challenge is not one Octet issued, or it was changed. | Pass the challenge from POST /v1/challenge to the page unchanged. |
401 |
challenge_expired |
The challenge is more than 10 minutes old. | Get a new challenge and call start() again. Get the challenge when you render the page, not earlier. |
401 |
challenge_mismatch |
The challenge was issued for a different sessionRef, or under a different license from the edge's. |
Request the challenge with the same sessionRef you pass to start(), with a read token from the same license as the edge's LICENSE. |
409 |
challenge_spent |
Octet already accepted a collection with this challenge. | Get a new challenge for each collection. |
400 |
invalid_json |
The body is not JSON. | Send the collector's POST unchanged. |
400 |
unsealed_bundle |
The body isn't a sealed collection. A collector older than v1.3.0 gets this, and so does an empty {} body. |
Update the collector. |
400 |
unknown_seal_key |
The collector is sealed to a key Octet no longer accepts, because the collector is too old. | Update the collector. See Release Notes. |
400 |
bad_seal, bad_signature, bad_payload |
The sealed collection doesn't open, or its contents don't check out. This usually means a proxy or a script on the page changed the body. | Send the collector's POST unchanged. |
400 |
missing_fields |
The sealed collection opened, but isn't a complete collection. | Send the collector's POST unchanged. |
409 |
replayed_bundle |
Octet accepted this collection recently, through this edge or another. A copy of a collection is refused. | None for the original session, whose verdict stands. |
413 |
payload_too_large |
The body is larger than Octet accepts. | Send the collector's POST unchanged. |
A 401 with bad_signature or bad_payload concerns the license token, and a 400 with the same reason concerns the collection.
From POST /v1/challenge
These are the responses to POST /v1/challenge, which your backend sees. If you send the request through your edge, the edge returns Octet's response unchanged.
| Status | Body | Cause | What to do |
|---|---|---|---|
200 |
{"challenge":"…","exp":…} |
Octet issued a challenge. | Pass challenge to the page. |
400 |
{"error":"invalid_json"} |
The body is not JSON. | Send {"sessionRef":"…"} with content-type: application/json. |
400 |
{"error":"invalid_session_ref"} |
sessionRef is missing, not a string, empty, or longer than 256 characters. |
Send the sessionRef you created for the page view. |
400 |
{"error":"invalid_client_ip"} |
clientIp is present but isn't an IPv4 or IPv6 address. |
Send the user's IP address as a string, or leave clientIp out. |
401 |
The same bodies as for GET /v1/verdict |
The read token was refused. | See From the Octet API. |
413 |
{"error":"payload_too_large"} |
The body is larger than 4 KB. | Send only sessionRef and clientIp. |
429 |
{"error":"rate_limited"} |
Your license requested too many challenges in one minute. The limit is far above any real page-view rate. | Retry after a short wait. If it persists, check that your backend requests one challenge per page view, and that your read token hasn't leaked. |
Through your edge, you can also get the edge's own errors: 405 with method_not_allowed for a method other than POST, 400 with read_failed, and 502 with octet_unreachable. See From your edge.
From the Octet API
These are the responses to GET /v1/verdict/{sessionRef}, which your backend sees:
| Status | Body | Cause | What to do |
|---|---|---|---|
200 |
The verdict | See Verdict Reference. | Apply your policy. |
400 |
{"error":"stage_not_allowed"} |
You sent stage, and your license doesn't have the early verdict. |
Leave out stage, or ask Octet for the early verdict. |
400 |
{"error":"invalid_stage"} |
stage is not provisional or final. |
Send provisional or final. |
404 |
{"status":"pending","ref":"…"} |
No verdict for this sessionRef yet (with the early verdict, no final verdict yet unless you asked for stage=provisional), the verdict is more than 2 minutes old, the sessionRef is wrong, or the session arrived under another license. |
Retry with waitMs. If it persists, check that the page passes the same sessionRef to start(). |
401 |
{"error":"unauthorized"} |
No Authorization: Bearer octet_read_… header. A license token sent as the bearer also gets this. |
Send your read token. |
401 |
{"error":"expired"} |
The read token has expired. | Create a new one. See Credentials. |
401 |
{"error":"revoked"} |
The read token, or its license, has been revoked. | Create a new one, or contact Octet if the license was revoked. |
401 |
{"error":"not_yet_valid"} |
The read token isn't valid yet. | Check your server's clock. |
401 |
malformed_token, bad_footer, bad_payload, bad_signature, unknown_kid:… |
The read token is damaged or incomplete. | Copy it again, or create a new one. |
401 |
wrong_typ, wrong_issuer, wrong_audience, missing_scope, wrong_env, missing_lid, missing_exp, missing_iat_and_nbf, unsupported_schema:… |
The token is not an Octet Browser read token. | Create a read token under Read tokens in the Verdict API tab of your Octet Browser license. |
401 |
Any reason not listed on this page | Octet could not check the read token. | Retry. If it persists, contact developer@octetproof.com. |
503 |
{"error":"verdict_signing_unavailable"} |
Octet could not sign the verdict. | Retry after a short wait. Octet holds the verdict for 2 minutes. |
The 401 reason codes from GET /v1/verdict appear exactly as listed, and some carry a suffix after a colon.
From GET /v1/device/{deviceKey}
These are the responses to GET /v1/device/{deviceKey}, which returns one browser's device record:
| Status | Body | Cause | What to do |
|---|---|---|---|
200 |
The record | Your license has a record under this deviceKey. See below. |
Pass it on to your user, for example to answer an access request. |
400 |
{"error":"invalid_device_key"} |
The path doesn't hold a deviceKey in the form <prefix>_<value>. |
Send the deviceKey exactly as the verdict carried it. |
401 |
The same bodies as for GET /v1/verdict |
The read token was refused. | See From the Octet API. |
404 |
{"error":"device_record_not_found"} |
Your license has no record under this deviceKey, for example because it was deleted. A deviceKey from another license gets the same answer. |
Nothing. There is no record for this browser on your license. |
404 |
{"error":"device_records_not_configured"} |
This Octet server keeps no device records. Until device records start for your license (from 1 December 2026), every request gets this. | Nothing. There is no record to return. |
The response carries Cache-Control: no-store. A 200 body has these fields. Times are ISO 8601 in UTC, and a value Octet doesn't have is null.
| Field | Description |
|---|---|
lid |
Your license identifier. |
deviceKey |
The deviceKey the record is held under. |
firstSeen, lastSeen |
When the browser was first and last seen. |
privacyProtection |
The privacy protection recognised in the browser, for example brave, or null. |
maxProcessorCount |
The highest processor count the browser reported, or null. |
canvasHashes, audioHashes |
Up to 20 each, most often seen first. Each has hash, count and lastSeen. hash is keyed for your license: it matches nothing outside your license's records. |
sessions |
Up to 200, oldest first. Each has time, verdict (country, confidence, alarm), ipCountry (the country of the user's IP address, never the address), checks (one true or false for each of Octet's server-side checks) and networkTag (the keyed tag of the user's network). |
Field names don't change. Later versions may add fields, so ignore fields you don't recognise.
From DELETE /v1/device/{deviceKey}
These are the responses to DELETE /v1/device/{deviceKey}, which deletes one browser's device record:
| Status | Body | Cause | What to do |
|---|---|---|---|
200 |
{"deleted":true} |
Octet deleted the record. | Nothing. |
200 |
{"deleted":false} |
Your license has no record under this deviceKey, for example because it was already deleted. |
Nothing. |
400 |
{"error":"invalid_device_key"} |
The path doesn't hold a deviceKey in the form <prefix>_<value>. |
Send the deviceKey exactly as the verdict carried it. |
401 |
The same bodies as for GET /v1/verdict |
The read token was refused. | See From the Octet API. |
404 |
{"error":"device_records_not_configured"} |
This Octet server keeps no device records. Until device records start for your license (from 1 December 2026), every request gets this. | There is no record to delete. |
Key sets
Each of these returns 200 with a JSON document:
URL on https://geo.octetproof.com |
What it is |
|---|---|
/.well-known/browser-verdict-jwks.json |
The verdict key set. Cache it for up to 5 minutes. See Verify the Signed Token. |
/.well-known/browser-verdict-jwks-archive.json |
Every key Octet has signed verdicts with. See Keep Verdicts for Audit. |
/.well-known/browser-challenge-jwks.json |
The challenge key set. See The chain. |
/.well-known/octet-browser-manifest.json.jws |
The manifest signed by Octet's root keys. See Check a key against Octet's root keys. |