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.
Send your key in the X-RailCore-Key header. This operation requires the charts:read scope. See the authentication guide.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
job_id | path | string | Required | Job identifier returned by a submission endpoint. |
Base URL https://ir.railcore.tech/v1. Keep the key in an environment variable and call from a trusted server-side environment.
curl -N -sS \
-H "X-RailCore-Key: $RAILCORE_KEY" \
-H "Accept: text/event-stream" \
"https://ir.railcore.tech/v1/jobs/job_01J5Z7Y8N2K4M6P8R0T2V4X6Z8/stream"
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");
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")
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.
| Field | Type | Description |
|---|---|---|
event: progress | SSE | data contains status and progress. |
event: complete | SSE | data contains the final route-scan or hopping result. |
event: failed | SSE | data.error contains JOB_NOT_FOUND, CANCELLED, POLL_ERROR, or processing failure details. |
event: progress
data: {"status":"processing","progress":"Scanning trains..."}
event: complete
data: {"route_summary":{"trains_scanned":8}}
| Status | Code | Description | Retry |
|---|---|---|---|
| 400 | VALIDATION_ERROR | A required field is missing or invalid. | Not retryable |
| 401 | MISSING_API_KEY / INVALID_API_KEY | The credential is absent, unknown, disabled, or expired. | Not retryable |
| 403 | SCOPE_MISSING / PLAN_ENDPOINT_DISABLED | The key lacks the required scope or plan entitlement. | Not retryable |
| 402 | CREDITS_EXHAUSTED | The account balance cannot cover this operation. | Not retryable |
| 429 | RATE_LIMITED | Minute or daily request budget is exhausted. Honor Retry-After. | Retryable |
| 410 | API_VERSION_SUNSET | This API version has reached its announced sunset date. Migrate to a supported major version. | Not retryable |
| 502 | UPSTREAM_UNAVAILABLE | Live data could not be retrieved. Retry with backoff. | Retryable |
| 504 | UPSTREAM_TIMEOUT | The live-data request exceeded its deadline. Retry with backoff. | Retryable |
| 503 | METERING_UNAVAILABLE | Credit settlement is temporarily unavailable and no data was returned. | Retryable |
All RailCore Indian Railways API endpoints · Quickstart · Rate limits