Stream job progress

Receive progress and terminal state over SSE.

GET /v1/jobs/{job_id}/stream

Scope charts:read · Credits 0 · Plans Scale and Enterprise · text/event-stream

Emits a progress event every two seconds, then one complete or failed event before closing the connection.

Authentication

Send your key in the X-RailCore-Key header. This operation requires the charts:read scope. See the authentication guide.

Request parameters

NameInTypeRequiredDescription
job_idpathstringRequiredJob identifier returned by a submission endpoint.

Code examples

Base URL https://ir.railcore.tech/v1. Keep the key in an environment variable and call from a trusted server-side environment.

cURL

curl -N -sS \
  -H "X-RailCore-Key: $RAILCORE_KEY" \
  -H "Accept: text/event-stream" \
  "https://ir.railcore.tech/v1/jobs/job_01J5Z7Y8N2K4M6P8R0T2V4X6Z8/stream"

JavaScript

const streamUrl = "https://ir.railcore.tech/v1/jobs/job_01J5Z7Y8N2K4M6P8R0T2V4X6Z8/stream";
const terminalEvents = new Set(["complete", "failed"]);
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
let terminalReceived = false;

for (let attempt = 0; attempt < 5 && !terminalReceived; attempt += 1) {
  try {
    const response = await fetch(streamUrl, {
      headers: {
        "X-RailCore-Key": process.env.RAILCORE_KEY,
        Accept: "text/event-stream",
      },
    });
    if (!response.ok || !response.body) {
      throw new Error(`SSE request failed: ${response.status}`);
    }

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

    while (!terminalReceived) {
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
      const frames = buffer.split("\n\n");
      buffer = frames.pop() ?? "";

      for (const frame of frames) {
        const lines = frame.split("\n");
        const event = lines.find((line) => line.startsWith("event:"))?.slice(6).trim() ?? "message";
        const dataText = lines
          .filter((line) => line.startsWith("data:"))
          .map((line) => line.slice(5).trimStart())
          .join("\n");
        if (!dataText) continue;

        let data = dataText;
        try { data = JSON.parse(dataText); } catch { /* Keep non-JSON event data as text. */ }
        console.log(event, data);
        if (terminalEvents.has(event)) {
          terminalReceived = true;
          break;
        }
      }
    }
  } catch (error) {
    console.error(error instanceof Error ? error.message : error);
  }

  if (!terminalReceived && attempt < 4) {
    const delay = Math.min(1_000 * 2 ** attempt, 8_000);
    console.warn(`Stream closed before a terminal event; reconnecting in ${delay} ms...`);
    await sleep(delay);
  }
}

if (!terminalReceived) throw new Error("Stream ended without a complete or failed event");

Python

import json
import os
import time
import requests

stream_url = "https://ir.railcore.tech/v1/jobs/job_01J5Z7Y8N2K4M6P8R0T2V4X6Z8/stream"
terminal_events = {"complete", "failed"}
terminal_received = False

for attempt in range(5):
    try:
        with requests.get(
            stream_url,
            headers={
                "X-RailCore-Key": os.environ["RAILCORE_KEY"],
                "Accept": "text/event-stream",
            },
            stream=True,
            timeout=(10, 60),
        ) as response:
            response.raise_for_status()
            event_name = "message"
            data_lines = []

            for line in response.iter_lines(decode_unicode=True):
                if line == "":
                    if data_lines:
                        data_text = "\n".join(data_lines)
                        try:
                            data = json.loads(data_text)
                        except json.JSONDecodeError:
                            data = data_text
                        print(event_name, data, flush=True)
                        if event_name in terminal_events:
                            terminal_received = True
                            break
                    event_name = "message"
                    data_lines = []
                elif line.startswith("event:"):
                    event_name = line[6:].strip()
                elif line.startswith("data:"):
                    data_lines.append(line[5:].lstrip())
    except requests.RequestException as error:
        print(f"Stream error: {error}", flush=True)

    if terminal_received:
        break
    if attempt < 4:
        delay = min(2 ** attempt, 8)
        print(f"Stream closed before a terminal event; reconnecting in {delay}s...", flush=True)
        time.sleep(delay)

if not terminal_received:
    raise RuntimeError("Stream ended without a complete or failed event")

Rate limits and credits

This call costs 0 credits and is available on Scale and Enterprise. Per-minute and daily budgets, and the headers reporting remaining quota, are documented in rate limits and credits and billing.

Response fields

FieldTypeDescription
event: progressSSEdata contains status and progress.
event: completeSSEdata contains the final route-scan or hopping result.
event: failedSSEdata.error contains JOB_NOT_FOUND, CANCELLED, POLL_ERROR, or processing failure details.

Example response

event: progress
data: {"status":"processing","progress":"Scanning trains..."}

event: complete
data: {"route_summary":{"trains_scanned":8}}

Errors

StatusCodeDescriptionRetry
400VALIDATION_ERRORA required field is missing or invalid.Not retryable
401MISSING_API_KEY / INVALID_API_KEYThe credential is absent, unknown, disabled, or expired.Not retryable
403SCOPE_MISSING / PLAN_ENDPOINT_DISABLEDThe key lacks the required scope or plan entitlement.Not retryable
402CREDITS_EXHAUSTEDThe account balance cannot cover this operation.Not retryable
429RATE_LIMITEDMinute or daily request budget is exhausted. Honor Retry-After.Retryable
410API_VERSION_SUNSETThis API version has reached its announced sunset date. Migrate to a supported major version.Not retryable
502UPSTREAM_UNAVAILABLELive data could not be retrieved. Retry with backoff.Retryable
504UPSTREAM_TIMEOUTThe live-data request exceeded its deadline. Retry with backoff.Retryable
503METERING_UNAVAILABLECredit settlement is temporarily unavailable and no data was returned.Retryable

Related Jobs endpoints

All RailCore Indian Railways API endpoints · Quickstart · Rate limits