Noesis
Noesis quickstart
Create a key for Noesis, get a stream token, stream a recorded call over the WebSocket and read the answers.
Stream a recorded call to Noesis and watch the answers arrive, exactly as they would on a live call.
1. Create an API key for Noesis
Sign in at platform.detectif.ai, open API keys and create a key that is allowed to use Noesis: noesis:detect to analyse calls, and noesis:read to read sessions and usage. The key is shown once; keep it in an environment variable:
export DETECTIF_API_KEY="dtf_live_..."2. Get a stream token
Code on your server can send the key in the Authorization header when it opens the WebSocket. Clients that cannot set headers, such as Twilio, use a stream token instead. This quickstart uses a token, so you see both steps:
curl -X POST https://api.noesis.detectif.ai/v1/detect/stream-tokens \
-H "Authorization: Bearer $DETECTIF_API_KEY"{"token": "ntk_4mZ0pQx7Lr2Vd9Kc1Ty6Wb3Ns8Hf5Ja0Ge2Ru7Xo4Il", "expires_at": "2026-10-19T10:05:00Z"}A token opens one call and expires after 5 minutes. The scripts below create their own token each time they run.
3. Stream a recording
A recording streamed in real time behaves like a live call. Python needs pip install websockets (version 14 or later); Node.js 18 or later needs npm install ws. Both read a 16-bit PCM mono WAV file.
# noesis_quickstart.py: stream a recording to Noesis as if it were a live call
import asyncio, json, os, sys, urllib.request, wave
import websockets
API = "https://api.noesis.detectif.ai"
STREAM = "wss://api.noesis.detectif.ai/v1/detect/stream"
def stream_token():
request = urllib.request.Request(
f"{API}/v1/detect/stream-tokens",
data=b"{}",
method="POST",
headers={"Authorization": f"Bearer {os.environ['DETECTIF_API_KEY']}", "Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)["token"]
async def main(path):
with wave.open(path) as w: # 16-bit PCM WAV, mono
rate, pcm = w.getframerate(), w.readframes(w.getnframes())
url = f"{STREAM}?token={stream_token()}&encoding=pcm_s16le&sample_rate={rate}&call_id=test-1"
async with websockets.connect(url) as ws:
async def events():
try:
async for raw in ws:
e = json.loads(raw)
if e["type"] == "classification":
print(f'{e["t"]:6.1f}s {e["label"]} ({e["category"]})')
elif e["type"] == "summary":
print("final:", e["label"])
elif e["type"] == "error":
print("error:", e)
except websockets.ConnectionClosed as closed:
print("closed:", closed.rcvd.code if closed.rcvd else "")
reader = asyncio.create_task(events())
step = rate * 2 // 50 # 20 ms of audio per frame
for i in range(0, len(pcm), step):
await ws.send(pcm[i:i + step])
await asyncio.sleep(0.02) # real time, like a live call
await ws.send(json.dumps({"type": "end"}))
await reader # ends when Noesis closes the socket
asyncio.run(main(sys.argv[1]))// noesis-quickstart.mjs: stream a recording to Noesis as if it were a live call
import fs from "node:fs";
import WebSocket from "ws";
const API = "https://api.noesis.detectif.ai";
const STREAM = "wss://api.noesis.detectif.ai/v1/detect/stream";
const wav = fs.readFileSync(process.argv[2]); // 16-bit PCM WAV, mono
let rate, pcm;
for (let pos = 12; pos + 8 <= wav.length; ) {
const id = wav.toString("ascii", pos, pos + 4), size = wav.readUInt32LE(pos + 4);
if (id === "fmt ") rate = wav.readUInt32LE(pos + 12);
if (id === "data") { pcm = wav.subarray(pos + 8, pos + 8 + size); break; }
pos += 8 + size + (size % 2);
}
const res = await fetch(`${API}/v1/detect/stream-tokens`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.DETECTIF_API_KEY}`, "Content-Type": "application/json" },
body: "{}",
});
if (!res.ok) { console.error(await res.text()); process.exit(1); }
const { token } = await res.json();
const ws = new WebSocket(`${STREAM}?token=${token}&encoding=pcm_s16le&sample_rate=${rate}&call_id=test-1`);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
ws.on("message", async (raw) => {
const e = JSON.parse(raw);
if (e.type === "classification") console.log(`${e.t.toFixed(1).padStart(6)}s ${e.label} (${e.category})`);
if (e.type === "summary") console.log("final:", e.label);
if (e.type === "error") console.error("error:", e);
if (e.type === "ready") {
const step = rate * 2 / 50; // 20 ms of audio per frame
for (let i = 0; i < pcm.length && ws.readyState === WebSocket.OPEN; i += step) {
ws.send(pcm.subarray(i, i + step));
await sleep(20); // real time, like a live call
}
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "end" }));
}
});
ws.on("close", (code) => { if (code !== 1000) console.log("closed:", code); });4. Run it and read the answers
Run python noesis_quickstart.py call.wav or node noesis-quickstart.mjs call.wav. The output looks like this:
3.2s ivr (machine)
13.0s hold (machine)
23.4s human (human)
final: humanIf the socket closes with an error, both scripts print the error event and the close code; see Errors.
- Each
classificationevent is an answer:labelsays who or what answered,categoryishuman,machineorunknown, andtis seconds into the call. See Answers. - A new
classificationarrives every time the answer changes. Act on the one with the highestseq. - After
{"type": "end"}you receive asummarywith the final answer, and the socket closes.
Convert other recordings first, for example ffmpeg -i call.mp3 -ac 1 -ar 16000 -sample_fmt s16 call.wav. Live calls need no conversion: see Stream protocol.
The call is recorded for 30 days. To try it without recording, create the token with {"record": false} as the request body. See Data and recording.
Next steps
- Twilio and the other integration guides.
- Stream protocol: every message and event.
- Recorded calls: upload a WAV file instead of streaming it.