Embed the Collector

The collector is a JavaScript file you serve from your own site. Your backend gets a one-time challenge from Octet for each page view. The page calls start() with it when the page loads, and ready() when the user acts. This guide assumes your edge runs at https://octet.example.com. See Deploy the Edge.

1. Install the collector

Every Octet Browser file is a release asset in the public repository octetproof/octet-browser. No registry account or access token is needed. The commands in this guide download the latest release. Note its version number, and install the collector and the edge from the same release.

As a script tag

Download the script and its Subresource Integrity hash from the release:

curl -fLO https://github.com/octetproof/octet-browser/releases/latest/download/octet-collector.js
curl -fLO https://github.com/octetproof/octet-browser/releases/latest/download/octet-collector.js.sri

Serve octet-collector.js from your own origin, and put the contents of octet-collector.js.sri in the integrity attribute:

<script src="/static/octet-collector.js"
        integrity="sha384-REPLACE_WITH_THE_CONTENTS_OF_octet-collector.js.sri"
        crossorigin="anonymous"></script>

Serving the file yourself keeps the collector first-party and covered by your own script-src. Don't point the src at the GitHub release URL. GitHub serves release assets as downloads, without the headers a browser needs to run a cross-origin script with an integrity check, so the script won't load.

The script defines a global octet object with the functions octet.start(), octet.ready(), octet.settled() and octet.verify().

With npm or a bundler

Install the package straight from the release. The package file name carries the version, so set VERSION to the latest release's version number, without the v:

VERSION=REPLACE_WITH_THE_LATEST_VERSION
npm install "https://github.com/octetproof/octet-browser/releases/download/v$VERSION/octetproof-collector-$VERSION.tgz"

npm records the URL in your package.json, so every install gets the same version. Then import the functions:

import { start, ready } from '@octetproof/collector';

The package includes TypeScript types.

2. Create a sessionRef and get a challenge

For each page view, your backend creates a sessionRef and gets a challenge for it from Octet. The collector needs both.

Create the sessionRef

Each page view needs its own sessionRef. Your backend fetches the verdict with it, so it must be unguessable and must never be reused. Generate at least 128 random bits and encode them URL-safe:

// Node.js
import { randomBytes } from 'node:crypto';
const sessionRef = randomBytes(32).toString('base64url');

Store the sessionRef in the user's server-side session. The rules for session references, and why they matter, are in The session reference.

Get a challenge

A challenge is a one-time token from Octet, bound to your license and to one sessionRef. Octet accepts a collection only when it carries a valid challenge. Your backend requests one with your read token:

curl -s -X POST https://geo.octetproof.com/v1/challenge \
  -H "Authorization: Bearer $OCTET_READ_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"sessionRef":"SESSION_REF_FROM_YOUR_BACKEND"}'
Field Required Description
sessionRef Yes The sessionRef you created for this page view. A string of 1 to 256 characters.
clientIp No The user's IP address as your backend saw it. If your backend runs behind a proxy or load balancer, send the client address it reports, for example from the X-Forwarded-For entry your own proxy added. With it, the verdict token carries clientNetMatch. See Check the user's network.

The response is:

{ "challenge": "v4.public.…", "exp": 1791000600 }

exp is when the challenge expires, in Unix seconds. A challenge is valid for 10 minutes and works for one collection. Treat it as an opaque string and pass it to the page unchanged.

If your backend can't reach geo.octetproof.com, send the same request to your edge at https://octet.example.com/v1/challenge. The edge forwards it to Octet unchanged, with your Authorization header, and returns Octet's response.

In Node.js:

export async function getChallenge(sessionRef, clientIp) {
  const res = await fetch('https://geo.octetproof.com/v1/challenge', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.OCTET_READ_TOKEN}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ sessionRef, ...(clientIp ? { clientIp } : {}) }),
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error(`octet challenge failed: ${res.status} ${await res.text()}`);
  return (await res.json()).challenge;
}

Render the sessionRef and the challenge into the page. To check later that a verdict answers this challenge, store the challenge with the sessionRef. See The chain.

Every error this request can return is in Errors.

3. Start at page load, wait at the moment of action

Call start() as soon as the page loads, so the result is ready by the time the user acts. Call ready() when the user acts:

<script>
  octet.start({
    apiUrl: 'https://octet.example.com',
    sessionRef: 'SESSION_REF_FROM_YOUR_BACKEND',
    challenge: 'CHALLENGE_FROM_YOUR_BACKEND',
  });

  document.querySelector('#withdraw-form').addEventListener('submit', async (event) => {
    event.preventDefault();
    try {
      await octet.ready();
    } catch (err) {
      // Collection failed. Submit anyway, and your backend will get a 404 when it fetches the verdict.
      console.warn('octet', err);
    }
    event.target.submit();
  });
</script>

ready() resolves once your edge has passed the session to Octet, which means your backend can now fetch the verdict. Fetch it within 30 seconds of ready() resolving. When ready() resolves, the verdict can already be up to 90 seconds old, and verdicts expire at 2 minutes. See Fetch the Verdict.

Each challenge works for one collection. ready() reuses the result of the last collection for 90 seconds. After that, or after a collection whose POST to your edge failed, ready() rejects with octet: this challenge was already used. To collect again, get a new challenge from your backend and call start() with it. To start over with a new sessionRef, get a challenge for it and call start() again.

ready() resolves with { response, issuedAtMs, expiresAtMs, elapsedMs }. response is { "ok": true }. It never contains the verdict. See Collector API.

4. Choose a mode

The mode option sets how much network measurement the collector does. The default is full.

Mode What it does Network requests beyond your edge Typical time to ready()
full Everything, for the strongest results HTTPS and UDP 3478 to three Octet network hosts Up to about 1.5 s, or up to about 4.5 s on some connections
lite Skips the HTTPS requests to the Octet network hosts UDP 3478 to three Octet network hosts Up to about 1 s, or up to about 3 s on some connections
passive No network measurement and no WebRTC None The time of one request to your edge

In every mode the collector sends one POST to your edge. In full and lite it also opens a WebSocket to your edge. Every host the collector contacts is run by you or by Octet.

lite and passive give weaker results: more masked sessions come back as none or low.

octet.start({ apiUrl: 'https://octet.example.com', sessionRef, challenge, mode: 'lite' });

Each mode's hosts, ports and CSP entries are in Network and CSP.

5. Rules the collector enforces

  • apiUrl must be https://. On any other scheme, start() throws and verify() rejects at once. The only exception is http://localhost, http://127.0.0.1 or http://[::1], for local development.
  • The WebSocket URL follows apiUrl. It is wss:// on the same host, at /v1/ws.
  • challenge is required. Without it, start() throws and verify() rejects at once. If a challenge the collector has already sent is used again, ready() or verify() rejects before anything is sent.
  • Only the POST to your edge must succeed. A measurement that fails or is blocked is left out, and the collector sends what it has. Of the collector's requests, only a failed POST to your edge makes ready() or verify() reject.

Cancel a collection

Pass an AbortSignal to stop a collection, for example when the user leaves the page:

const controller = new AbortController();
octet.start({ apiUrl: 'https://octet.example.com', sessionRef, challenge, signal: controller.signal });

// later
controller.abort();

Aborting stops the collection and cancels the POST to your edge if it has started. ready() and verify() then reject with an AbortError, or with your own reason if you passed one to abort(). If the abort comes before the POST reaches your edge, no verdict is produced for that run.

One-shot collection

verify() runs one collection and resolves with your edge's reply. Use it where there is no separate moment of action, for example on a sign-in page:

await octet.verify({ apiUrl: 'https://octet.example.com', sessionRef, challenge });

verify() takes the same options as start(). Each call needs a new challenge.

TermsPrivacy© 2026 Understone, Inc. d/b/a Octet