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.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/credits. |
403 | The token isn't allowed to do this (e.g. guests reviewing a custom service). |
404 | Unknown job or record id. |
5xx | Transient 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
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
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
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 field | Type | Notes |
|---|---|---|
service_name | string | Short name of the service, e.g. checkout-api. |
description | string, required | What the service does, who uses it, its architecture and its dependencies. This is the text everything else is grounded in. |
metrics_notes | string, may be empty | Telemetry that exists today — metrics, traces, logs, monitoring stack. When empty or without request-level telemetry, the verdict comes back Needs instrumentation. |
incident_notes | string, may be empty | Recent incidents, current pain, known failure modes. |
strictness | string | auto (default) or one of conservative, standard, aggressive. conservative picks targets the service clearly meets today; aggressive picks targets that force investment. |
prescan | object | Client-side scan facts the web app sends as untrusted hints. API callers may send {} — the model re-derives every fact from description. |
_clipped | boolean, optional | true 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_note | string, optional | A 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
/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:
| Field | Type |
|---|---|
service_name | string — echoed back, normalized |
verdict | Ready to adopt | Needs instrumentation |
confidence | High | Medium | Low |
summary | string — 2–4 sentences: what the service is, whether its telemetry can support SLOs today, and the most important next step |
slis | array of {name, type, signal, promql, rationale}; type is one of availability, latency, error_rate, durability, correctness, freshness, throughput, quality |
slos | array 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_policy | array of {remaining_pct, action} — what the team does as budget drains |
alerts | array of {name, severity, burn_rate, long_window, short_window, budget_consumed_pct, applies_to}; may be an empty array when verdict is Needs instrumentation |
gaps | array of {gap, fix, priority}; priority is High, Medium or Low, most severe first |
open_questions | string[] — at most 4; empty when nothing critical is missing |
notes | string — 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)
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.