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
- Start a search:
POST /api/lead-finder/search→ returns{ "jobId": "…" }. - Open the stream for that
jobId. - Consume
progressandfoundevents until a terminalcompleteorerrorevent 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.
{
"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-aliveEvents
Prop
Type
Each frame uses the standard wire format — an event: line naming the event and a data: line carrying JSON:
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.
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:
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 / errorSee 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
errorevent. - If the job record disappears mid-stream, the connection simply closes with no terminal event. Treat an SSE stream that ends without
completeorerroras indeterminate and re-check the job withGET /api/lead-finder/jobs/:jobId. - The stream is not rate limited.
Status codes
| Code | Meaning |
|---|---|
200 | Either the SSE stream (text/event-stream) or the JSON snapshot for a finished job |
401 | Missing or invalid mrk_ key |
403 | Missing leads:read scope |
404 | Job not found or not owned by the caller |
See Errors and the Lead Finder API.