Predicta
Search
Docs
Ctrl KPredicta SearchGet an API key
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"]);