MisarMisar Docs
MisarMailMisarBlogMisarReachMisarPostMisarDevMisarCoderMisarSEOMisar PlatformMisar SSO
API Reference

Lead-Finder SSE Stream

Stream live progress of a lead-finder search job over Server-Sent Events — progress, found, complete, and error events, with a JSON snapshot for finished jobs.

Lead searches run asynchronously. Instead of polling GET /api/lead-finder/jobs/:jobId, you can subscribe to a Server-Sent Events (SSE) stream and receive events as the job runs.

Endpoint: GET https://api.misar.io/reach/api/lead-finder/jobs/{jobId}/stream · Auth: Authorization: Bearer mrk_… · Scope: leads:read

This is MisarReach's only streaming surface. There is no WebSocket API — all live progress is delivered over SSE.

Lifecycle

  1. Start a search: POST /api/lead-finder/search → returns { "jobId": "…" }.
  2. Open the stream for that jobId.
  3. Consume progress and found events until a terminal complete or error event arrives, then the connection closes.

The server polls the job record roughly every 1.5 seconds and emits an event only when something actually changed.

Already-finished jobs return JSON, not SSE

Two different response shapes

If the job is already done or failed when you connect, the endpoint does not open a stream. It returns an ordinary 200 application/json snapshot. Branch on the response Content-Type before you start parsing SSE frames.

200 application/json — job already terminal
{
  "status": "done",
  "progress": null,
  "total_found": 0,
  "error": null,
  "completed_at": "2026-08-04T09:31:12.004Z"
}

status is done or failed. progress is always null in this snapshot — it is not a percentage and it is not carried over from the live stream.

Live stream

When the job is still running, the response is 200 with:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Events

Prop

Type

Each frame uses the standard wire format — an event: line naming the event and a data: line carrying JSON:

wire format
event: progress
data: {"message":"Enriching 58 companies","total_found":58}

event: found
data: {"total":140}

event: complete
data: {"error":null,"total_found":140,"completed_at":"2026-08-04T09:31:12.004Z"}

There are no id: or retry: fields, no opening comment, and no keep-alive heartbeat. Set your own client-side idle timeout rather than relying on a ping.

Consuming the stream

Because browsers' native EventSource cannot send an Authorization header, authenticate with a fetch-based reader (server-side) or use an official SDK, which wraps this for you.

stream-job.ts
async function streamJob(jobId: string, apiKey: string) {
  const res = await fetch(
    `https://api.misar.io/reach/api/lead-finder/jobs/${jobId}/stream`,
    { headers: { Authorization: `Bearer ${apiKey}`, Accept: "text/event-stream" } },
  );

  // A finished job answers with a plain JSON snapshot, not a stream.
  if (!res.headers.get("content-type")?.includes("text/event-stream")) {
    return res.json();
  }

  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // SSE frames are separated by a blank line
    const frames = buffer.split("\n\n");
    buffer = frames.pop() ?? "";
    for (const frame of frames) {
      const event = frame.match(/^event:\s*(.+)$/m)?.[1] ?? "message";
      const data = JSON.parse(frame.match(/^data:\s*(.+)$/m)?.[1] ?? "{}");
      if (event === "progress") console.log(`${data.message} — ${data.total_found} found`);
      if (event === "found") console.log(`${data.total} leads so far`);
      if (event === "complete") return data;
      if (event === "error") throw new Error(data.error ?? "job failed");
    }
  }
}

With an SDK

Every SDK exposes the stream as an idiomatic iterator or callback:

python
job = client.leads.search({"query": "SaaS founders in Berlin", "useAI": True})
for evt in client.leads.stream_job(job["jobId"]):
    print(evt.event, evt.data)   # progress / found … then complete / error

See each SDK page for the exact streaming method name.

Operational notes

Client disconnects do not stop the stream loop

Ownership of the job is verified once, before the stream opens. After that the server polls until the job reaches a terminal state, even if the client has gone away. Close streams you no longer need, and do not open one per render.

  • A transient database read failure is swallowed and retried on the next poll — it does not produce an error event.
  • If the job record disappears mid-stream, the connection simply closes with no terminal event. Treat an SSE stream that ends without complete or error as indeterminate and re-check the job with GET /api/lead-finder/jobs/:jobId.
  • The stream is not rate limited.

Status codes

CodeMeaning
200Either the SSE stream (text/event-stream) or the JSON snapshot for a finished job
401Missing or invalid mrk_ key
403Missing leads:read scope
404Job not found or not owned by the caller

See Errors and the Lead Finder API.