# Quickstart

This quickstart takes you from an approved 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.

## What you need

- Your license token, emailed by Octet when your application was approved. To apply, see [Credentials](/docs/browser/integration/credentials/).
- A read token, created in the Octet portal's **Read tokens** tab.
- A Linux host for the edge, with a DNS name such as `octet.example.com`.
- Node.js 18 or later on the machine that runs the example server.

## 1. Run the edge

Follow [Deploy the Edge](/docs/browser/integration/deploy-edge/) on your Linux host. For this quickstart, set `ALLOWED_ORIGIN` to include the example server's origin:

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

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

```bash
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 your license and client certificate were accepted.

## 2. Get the collector

In a new directory, download `octet-collector.js` and `octet-collector.js.sri` from the [v1.3.0 release](https://github.com/octetproof/octet-browser/releases/tag/v1.3.0):

```bash
curl -fLO https://github.com/octetproof/octet-browser/releases/download/v1.3.0/octet-collector.js
curl -fLO https://github.com/octetproof/octet-browser/releases/download/v1.3.0/octet-collector.js.sri
```

## 3. Create the example server

Save this as `server.mjs` in the same directory:

```js
// 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) => `<!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)} });
  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 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');
    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));
  }
  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`, keeps it server-side against a cookie, 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

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

Open [http://localhost:3000](http://localhost:3000) and select **Withdraw**. The page shows the decision and the verdict:

```text
allow

{
  "country": "GB",
  "confidence": 0.9,
  "alarm": "none",
  "token": "eyJhbGciOiJFZERTQSIs..."
}
```

## Next

- Move each piece into your app: [Embed the Collector](/docs/browser/integration/embed-collector/) and [Fetch the Verdict](/docs/browser/integration/fetch-verdict/).
- Decide your policy: [Verdicts](/docs/browser/concepts/verdicts/).
- Verify and store the `token`: [Verify the Signed Token](/docs/browser/integration/verify-token/).
- Before launch, remove `http://localhost:3000` from `ALLOWED_ORIGIN` and work through the [Go-Live Checklist](/docs/browser/integration/go-live-checklist/).
