Use the advisor 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. 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> 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 running a custom analysis). |
404 | Unknown job 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, sends JSON and unwraps the data envelope. The later steps
reuse this helper.
export API="https://api.skillsafe.ai/v1/app-api"
export SKILLSAFE_TOKEN="YOUR_TOKEN" # see step 1
export TOKEN="$SKILLSAFE_TOKEN"
# 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, os, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_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}", **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 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
A guest token lets you check balances, estimate costs and run the built-in example
(free). To analyze your own dataset 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":"chart-coach"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "chart-coach"})["token"]
const { token } = await api("POST", "/guest", { slug: "chart-coach" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "chart-coach"}, &guest)
String envelope = api("POST", "/guest", """
{"slug":"chart-coach"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "chart-coach" })["token"]
$token = api("POST", "/guest", ["slug" => "chart-coach"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "chart-coach" });
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" | 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 |
|---|---|---|
data_text | string, required | The dataset: CSV/TSV rows (a clipped sample is fine), or a prose description of the data. |
data_profile | object |
Facts computed from the full dataset before clipping:
{ rows: number, columns: [{ name, type, distinct, min?, max? }] } where
type is "numeric", "categorical",
"temporal" or "text", and min/max
are sent for numeric columns. Treated as ground truth over the sample.
|
goal | string, required | What the chart should communicate — the message, not the topic (e.g. "show the trend"). |
library | string | Code target: "matplotlib", "plotly", "vega-lite",
"recharts", "d3" or "echarts". |
current_chart | string, optional | Description of a chart you already built, for critique. |
prescan_pick | string, optional | The chart type a local heuristic suggested — the model must justify disagreeing with it. |
cat > input.json <<'JSON'
{
"data_text": "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
"data_profile": {
"rows": 4,
"columns": [
{ "name": "month", "type": "temporal", "distinct": 4 },
{ "name": "signups", "type": "numeric", "distinct": 4, "min": 120, "max": 265 }
]
},
"goal": "show the trend",
"library": "matplotlib",
"prescan_pick": "line"
}
JSON
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json | jq '.data.hold_credits'
INPUT = {
"data_text": "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
"data_profile": {
"rows": 4,
"columns": [
{"name": "month", "type": "temporal", "distinct": 4},
{"name": "signups", "type": "numeric", "distinct": 4, "min": 120, "max": 265},
],
},
"goal": "show the trend",
"library": "matplotlib",
"prescan_pick": "line",
}
est = api("POST", "/estimate", INPUT)
print("worst case:", est.get("hold_credits", est.get("credits")), "credits")
const input = {
data_text: "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
data_profile: {
rows: 4,
columns: [
{ name: "month", type: "temporal", distinct: 4 },
{ name: "signups", type: "numeric", distinct: 4, min: 120, max: 265 },
],
},
goal: "show the trend",
library: "matplotlib",
prescan_pick: "line",
};
const est = await api("POST", "/estimate", input);
console.log("worst case:", est.hold_credits ?? est.credits, "credits");
input := map[string]any{
"data_text": "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
"data_profile": map[string]any{
"rows": 4,
"columns": []map[string]any{
{"name": "month", "type": "temporal", "distinct": 4},
{"name": "signups", "type": "numeric", "distinct": 4, "min": 120, "max": 265},
},
},
"goal": "show the trend",
"library": "matplotlib",
"prescan_pick": "line",
}
var est struct{ HoldCredits int64 `json:"hold_credits"` }
err := call("POST", "/estimate", input, &est)
static final String INPUT = """
{
"data_text": "month,signups\\n2024-01,120\\n2024-02,148\\n2024-03,203\\n2024-04,265",
"data_profile": {
"rows": 4,
"columns": [
{"name": "month", "type": "temporal", "distinct": 4},
{"name": "signups", "type": "numeric", "distinct": 4, "min": 120, "max": 265}
]
},
"goal": "show the trend",
"library": "matplotlib",
"prescan_pick": "line"
}""";
String envelope = api("POST", "/estimate", INPUT);
// worst-case cost is at data.hold_credits
INPUT = {
data_text: "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
data_profile: {
rows: 4,
columns: [
{ name: "month", type: "temporal", distinct: 4 },
{ name: "signups", type: "numeric", distinct: 4, min: 120, max: 265 },
],
},
goal: "show the trend",
library: "matplotlib",
prescan_pick: "line",
}
est = api("POST", "/estimate", INPUT)
puts "worst case: #{est["hold_credits"] || est["credits"]} credits"
$input = [
"data_text" => "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
"data_profile" => [
"rows" => 4,
"columns" => [
["name" => "month", "type" => "temporal", "distinct" => 4],
["name" => "signups", "type" => "numeric", "distinct" => 4, "min" => 120, "max" => 265],
],
],
"goal" => "show the trend",
"library" => "matplotlib",
"prescan_pick" => "line",
];
$est = api("POST", "/estimate", $input);
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) . " credits\n";
var input = new {
data_text = "month,signups\n2024-01,120\n2024-02,148\n2024-03,203\n2024-04,265",
data_profile = new {
rows = 4,
columns = new object[] {
new { name = "month", type = "temporal", distinct = 4 },
new { name = "signups", type = "numeric", distinct = 4, min = 120, max = 265 },
},
},
goal = "show the trend",
library = "matplotlib",
prescan_pick = "line",
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", input);
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");
Compute data_profile from every row you have, even when you only send a
sample in data_text — row counts, distinct counts and numeric ranges are
what keep the advice honest about overplotting and scale choices.
Step 4 — Run the advisor 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 recommendation is in output (sometimes
nested as output.output, and usually a JSON string — parse defensively).
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -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")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
# output is a JSON string — fromjson turns it into the recommendation object
echo "$JOB" | jq -r '.data.output | if type == "string" then fromjson else . end
| {chart: .recommendation.chart, verdict}'
import time
job_id = api("POST", "/run", INPUT,
**{"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"]
rec = json.loads(raw) if isinstance(raw, str) else raw
print(rec["recommendation"]["chart"], "—", rec["verdict"])
print(rec["code"]["source"])
const { job_id } = await api("POST", "/run", input,
{ "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 rec = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(rec.recommendation.chart, "—", rec.verdict);
console.log(rec.code.source);
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", input, &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 recommendation (may be {"output": …} or a JSON string —
// unwrap/unquote before unmarshalling into your own 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 recommendation is at data.output (sometimes data.output.output, usually a
// JSON string — parse it again if so)
started = api("POST", "/run", INPUT)
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"]
rec = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{rec["recommendation"]["chart"]} — #{rec["verdict"]}"
$started = api("POST", "/run", $input);
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"];
$rec = is_string($raw) ? json_decode($raw, true) : $raw;
echo "{$rec['recommendation']['chart']} — {$rec['verdict']}\n";
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", input);
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 recommendation is at job.GetProperty("output") — sometimes nested under
// "output", usually a JSON string; parse defensively.
Once parsed, the recommendation object has this shape:
| Field | Type |
|---|---|
title | string — short name for the dataset or chart task |
goal | string — your goal restated in one sentence |
verdict | string — one-line bottom line: the chart and why |
recommendation |
{chart, rationale, encodings[]}; each encoding is
{channel, field, type, note} |
alternatives | array of {chart, when, why_not} — at least 2 |
pitfalls |
array of {severity, title, problem, fix} — at least 1;
severity is "high" | "medium" | "low" |
current_chart_review | string — "" when no current_chart was sent |
design_notes | string[] — at least 3 imperative directives |
accessibility | string — palette, labeling and alt-text guidance |
code |
{library, language, source}; language is
"python" | "javascript" | "json",
source is the complete runnable code as one string |
summary | string — 3–5 sentence wrap-up |
Every top-level key is always present. Guard anyway: treat a missing or short
encodings/alternatives array as a malformed reply and re-run
rather than rendering a half-empty chart brief.
Step 5 — Stream the answer as it is written
Same body as /run, but the response is
text/event-stream: delta events carry
{"text": "…"} fragments of the JSON as the model writes it, an optional
job event announces the job_id, and a final done
event carries {job_id, status, charged_credits, output}. An
error event carries {code, message}. If the server answers with
JSON instead of an event stream (an idempotent replay), treat the body as a normal
/run response. Concatenating every delta gives you the same JSON
string as output.
curl -sN -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: stream-$(date +%s)" \
-d @input.json
with requests.post(API + "/run-stream", json=INPUT, stream=True, headers={
"Authorization": f"Bearer {TOKEN}",
"Accept": "text/event-stream",
"Idempotency-Key": "my-stream-001",
}) as res:
res.raise_for_status()
event, pieces, done = "message", [], None
for line in res.iter_lines(decode_unicode=True):
if line is None:
continue
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "delta":
pieces.append(payload.get("text", ""))
elif event == "done":
done = payload
elif event == "error":
raise RuntimeError(payload.get("message", "run failed"))
rec = json.loads("".join(pieces))
print(rec["recommendation"]["chart"], "charged:", done["charged_credits"])
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(input),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", text = "", done = null;
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let event = "message", data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
const payload = JSON.parse(data);
if (event === "delta") text += payload.text ?? "";
else if (event === "done") done = payload;
else if (event === "error") throw new Error(payload.message ?? "run failed");
}
}
const rec = JSON.parse(text);
console.log(rec.recommendation.chart, "charged:", done.charged_credits);
body, _ := json.Marshal(input)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
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()
var out strings.Builder
event := "message"
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var payload struct {
Text string `json:"text"`
Message string `json:"message"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &payload)
if event == "delta" {
out.WriteString(payload.Text)
} else if event == "error" {
log.Fatal(payload.Message)
}
}
}
// out.String() is the recommendation JSON — unmarshal it into your struct.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(INPUT))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
var out = new StringBuilder();
var event = new String[] { "message" };
res.body().forEach(line -> {
if (line.startsWith("event:")) {
event[0] = line.substring(6).trim();
} else if (line.startsWith("data:") && event[0].equals("delta")) {
// data is {"text": "…"} — append the decoded text with your JSON library
out.append(textOf(line.substring(5).trim()));
}
});
// out.toString() is the recommendation JSON
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req.body = INPUT.to_json
out = +""
event = "message"
buffer = +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) do |http|
http.request(req) do |res|
res.read_body do |chunk|
buffer << chunk
while (i = buffer.index("\n"))
line = buffer.slice!(0, i + 1).chomp
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
payload = JSON.parse(line[5..].strip) rescue next
out << payload.fetch("text", "") if event == "delta"
raise payload["message"] if event == "error"
end
end
end
end
end
rec = JSON.parse(out)
puts rec["recommendation"]["chart"]
$out = "";
$event = "message";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode($input),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$out, &$event) {
foreach (explode("\n", $chunk) as $line) {
$line = trim($line);
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:") && $event === "delta") {
$payload = json_decode(trim(substr($line, 5)), true);
$out .= $payload["text"] ?? "";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$rec = json_decode($out, true);
echo $rec["recommendation"]["chart"] . "\n";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream")
{
Content = JsonContent.Create(input),
};
req.Headers.Accept.Add(new("text/event-stream"));
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new StringBuilder();
var evt = "message";
while (await reader.ReadLineAsync() is string line)
{
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:") && evt == "delta")
{
var payload = JsonSerializer.Deserialize<JsonElement>(line[5..].Trim());
text.Append(payload.GetProperty("text").GetString());
}
}
// text.ToString() is the recommendation JSON
var rec = JsonSerializer.Deserialize<JsonElement>(text.ToString());
Console.WriteLine(rec.GetProperty("recommendation").GetProperty("chart"));
Streaming charges the same credits as /run. Send an
Idempotency-Key here too — a dropped connection you retry without one is a
second, separately billed run.