SLO Studio — API
Open the app

Generate SLI/SLO packages from your own code

Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS, so you can script it from any language. Paste a service description and get back SLIs with PromQL, SLO targets with rationale, an error-budget policy, multiwindow burn-rate alerts, gaps and open questions. This page walks through each task with examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api. Every request sends Authorization: Bearer <token>, the app selector X-App-Slug: slo-studio, and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"data": …} on success, {"error": {"code", "message"}} on failure.

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. guests reviewing a custom service).
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 15-line helper that adds the auth header and the X-App-Slug header, sends JSON and unwraps the data envelope. The later steps reuse this helper.

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

# every call looks like:
#   curl -s "$API/…" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" [-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"
SLUG = "slo-studio"
TOKEN = "YOUR_TOKEN"  # see step 1

def api(method, path, body=None, **headers):
    res = requests.request(method, API + path, json=body,
                           headers={"Authorization": f"Bearer {TOKEN}",
                                    "X-App-Slug": SLUG, **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 SLUG = "slo-studio";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your environment in real code

async function api(method, path, body, extraHeaders = {}) {
  const res = await fetch(API + path, {
    method,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "X-App-Slug": SLUG,
      "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"
const Slug = "slo-studio"

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("X-App-Slug", Slug)
	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 SLUG = "slo-studio";
    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("X-App-Slug", SLUG)
            .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"
SLUG = "slo-studio"
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["X-App-Slug"] = SLUG
  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";
const SLUG = "slo-studio";
$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",
            "X-App-Slug: " . SLUG,
            "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";
    const string Slug = "slo-studio";
    static readonly HttpClient Http = new();

    static SkillSafe()
    {
        Http.DefaultRequestHeaders.Authorization =
            new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1
        Http.DefaultRequestHeaders.Add("X-App-Slug", Slug);
    }

    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, estimate costs and run the built-in example (free). To review your own services you need your personal token: open the token page, sign in, and hit "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 (below) mints a guest token with no browser involved.

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

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 an expensive run.

curl -s "$API/me" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" | 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 the same 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
service_namestringShort name of the service, e.g. checkout-api.
descriptionstring, requiredWhat the service does, who uses it, its architecture and its dependencies. This is the text everything else is grounded in.
metrics_notesstring, may be emptyTelemetry that exists today — metrics, traces, logs, monitoring stack. When empty or without request-level telemetry, the verdict comes back Needs instrumentation.
incident_notesstring, may be emptyRecent incidents, current pain, known failure modes.
strictnessstringauto (default) or one of conservative, standard, aggressive. conservative picks targets the service clearly meets today; aggressive picks targets that force investment.
prescanobjectClient-side scan facts the web app sends as untrusted hints. API callers may send {} — the model re-derives every fact from description.
_clippedboolean, optionaltrue when the caller dropped the middle of a long field to fit the input budget, marking the cut in-band with [... middle omitted by SLO Studio ...]. The model then works from the surviving start and end and says so in notes. Omit it (or send false) when you send the whole text.
retry_notestring, optionalA correction instruction sent only when a previous reply could not be parsed as the single JSON object. The model obeys it and never mentions it in the package. The web app reuses the same idempotency key derived from the input on that retry, so a reformat never double-bills.
curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" \
  -d '{"service_name":"checkout-api","description":"Stateless Go API behind an ALB that takes cart checkout requests, calls Stripe and writes orders to Postgres.","metrics_notes":"Prometheus http_requests_total and histogram by route.","incident_notes":"","strictness":"auto","prescan":{}}' \
  | jq '.data.hold_credits'
service = {
    "service_name": "checkout-api",
    "description": ("Stateless Go API behind an ALB that takes cart checkout requests, "
                    "calls Stripe and writes orders to Postgres."),
    "metrics_notes": "Prometheus http_requests_total and histogram by route.",
    "incident_notes": "",
    "strictness": "auto",
    "prescan": {},
}

est = api("POST", "/estimate", service)
print("worst case:", est.get("hold_credits", est.get("credits")), "credits")
const service = {
  service_name: "checkout-api",
  description:
    "Stateless Go API behind an ALB that takes cart checkout requests, " +
    "calls Stripe and writes orders to Postgres.",
  metrics_notes: "Prometheus http_requests_total and histogram by route.",
  incident_notes: "",
  strictness: "auto",
  prescan: {},
};

const est = await api("POST", "/estimate", service);
console.log("worst case:", est.hold_credits ?? est.credits, "credits");
service := map[string]any{
	"service_name":   "checkout-api",
	"description":    "Stateless Go API behind an ALB that takes cart checkout requests, calls Stripe and writes orders to Postgres.",
	"metrics_notes":  "Prometheus http_requests_total and histogram by route.",
	"incident_notes": "",
	"strictness":     "auto",
	"prescan":        map[string]any{},
}

var est struct{ HoldCredits int64 `json:"hold_credits"` }
err := call("POST", "/estimate", service, &est)
String input = """
    {"service_name":"checkout-api",
     "description":"Stateless Go API behind an ALB that takes cart checkout requests, calls Stripe and writes orders to Postgres.",
     "metrics_notes":"Prometheus http_requests_total and histogram by route.",
     "incident_notes":"","strictness":"auto","prescan":{}}""";

String envelope = api("POST", "/estimate", input);
// worst-case cost is at data.hold_credits
service = {
  service_name: "checkout-api",
  description: "Stateless Go API behind an ALB that takes cart checkout requests, " \
               "calls Stripe and writes orders to Postgres.",
  metrics_notes: "Prometheus http_requests_total and histogram by route.",
  incident_notes: "",
  strictness: "auto",
  prescan: {},
}

est = api("POST", "/estimate", service)
puts "worst case: #{est["hold_credits"] || est["credits"]} credits"
$service = [
    "service_name"   => "checkout-api",
    "description"    => "Stateless Go API behind an ALB that takes cart checkout requests, "
                      . "calls Stripe and writes orders to Postgres.",
    "metrics_notes"  => "Prometheus http_requests_total and histogram by route.",
    "incident_notes" => "",
    "strictness"     => "auto",
    "prescan"        => new stdClass(),
];

$est = api("POST", "/estimate", $service);
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) . " credits\n";
var service = new {
    service_name = "checkout-api",
    description = "Stateless Go API behind an ALB that takes cart checkout requests, "
                + "calls Stripe and writes orders to Postgres.",
    metrics_notes = "Prometheus http_requests_total and histogram by route.",
    incident_notes = "",
    strictness = "auto",
    prescan = new { },
};

var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", service);
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");

Step 4 — Run a review 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 (a run typically takes 20–60 s). Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The SLO package is in output (sometimes nested as output.output, and possibly a JSON string — parse defensively).

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

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

echo "$JOB" | jq '.data.output'
import time

job_id = api("POST", "/run", service,
             **{"Idempotency-Key": "my-run-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", "run failed"))

raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
    raw = raw["output"]
pkg = json.loads(raw) if isinstance(raw, str) else raw
print(pkg["verdict"], pkg["confidence"])
for slo in pkg["slos"]:
    print(f'{slo["sli"]}: {slo["target_pct"]}% over {slo["window_days"]}d')
const { job_id } = await api("POST", "/run", service, {
  "Idempotency-Key": crypto.randomUUID(),
});

let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");

if (job.status === "failed") throw new Error(job.error ?? "run failed");

const raw = job.output?.output ?? job.output;
const pkg = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(pkg.verdict, pkg.confidence);
for (const slo of pkg.slos)
  console.log(`${slo.sli}: ${slo.target_pct}% over ${slo.window_days}d`);
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", service, &started)
if err != nil {
	log.Fatal(err)
}

var job struct {
	Status string          `json:"status"`
	Error  string          `json:"error"`
	Output json.RawMessage `json:"output"`
}
for {
	if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
		log.Fatal(err)
	}
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(1500 * time.Millisecond)
}
// job.Output holds the SLO package (may be {"output": …} or a JSON string —
// unwrap/unquote before unmarshalling into your package struct).
String envelope = api("POST", "/run", input);
String jobId = /* data.job_id via your JSON library */;

while (true) {
    String job = api("GET", "/jobs/" + jobId, null);
    String status = /* data.status */;
    if (status.equals("succeeded") || status.equals("failed")) break;
    Thread.sleep(1500);
}
// the SLO package is at data.output (sometimes data.output.output, possibly a
// JSON string — parse it again if so): verdict, slis, slos, alerts, gaps…
started = api("POST", "/run", service)

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

raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
pkg = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{pkg["verdict"]} (#{pkg["confidence"]})"
pkg["slos"].each { |s| puts "#{s["sli"]}: #{s["target_pct"]}% over #{s["window_days"]}d" }
$started = api("POST", "/run", $service);

do {
    sleep(2);
    $job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));

if ($job["status"] === "failed") {
    throw new Exception($job["error"] ?? "run failed");
}

$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$pkg = is_string($raw) ? json_decode($raw, true) : $raw;
echo "{$pkg['verdict']} ({$pkg['confidence']})\n";
foreach ($pkg["slos"] as $slo) {
    echo "{$slo['sli']}: {$slo['target_pct']}% over {$slo['window_days']}d\n";
}
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", service);
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true)
{
    job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
    var status = job.GetProperty("status").GetString();
    if (status is "succeeded" or "failed") break;
    await Task.Delay(1500);
}
// the SLO package is at job.GetProperty("output") — sometimes nested under
// "output", possibly a JSON string; parse defensively.

The result object has this shape:

FieldType
service_namestring — echoed back, normalized
verdictReady to adopt | Needs instrumentation
confidenceHigh | Medium | Low
summarystring — 2–4 sentences: what the service is, whether its telemetry can support SLOs today, and the most important next step
slisarray of {name, type, signal, promql, rationale}; type is one of availability, latency, error_rate, durability, correctness, freshness, throughput, quality
slosarray of {sli, target_pct, window_days, rationale}; target_pct is a number from 90 to 99.999, window_days is 7, 28 or 30
error_budget_policyarray of {remaining_pct, action} — what the team does as budget drains
alertsarray of {name, severity, burn_rate, long_window, short_window, budget_consumed_pct, applies_to}; may be an empty array when verdict is Needs instrumentation
gapsarray of {gap, fix, priority}; priority is High, Medium or Low, most severe first
open_questionsstring[] — at most 4; empty when nothing critical is missing
notesstring — caveats, sequencing, quick wins

Burn-rate arithmetic: budget_consumed_pct = burn_rate × long_window_hours ÷ (window_days × 24) × 100, rounded to one decimal. The standard fast-burn pair (burn_rate 14.4 over a long_window of 1h) consumes 2% of a 30-day budget; the slow-burn pair (6 over 6h) consumes 5%. Recompute this yourself if you rewrite an alert's windows.

An abbreviated response body looks like this:

{
  "service_name": "checkout-api",
  "verdict": "Ready to adopt",
  "confidence": "High",
  "summary": "Stateless checkout API with request-level Prometheus metrics …",
  "slis": [
    { "name": "api_availability", "type": "availability",
      "signal": "Non-5xx HTTP responses over all requests at the ALB",
      "promql": "sum(rate(http_requests_total{status!~\"5..\"}[28d])) / sum(rate(http_requests_total[28d]))",
      "rationale": "A failed checkout call is a lost order." }
  ],
  "slos": [
    { "sli": "api_availability", "target_pct": 99.9, "window_days": 28,
      "rationale": "Payment path; Stripe's own SLA caps achievable nines." }
  ],
  "error_budget_policy": [
    { "remaining_pct": 100, "action": "Normal development velocity." },
    { "remaining_pct": 10,  "action": "Freeze non-critical changes." }
  ],
  "alerts": [
    { "name": "FastBurn", "severity": "critical", "burn_rate": 14.4,
      "long_window": "1h", "short_window": "5m",
      "budget_consumed_pct": 2, "applies_to": "api_availability" }
  ],
  "gaps": [
    { "gap": "No latency histogram on the Stripe call.",
      "fix": "Instrument the outbound client with a duration histogram.",
      "priority": "High" }
  ],
  "open_questions": ["What p95 checkout latency do users tolerate today?"],
  "notes": "Start with availability; add the latency SLO once the histogram lands."
}
# same JSON, read from Python
print(pkg["summary"])
for a in pkg["alerts"]:
    print(a["name"], a["burn_rate"], a["long_window"], a["short_window"],
          a["budget_consumed_pct"], "%", a["applies_to"])
for g in pkg["gaps"]:
    print(g["priority"], g["gap"], "->", g["fix"])
print("open questions:", pkg["open_questions"])
// same JSON, read from JavaScript
console.log(pkg.summary);
for (const a of pkg.alerts)
  console.log(a.name, a.burn_rate, a.long_window, a.short_window,
              `${a.budget_consumed_pct}%`, a.applies_to);
for (const g of pkg.gaps) console.log(g.priority, g.gap, "->", g.fix);
console.log("open questions:", pkg.open_questions);
// a struct that matches the response
type Package struct {
	ServiceName string `json:"service_name"`
	Verdict     string `json:"verdict"`
	Confidence  string `json:"confidence"`
	Summary     string `json:"summary"`
	SLIs        []struct {
		Name, Type, Signal, PromQL, Rationale string
	} `json:"slis"`
	SLOs []struct {
		SLI        string  `json:"sli"`
		TargetPct  float64 `json:"target_pct"`
		WindowDays int     `json:"window_days"`
		Rationale  string  `json:"rationale"`
	} `json:"slos"`
	ErrorBudgetPolicy []struct {
		RemainingPct float64 `json:"remaining_pct"`
		Action       string  `json:"action"`
	} `json:"error_budget_policy"`
	Alerts []struct {
		Name, Severity    string  `json:"name"`
		BurnRate          float64 `json:"burn_rate"`
		LongWindow        string  `json:"long_window"`
		ShortWindow       string  `json:"short_window"`
		BudgetConsumedPct float64 `json:"budget_consumed_pct"`
		AppliesTo         string  `json:"applies_to"`
	} `json:"alerts"`
	Gaps []struct {
		Gap, Fix, Priority string
	} `json:"gaps"`
	OpenQuestions []string `json:"open_questions"`
	Notes         string   `json:"notes"`
}
// with Jackson: map the envelope's data.output onto a record tree
record Sli(String name, String type, String signal, String promql, String rationale) {}
record Slo(String sli, double target_pct, int window_days, String rationale) {}
record Budget(double remaining_pct, String action) {}
record Alert(String name, String severity, double burn_rate, String long_window,
             String short_window, double budget_consumed_pct, String applies_to) {}
record Gap(String gap, String fix, String priority) {}
record Package(String service_name, String verdict, String confidence, String summary,
               List<Sli> slis, List<Slo> slos, List<Budget> error_budget_policy,
               List<Alert> alerts, List<Gap> gaps, List<String> open_questions,
               String notes) {}
# same JSON, read from Ruby
puts pkg["summary"]
pkg["alerts"].each do |a|
  puts "#{a["name"]} #{a["burn_rate"]}x #{a["long_window"]}/#{a["short_window"]} " \
       "#{a["budget_consumed_pct"]}% #{a["applies_to"]}"
end
pkg["gaps"].each { |g| puts "#{g["priority"]}: #{g["gap"]} -> #{g["fix"]}" }
puts "open questions: #{pkg["open_questions"].join("; ")}"
// same JSON, read from PHP
echo $pkg["summary"] . "\n";
foreach ($pkg["alerts"] as $a) {
    echo "{$a['name']} {$a['burn_rate']}x {$a['long_window']}/{$a['short_window']} "
       . "{$a['budget_consumed_pct']}% {$a['applies_to']}\n";
}
foreach ($pkg["gaps"] as $g) {
    echo "{$g['priority']}: {$g['gap']} -> {$g['fix']}\n";
}
echo "open questions: " . implode("; ", $pkg["open_questions"]) . "\n";
// same JSON, read from C# with System.Text.Json
var output = job.GetProperty("output");
Console.WriteLine(output.GetProperty("summary").GetString());
foreach (var a in output.GetProperty("alerts").EnumerateArray())
    Console.WriteLine($"{a.GetProperty("name")} {a.GetProperty("burn_rate")}x " +
                      $"{a.GetProperty("long_window")}/{a.GetProperty("short_window")} " +
                      $"{a.GetProperty("budget_consumed_pct")}%");
foreach (var g in output.GetProperty("gaps").EnumerateArray())
    Console.WriteLine($"{g.GetProperty("priority")}: {g.GetProperty("gap")}");

Step 5 — Stream the review live (SSE)

POST /run-stream

Same input and billing as /run, but the response is a Server-Sent-Events stream: job (accepted), repeated delta events carrying output text as it is generated, then a final done event whose payload matches the finished job (its output is authoritative — the app itself uses this endpoint). Send an Idempotency-Key here too.

curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" -H "Idempotency-Key: slo-$(date +%s)" \
  -d @input.json
# events arrive as:  event: delta\ndata: {"text":"…"}
import requests, json

with requests.post(f"{API}/run-stream",
        headers={"Authorization": f"Bearer {TOKEN}", "X-App-Slug": SLUG,
                 "Accept": "text/event-stream", "Idempotency-Key": "slo-001"},
        json=service, stream=True) as res:
    for line in res.iter_lines(decode_unicode=True):
        if line.startswith("data:"):
            evt = json.loads(line[5:])
            if "text" in evt:
                print(evt["text"], end="", flush=True)
const res = await fetch(`${API}/run-stream`, {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "X-App-Slug": SLUG,
             "Content-Type": "application/json",
             Accept: "text/event-stream", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify(service),
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  for (const line of value.split("\n"))
    if (line.startsWith("data:")) process.stdout.write(JSON.parse(line.slice(5)).text ?? "");
}
body, _ := json.Marshal(service)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", Slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
	line := sc.Text()
	if strings.HasPrefix(line, "data:") {
		fmt.Print(line[5:]) // parse JSON for the "text" field in real code
	}
}
// Java 17+: read the SSE body line by line
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("X-App-Slug", SLUG)
    .header("Content-Type", "application/json")
    .header("Accept", "text/event-stream")
    .POST(HttpRequest.BodyPublishers.ofString(input))
    .build();
HTTP.send(req, HttpResponse.BodyHandlers.ofLines())
    .body()
    .filter(l -> l.startsWith("data:"))
    .forEach(l -> System.out.println(l.substring(5)));
uri = URI("#{API}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req.body = service.to_json
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line { |l| print l[5..] if l.start_with?("data:") }
    end
  end
end
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST          => true,
    CURLOPT_HTTPHEADER    => ["Authorization: Bearer $TOKEN",
                              "X-App-Slug: " . SLUG,
                              "Content-Type: application/json",
                              "Accept: text/event-stream"],
    CURLOPT_POSTFIELDS    => json_encode($service),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) {
        foreach (explode("\n", $chunk) as $line) {
            if (str_starts_with($line, "data:")) echo substr($line, 5);
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream")
{
    Content = JsonContent.Create(service)
};
req.Headers.Accept.ParseAdd("text/event-stream");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await reader.ReadLineAsync() is { } line)
    if (line.StartsWith("data:")) Console.Write(line[5..]);

The web app saves your SLO packages in this browser only — there is no server-side collection to query. Keep the done event's payload if you want a record.