Review Season — API

Notes in, a review-ready document out.

API tokens Open the app

Write the review from your own tools

Send the raw material of one performance-review task — scattered accomplishments, notes on a direct report, or a whole team roster — and get back one JSON object shaped for the mode you asked for: a self-assessment with situation/contribution/impact for every accomplishment, a manager review with a rating recommendation and a development plan, or a calibration packet with a rating-distribution check against your target bands. Every claim in the response traces back to what you sent; thin evidence becomes an open question, never an invented fact. Everything goes through the SkillSafe App API — plain JSON over HTTPS — so you can wire it to your HRIS export, a review-cycle bot, or a script that turns a roster CSV into a calibration packet on a schedule. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api, app slug review-season. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"data": …} on success, {"error": {"code", "message"}} on failure. The document is produced by the gpt-terra model. Estimates are free; runs are metered against your credit balance. There is a single run task per mode — one paste of notes (or a roster) in, one document out, no follow-up calls and no session state to carry.

StatusMeaning
401Missing or expired token — create a new session.
402Not enough credits — top up at skillsafe.ai/account/credits.
403The token isn't allowed to do this (e.g. a guest submitting a very large paste).
404Unknown job or record id.
5xxTransient platform error — retry with backoff.

Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.

Step 0 — A tiny client

Every task below is a single HTTP call, so start with a short helper that adds the auth header, sends JSON and unwraps the data envelope. The later steps reuse it.

export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN"      # see step 1

# every call looks like:
#   curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"  # see step 1 — read it from your shell environment in real code

def api(method, path, body=None, **headers):
    res = requests.request(method, API + path, json=body,
                           headers={"Authorization": f"Bearer {TOKEN}", **headers})
    payload = res.json()
    if not res.ok:
        raise RuntimeError(payload.get("error", {}).get("message", res.reason))
    return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your shell environment in real code

async function api(method, path, body, extraHeaders = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
  return json.data;
}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

const API = "https://api.skillsafe.ai/v1/app-api"

var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1

func call(method, path string, body, out any) error {
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req, _ := http.NewRequest(method, API+path, &buf)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var env struct {
		Data  json.RawMessage `json:"data"`
		Error *struct{ Message string `json:"message"` } `json:"error"`
	}
	json.NewDecoder(res.Body).Decode(&env)
	if res.StatusCode >= 400 {
		return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SkillSafe {
    static final String API = "https://api.skillsafe.ai/v1/app-api";
    static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String api(String method, String path, String jsonBody) throws Exception {
        var req = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + TOKEN)
            .header("Content-Type", "application/json")
            .method(method, jsonBody == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();
        var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException(res.body());
        return res.body(); // envelope: {"data": …}
    }
}
require "net/http"
require "json"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1

def api(method, path, body = nil)
  uri = URI(API + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1

function api(string $method, string $path, ?array $body = null): mixed {
    global $TOKEN;
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer $TOKEN",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    $status  = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new Exception($payload["error"]["message"] ?? "HTTP $status");
    }
    return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;

static class SkillSafe
{
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    static readonly HttpClient Http = new();

    static SkillSafe() =>
        Http.DefaultRequestHeaders.Authorization =
            new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1

    public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(method, Api + path);
        if (body != null) req.Content = JsonContent.Create(body);
        var res = await Http.SendAsync(req);
        var json = await res.Content.ReadFromJsonAsync<JsonElement>();
        if (!res.IsSuccessStatusCode)
            throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
        return json.GetProperty("data");
    }
}

Step 1 — Get a token

POST /guest

A guest token lets you check balances and estimate costs for free. For metered writing runs billed to your own account, use your personal token: open the token page, sign in with SkillSafe, and press Copy shell export — it puts export SKILLSAFE_TOKEN="…" on your clipboard, which every example below reads. Treat the token like a password: it can spend your credits. For fully headless scripts, POST /guest mints a guest token with no browser involved.

curl -s -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"review-season"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "review-season"})["token"]
const { token } = await api("POST", "/guest", { slug: "review-season" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "review-season"}, &guest)
String envelope = api("POST", "/guest", """
    {"slug":"review-season"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "review-season" })["token"]
$token = api("POST", "/guest", ["slug" => "review-season"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
    new { slug = "review-season" });
var token = guest.GetProperty("token").GetString();

The app stores this browser's token under the localStorage key skillsafe_app_token:review-season, on the app's own origin. The token page reads and manages it for you — you never need to open developer tools.

Step 2 — Check who you are and your balance

GET /me

Returns subject_type ("user" or "guest"), subject_id and your credits balance. Check this before sending a large roster.

curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int64  `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");

Step 3 — Estimate the cost

POST /estimate

Send exactly the input you would send to /run; the response's hold_credits is the worst-case cost. Nothing is charged and no job is created.

Input fieldTypeNotes
modestring, requiredself | manager | calibration. Selects which field group of the response is populated.
review_periodstringFree text, e.g. "H2 2026". May be empty.
subject_namestring, requiredWho the document is about — "You" for a self-assessment, the direct report's name for a manager review, or the team's name for calibration.
subject_rolestring, optionalRole and level, e.g. "Senior Backend Engineer, L4". Not used in calibration mode.
notesstring, requiredThe raw pasted material. For self/manager this is the accomplishments and observations; for calibration this is context that doesn't fit a single roster row (budget constraints, reorgs, partial-year notes). Up to ~20,000 characters; a longer paste should be clipped with a marker showing what was cut, both ends kept.
prior_goalsstring, optionalGoals set last cycle, one per line. Ignored for calibration.
roster_rowsarray, calibration only{name, role, level, tenure, proposed_rating, notes} per person. Carried through unchanged in the response's team_overview — never dropped, never invented.
target_bandsobject, calibration only{exceeds: [min, max], meets: [min, max], below: [min, max]} as percentages. Defaults used by the web UI: [15,20] / [60,70] / [10,15].
prescan_factsobject, optionalWhat a client-side scan mechanically found: accomplishment counts, claims with no metric, roster counts and rating gaps, each as a {id, label} flag. Every flag id you send comes back in coverage_check. API callers may omit this or send empty counts.
current_datetimestringISO 8601. Used only for context; the document itself is not date-sensitive beyond the review period you name.
jq -n '{
  mode: "manager",
  review_period: "H2 2026",
  subject_name: "Priya Natarajan",
  subject_role: "Product Designer, L3",
  notes: "Priya shipped the onboarding redesign and handled several support escalations. A couple of stakeholders said they lost track of what she was working on for a stretch in Q3.",
  prior_goals: "",
  roster_rows: [],
  target_bands: null,
  prescan_facts: {accomplishment_count: 2, accomplishments_without_metric: 2, has_goals_section: false, roster_count: 0, roster_missing_rating: 0, flags: [{id: "no-metric:2", label: "2 accomplishments have no number or measurable result"}]},
  current_datetime: (now | todate)
}' > input.json

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d @input.json | jq '.data'
payload = {
    "mode": "manager",
    "review_period": "H2 2026",
    "subject_name": "Priya Natarajan",
    "subject_role": "Product Designer, L3",
    "notes": "Priya shipped the onboarding redesign and handled several support "
             "escalations. A couple of stakeholders said they lost track of what "
             "she was working on for a stretch in Q3.",
    "prior_goals": "",
    "roster_rows": [],
    "target_bands": None,
    "prescan_facts": {"accomplishment_count": 2, "accomplishments_without_metric": 2,
                       "has_goals_section": False, "roster_count": 0, "roster_missing_rating": 0,
                       "flags": [{"id": "no-metric:2", "label": "2 accomplishments have no number or measurable result"}]},
    "current_datetime": "2026-08-10T12:00:00Z",
}
est = api("POST", "/estimate", payload)
print(est["hold_credits"], est["model"], est["model_alias"])
const payload = {
  mode: "manager",
  review_period: "H2 2026",
  subject_name: "Priya Natarajan",
  subject_role: "Product Designer, L3",
  notes: "Priya shipped the onboarding redesign and handled several support escalations. " +
    "A couple of stakeholders said they lost track of what she was working on for a stretch in Q3.",
  prior_goals: "",
  roster_rows: [],
  target_bands: null,
  prescan_facts: { accomplishment_count: 2, accomplishments_without_metric: 2, has_goals_section: false,
    roster_count: 0, roster_missing_rating: 0,
    flags: [{ id: "no-metric:2", label: "2 accomplishments have no number or measurable result" }] },
  current_datetime: new Date().toISOString(),
};
const est = await api("POST", "/estimate", payload);
console.log(est.hold_credits, est.model, est.model_alias);
payload := map[string]any{
	"mode": "manager", "review_period": "H2 2026", "subject_name": "Priya Natarajan",
	"subject_role": "Product Designer, L3",
	"notes": "Priya shipped the onboarding redesign and handled several support escalations.",
	"prior_goals": "", "roster_rows": []any{}, "target_bands": nil,
}
var est struct {
	HoldCredits int64  `json:"hold_credits"`
	Model       string `json:"model"`
}
err := call("POST", "/estimate", payload, &est)
// Build the JSON body with your preferred library, then:
String envelope = api("POST", "/estimate", jsonBody);
// data.hold_credits, data.model, data.model_alias
payload = { mode: "manager", review_period: "H2 2026", subject_name: "Priya Natarajan",
            subject_role: "Product Designer, L3",
            notes: "Priya shipped the onboarding redesign and handled several support escalations.",
            prior_goals: "", roster_rows: [], target_bands: nil }
est = api("POST", "/estimate", payload)
puts "#{est["hold_credits"]} #{est["model"]}"
$payload = ["mode" => "manager", "review_period" => "H2 2026", "subject_name" => "Priya Natarajan",
            "notes" => "Priya shipped the onboarding redesign and handled several support escalations.",
            "prior_goals" => "", "roster_rows" => [], "target_bands" => null];
$est = api("POST", "/estimate", $payload);
echo "{$est['hold_credits']} {$est['model']}\n";
var payload = new {
    mode = "manager", review_period = "H2 2026", subject_name = "Priya Natarajan",
    notes = "Priya shipped the onboarding redesign and handled several support escalations.",
    prior_goals = "", roster_rows = Array.Empty<object>(), target_bands = (object?)null
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("model")}");

Assert model == "gpt-5.6-terra", model_alias == "gpt-terra" and markup_bps == 1000 if you are verifying the app's wiring — that is exactly what this app's own deploy check does, with no charge.

Step 4 — Write the document and wait for the result

POST /run
GET /jobs/{job_id}

/run takes the same input as /estimate, places a credit hold and returns a job_id. Poll /jobs/{job_id} every 1–2 seconds until status is succeeded or failed. Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The document is in output — usually nested as output.output, and as a JSON string, so parse defensively.

JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: rs-$(date +%s)" \
  -d @input.json | jq -r '.data.job_id')

while :; do
  JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
  STATUS=$(echo "$JOB" | jq -r '.data.status')
  [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
  sleep 2
done

echo "$JOB" | jq -r '.data.output.output' > review.json

jq -r '
  "\(.subject_name) [\(.mode)] — \(.posture)",
  "",
  .overview,
  "",
  "OPEN QUESTIONS",
  (.open_questions[] | "  - \(.)"),
  "",
  "COVERAGE",
  (.coverage_check[] | "  \(.id): \(if .addressed then "ok" else "OPEN" end) - \(.note)")' \
  review.json

# only treat it as done when the document says so
jq -e '.posture == "ready-to-send"' review.json > /dev/null \
  || { echo "needs more input before it is ready"; }
import time

job_id = api("POST", "/run", payload, **{"Idempotency-Key": "rs-001"})["job_id"]

while True:
    job = api("GET", f"/jobs/{job_id}")
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)

if job["status"] == "failed":
    raise RuntimeError(job.get("error"))

doc = json.loads(job["output"]["output"])
print(doc["subject_name"], doc["mode"], doc["posture"])
print(doc["overview"])
with open("review.json", "w", encoding="utf-8") as fh:
    json.dump(doc, fh, indent=2)
const { job_id } = await api("POST", "/run", payload, { "Idempotency-Key": "rs-001" });

let job;
for (;;) {
  job = await api("GET", `/jobs/${job_id}`);
  if (job.status === "succeeded" || job.status === "failed") break;
  await new Promise((r) => setTimeout(r, 1500));
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const doc = JSON.parse(job.output.output);
console.log(doc.subject_name, doc.mode, doc.posture);
console.log(doc.overview);
var run struct{ JobID string `json:"job_id"` }
call("POST", "/run", payload, &run)

var job struct {
	Status string          `json:"status"`
	Output struct{ Output string `json:"output"` } `json:"output"`
}
for {
	call("GET", "/jobs/"+run.JobID, nil, &job)
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(1500 * time.Millisecond)
}
// POST /run with an Idempotency-Key header, read data.job_id, then poll
// GET /jobs/{job_id} every 1-2s until status is succeeded/failed, then
// parse data.output.output as JSON with your library of choice.
job_id = api("POST", "/run", payload)["job_id"]

loop do
  job = api("GET", "/jobs/#{job_id}")
  break if %w[succeeded failed].include?(job["status"])
  sleep 1.5
end

doc = JSON.parse(job["output"]["output"])
puts "#{doc["subject_name"]} #{doc["mode"]} #{doc["posture"]}"
$jobId = api("POST", "/run", $payload)["job_id"];

do {
    $job = api("GET", "/jobs/$jobId");
    if (in_array($job["status"], ["succeeded", "failed"])) break;
    sleep(2);
} while (true);

$doc = json_decode($job["output"]["output"], true);
echo "{$doc['subject_name']} {$doc['mode']} {$doc['posture']}\n";
var run = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload);
var jobId = run.GetProperty("job_id").GetString();

JsonElement job;
do {
    await Task.Delay(1500);
    job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));

var doc = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine($"{doc.GetProperty("subject_name")} {doc.GetProperty("posture")}");

The document object — output schema

One JSON object, always the same envelope regardless of mode — fields outside the requested mode's group come back empty ([] or null), never omitted. Every claim traces to what you sent; where the notes were too thin, the gap shows up in open_questions instead of an invented fact.

FieldTypeMeaning
modestringEchoes the request's mode.
posturestringready-to-send | needs-more-input | insufficient-evidence.
overviewstring2–3 sentence summary of the overall picture.
accomplishmentsarray, self mode{title, situation, contribution, impact, evidence_quote, has_metric} per accomplishment.
goals_reviewarray, self mode{goal, status, evidence}; status is met | exceeded | missed | unknown.
growth_areas, challenges, goals_next, feedback_for_managerstring[], self modePlain lists.
overall_ratingstring|null, manager modeExceeds | Meets | Below.
strengths, development_areasarray, manager mode{point, example} / {point, guidance}.
goal_achievementarray, manager mode{goal, rating, comments}.
development_planarray, manager mode{skill, current, target, actions}.
compensation_notestring, manager modeA direction and reason — never an invented dollar figure or percentage.
team_overviewarray, calibration modeOne entry per roster_rows input, same order, carried through unchanged.
rating_distributionarray, calibration mode{rating, count, pct, target_band}.
discussion_points, promotion_candidates, calibration_actionsarray, calibration modeGrounded in the roster's notes column.
open_questionsstring[]What's missing, phrased so you know exactly what to add.
coverage_checkarray{id, addressed, note} — one per prescan_facts.flags id you sent.
next_stepsstring[]Concrete next actions.
summarystringClosing paragraph, suitable as a doc header.

Step 5 — Stream the document as it is written

POST /run-stream

/run-stream takes exactly the same body as /run but answers with server-sent events. This app's own progress panel is this endpoint. Events are separated by a blank line; each has an event: line and a data: line carrying JSON.

EventPayloadMeaning
job{job_id, status}Sent once, when the job is accepted.
delta{text}A chunk of the document, in order. The app advances its step list by watching for keys like "posture", "accomplishments"/"overall_rating"/"team_overview" and "coverage_check" as they arrive.
done{job_id, status, charged_credits, output}The final, authoritative result — read the document from output.output.
error{code, message}Replaces done when the run fails.
# -N disables buffering so events print as they arrive
curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: rs-$(date +%s)" \
  -d @input.json

# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"mode\":\"manager\",\"posture\":"}
# ...
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":312,"output":{"output":"{...}"}}
import json, requests

result = None
with requests.post(
    API + "/run-stream",
    headers={"Authorization": f"Bearer {TOKEN}", "Idempotency-Key": "rs-001"},
    json=payload, stream=True,
) as r:
    r.raise_for_status()
    event = None
    for line in r.iter_lines(decode_unicode=True):
        if not line:
            continue
        if line.startswith("event:"):
            event = line[len("event:"):].strip()
        elif line.startswith("data:"):
            data = json.loads(line[len("data:"):].strip())
            if event == "delta":
                print(".", end="", flush=True)
            elif event == "done":
                result = data
            elif event == "error":
                raise RuntimeError(data.get("message", "run failed"))

doc = json.loads(result["output"]["output"])
print("charged:", result["charged_credits"], "-", doc["subject_name"], doc["posture"])
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", "Idempotency-Key": "rs-001" },
  body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = null, result = null;
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  let idx;
  while ((idx = buf.indexOf("\n")) >= 0) {
    const line = buf.slice(0, idx); buf = buf.slice(idx + 1);
    if (line.startsWith("event:")) event = line.slice(6).trim();
    else if (line.startsWith("data:")) {
      const data = JSON.parse(line.slice(5).trim());
      if (event === "done") result = data;
      if (event === "error") throw new Error(data.message || "run failed");
    }
  }
}
const doc = JSON.parse(result.output.output);
console.log(doc.subject_name, doc.posture);
// Open the POST response body as a stream, scan line by line, split on
// "event:"/"data:" prefixes exactly as the Python example does, and parse
// the "done" event's data.output.output as your result JSON string.
// HttpResponse.BodyHandlers.ofLines() gives you a line stream; apply the
// same event:/data: parsing as the other examples and JSON-decode the
// "done" event's output.output field.
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  req = Net::HTTP::Post.new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Idempotency-Key"] = "rs-001"
  req.body = payload.to_json
  http.request(req) do |res|
    event = nil
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.strip
        next if line.empty?
        event = line.sub("event:", "").strip if line.start_with?("event:")
        if line.start_with?("data:")
          data = JSON.parse(line.sub("data:", "").strip)
          raise data["message"] if event == "error"
          # event == "done" carries data["output"]["output"]
        end
      end
    end
  end
end
// Use curl with CURLOPT_WRITEFUNCTION to receive the stream incrementally,
// buffering partial lines and applying the same event:/data: parsing as
// the other examples.
// HttpCompletionOption.ResponseHeadersRead + a StreamReader over the
// response content lets you read line by line; apply the same event:/data:
// parsing as the other examples.

In a browser, the native EventSource only speaks GET, and this endpoint is a POST — read the fetch response body incrementally, as the JavaScript sample above does. On an idempotent replay the server may answer with a plain JSON envelope instead of an event stream; check the Content-Type before you start parsing frames.