Guides
Full client examples
Start a search, print its events, and resume the stream after a disconnection.
Python, with httpx
search.py
import json
import time
import httpx
BASE_URL = "https://dev.predictasearch.com"
API_KEY = "<your API key>"
def search(query: str, query_type: str, networks: list[str]) -> None:
headers = {"x-api-key": API_KEY}
# read=60: a ping arrives at least every 15 s, so 60 s of silence means a dead connection.
with httpx.Client(timeout=httpx.Timeout(30, read=60)) as client:
r = client.post(
f"{BASE_URL}/api/v2/search",
headers=headers,
json={"query": query, "query_type": query_type, "networks": networks},
)
r.raise_for_status()
search_id = r.json()["search_id"]
last_id = None
deadline = None
while True:
stream_headers = {**headers, "Accept": "text/event-stream"}
if last_id is not None:
stream_headers["Last-Event-ID"] = last_id
try:
with client.stream(
"GET", f"{BASE_URL}/api/v2/search/{search_id}", headers=stream_headers
) as s:
s.raise_for_status()
deadline = None
event_id = event = None
for line in s.iter_lines():
if line.startswith("id:"):
event_id = line[3:].strip()
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
last_id = event_id
handle(event, json.loads(line[5:]))
if event in ("done", "failed"):
return
except httpx.TransportError:
deadline = deadline or time.monotonic() + 120
if time.monotonic() > deadline:
raise
time.sleep(3)
def handle(event: str, data: dict) -> None:
if event == "result":
print(f"{data['network']}: {len(data['data'])} profile(s)")
elif event == "progress":
print(f"{data['done']}/{data['total']} sources")
elif event in ("done", "failed"):
print(event, data)
search("johndoe", "username", ["all"])JavaScript, Node.js 18+
The browser's EventSource cannot send the x-api-key header. Read the stream with fetch, and keep the key on your server (Authentication).
search.mjs
const BASE_URL = "https://dev.predictasearch.com";
const API_KEY = process.env.API_KEY;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function search(query, queryType, networks) {
const post = await fetch(`${BASE_URL}/api/v2/search`, {
method: "POST",
headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ query, query_type: queryType, networks }),
});
if (!post.ok) throw new Error(`${post.status} ${await post.text()}`);
const { search_id } = await post.json();
let lastId = null;
let deadline = null;
for (;;) {
try {
const headers = { "x-api-key": API_KEY, Accept: "text/event-stream" };
if (lastId !== null) headers["Last-Event-ID"] = lastId;
const res = await fetch(`${BASE_URL}/api/v2/search/${search_id}`, { headers });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
deadline = null;
for await (const { id, event, data } of readEvents(res.body)) {
lastId = id;
handle(event, data);
if (event === "done" || event === "failed") return;
}
} catch (err) {
if (/^\d{3} /.test(err.message)) throw err; // an HTTP error: do not retry
deadline ??= Date.now() + 120_000;
if (Date.now() > deadline) throw err;
await sleep(3000);
}
}
}
// Splits the stream into events; a chunk can end in the middle of one.
async function* readEvents(body) {
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of body) {
buffer += decoder.decode(chunk, { stream: true });
let end;
while ((end = buffer.indexOf("\n\n")) !== -1) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const fields = {};
for (const line of block.split("\n")) {
const sep = line.indexOf(":");
if (sep > 0) fields[line.slice(0, sep)] = line.slice(sep + 1).trim();
}
if (fields.data) yield { id: fields.id, event: fields.event, data: JSON.parse(fields.data) };
}
}
}
function handle(event, data) {
if (event === "result") console.log(`${data.network}: ${data.data.length} profile(s)`);
else if (event === "progress") console.log(`${data.done}/${data.total} sources`);
else if (event === "done" || event === "failed") console.log(event, data);
}
search("johndoe", "username", ["all"]);