Docs/Octet Browser/Getting Started

Quickstart

This quickstart takes you from your license to a verdict on your backend. It uses a small Node.js server with no dependencies, so you can see every step working before you change your own app. Each step links to the guide with the full detail.

Before you start

The quickstart needs all of these. Step 1 sets up the edge's client certificate and signing key, which takes about an hour the first time.

  • Your license token, emailed by Octet when you signed up. To sign up, see Credentials.
  • A portal user with the owner or admin role on the license, to issue the edge's client certificate and register its signing key.
  • A Linux host for the edge, with a public IP address and a DNS name such as octet.example.com. See Deploy the Edge.
  • A client certificate for the edge, issued in the Octet portal. See Get the edge's client certificate.
  • A signing key for the edge, which you create on the edge host and register in the Octet portal. See Register an edge signing key.
  • A read token, created in the Octet portal's Verdict API tab, under Read tokens. Your backend uses it to get challenges and to fetch verdicts. See Create a read token.
  • Node.js 18 or later on the machine that runs the example server.

1. Run the edge

Follow Deploy the Edge on your Linux host. For this quickstart, set ALLOWED_ORIGIN to include the example server's origin:

ALLOWED_ORIGIN=https://www.example.com,http://localhost:3000

Check that the edge is up and that Octet accepts your license:

curl -s https://octet.example.com/health
curl -s -X POST https://octet.example.com/v1/signals -H 'content-type: application/json' -d '{}'

The first returns {"ok":true,"role":"octet-edge"}. The second returns "reason":"unsealed_bundle", which means Octet accepted your license, your client certificate and your edge's signature.

2. Get the collector

In a new directory, download octet-collector.js and octet-collector.js.sri from the latest 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

3. Create the example server

Save this as server.mjs in the same directory:

// server.mjs: a minimal Octet Browser integration. Node.js 18 or later.
import { createServer } from 'node:http';
import { randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';

const EDGE_URL = process.env.OCTET_EDGE_URL; // for example https://octet.example.com
const READ_TOKEN = process.env.OCTET_READ_TOKEN;
const collector = readFileSync('octet-collector.js');
const sri = readFileSync('octet-collector.js.sri', 'utf8').trim();
const sessionRefs = new Map(); // browser session id -> sessionRef. Use your own session store.

const page = (sessionRef, challenge) => `<!doctype html>
<script src="/octet-collector.js" integrity="${sri}" crossorigin="anonymous"></script>
<form id="withdraw" method="post" action="/withdraw"><button>Withdraw</button></form>
<script>
  octet.start({
    apiUrl: ${JSON.stringify(EDGE_URL)},
    sessionRef: ${JSON.stringify(sessionRef)},
    challenge: ${JSON.stringify(challenge)},
  });
  document.getElementById('withdraw').addEventListener('submit', async (event) => {
    event.preventDefault();
    try { await octet.ready(); } catch (err) { console.warn('octet', err); }
    event.target.submit();
  });
</script>`;

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

async function fetchVerdict(sessionRef) {
  const url = `https://geo.octetproof.com/v1/verdict/${encodeURIComponent(sessionRef)}?waitMs=5000`;
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${READ_TOKEN}` },
    signal: AbortSignal.timeout(8000),
  });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`verdict fetch failed: ${res.status} ${await res.text()}`);
  return res.json();
}

createServer(async (req, res) => {
  if (req.method === 'GET' && req.url === '/octet-collector.js') {
    res.writeHead(200, { 'content-type': 'text/javascript' });
    return res.end(collector);
  }
  if (req.method === 'GET' && req.url === '/') {
    const sid = randomBytes(16).toString('hex');
    const sessionRef = randomBytes(32).toString('base64url');
    let challenge;
    try {
      challenge = await getChallenge(sessionRef);
    } catch (err) {
      res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
      return res.end(`could not get a challenge from Octet: ${err.message}\n`);
    }
    sessionRefs.set(sid, sessionRef);
    res.writeHead(200, {
      'content-type': 'text/html; charset=utf-8',
      'set-cookie': `sid=${sid}; HttpOnly; SameSite=Lax; Path=/`,
    });
    return res.end(page(sessionRef, challenge));
  }
  if (req.method === 'POST' && req.url === '/withdraw') {
    const sid = /(?:^|;\s*)sid=([0-9a-f]+)/.exec(req.headers.cookie ?? '')?.[1];
    const sessionRef = sessionRefs.get(sid);
    if (!sessionRef) {
      res.writeHead(400);
      return res.end('no session');
    }
    const verdict = await fetchVerdict(sessionRef);
    const masked = verdict?.alarm === 'medium' || verdict?.alarm === 'high';
    const decision = !verdict || masked ? 'require KYC' : 'allow';
    res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
    return res.end(`${decision}\n\n${JSON.stringify(verdict, null, 2)}\n`);
  }
  res.writeHead(404);
  res.end();
}).listen(3000, () => console.log('http://localhost:3000'));

The server gives each page view a new sessionRef, gets a one-time challenge for it from Octet, and keeps the sessionRef server-side against a cookie. It renders both into the page, and fetches the verdict when the form is submitted. Its policy requires KYC when there is no verdict or when alarm is medium or above.

4. Run it

OCTET_EDGE_URL=https://octet.example.com OCTET_READ_TOKEN=octet_read_REPLACE_WITH_YOUR_READ_TOKEN node server.mjs

Open http://localhost:3000 and select Withdraw. Use that exact address. The collector runs only on an HTTPS page or on localhost, so it refuses to run on any other http:// address, such as a LAN IP. The page shows the decision and the verdict:

allow

{
  "country": "GB",
  "confidence": 0.9,
  "alarm": "none",
  "deviceKey": "dk1_…",
  "token": "eyJhbGciOiJFZERTQSIs..."
}

Each challenge works for one collection. To run the check again, reload the page, which gets a new sessionRef and a new challenge.

Next

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