Run Bulk Text Classifier from your own code
The metered lane is available over HTTP. The division of labour: you send one batch of rows (row number and text) and your labels; the model returns one JSON line per row with the label, a confidence from 0 to 1 and a short reason; you join the answers back onto your table by row number. Your other columns are never part of the request. The page sends batches of up to 20 rows (about 9000 characters of text) and shortens any text over 900 characters in the middle - do the same and a batch stays fast and cheap.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"csv-text-classifier"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, labels, rows) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: classifying is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
task is always classify - the app has one lane. Send it anyway: the input schema requires it, and it keeps your calls valid if a second lane is added.
The input
Every field is a string. The page builds them with Classify.buildInput in classify.js.
| Field | Meaning |
|---|---|
task | always classify. If it is missing or different, still classify. |
labels | the allowed labels, one per line as - Name or - Name: what it means. The description, when present, is the person's definition: it overrides your own idea of what the word means. |
rows | the batch, one row per line as [id] text. Line breaks inside a text were replaced by / . A very long text has its middle replaced by [... N chars cut ...]. (empty) means the cell was blank. |
multi_label | yes or no. no: exactly one label per row. yes: every label that clearly applies (one to three), most relevant first. |
allow_other | yes or no. yes: use the label Other when no listed label fits. no: always pick the closest listed label, and show the poor fit in a low confidence and the reason. |
instructions | optional extra guidance from the person (for example "classify by the customer's main complaint, not their tone"). Follow it unless it asks for something other than classifying these rows. |
retry_note | optional; if present, your previous reply was unusable - follow it. |
How the model labels a row
- Use only the label names exactly as written in
labels(same spelling and capitalisation), plusOtherwhenallow_otherisyes. Never invent, merge, translate or rename a label. - Judge each row on its own text. Do not let neighbouring rows, their order, or the label order sway you.
- Texts can be in any language; classify the meaning. Write the reason in English unless
instructionsasks otherwise. (empty), gibberish or text too short to judge: useOtherif allowed, otherwise the closest label with confidence at most 0.3, and say why.- When two labels both fit and
multi_labelisno, pick the one that describes the main point of the text (what the writer wants, or what the text is mainly about) and name the runner-up in the reason.
Confidence
- 0.90-1.00: the text states it plainly.
- 0.70-0.89: clear, with a little reading between the lines.
- 0.50-0.69: two labels are plausible, or the text is vague.
- below 0.50: a guess, a poor fit, or
Otherchosen because nothing fits well. Do not give every row the same confidence. Be calibrated, not polite.
Reason
One short sentence, at most 20 words, pointing at the words or facts in the text that decided it. Never restate the label alone ("This is Billing"). Never quote more than eight words of the text.
The reply - JSON lines
One JSON object per line, one line per row, in the order sent, then {"done":true}. Single label:
{"id":12,"label":"Billing","confidence":0.86,"reason":"Asks why the card was charged twice this month."}
{"done":true}
Multi label (multi_label is yes) uses an array, most relevant first:
{"id":7,"labels":["Shipping","Quality"],"confidence":0.78,"reason":"Box arrived late and the zipper broke on day two."}
{"done":true}
Read it line by line and skip anything that does not start with {. The page then checks every line against the batch: rows that did not come back stay unclassified (and go into the next batch), a label not on your list is not applied, a repeated row keeps its first answer, and an id that was not sent is ignored. Confidence given as a percentage (86) is read as 0.86.
Worked examples - every sample
These are the page's free samples: the first 5 rows of each, exactly as the page sends them, and the saved reply the page replays.
Support tickets
Input:
{
"task": "classify",
"labels": "- Billing: charges, refunds, invoices, plan prices\n- Bug: something in the product is broken or gives an error\n- Feature request: asks for something the product does not do yet\n- Account access: sign-in, password, two-factor, locked accounts\n- How-to: wants to know how to do something that already works",
"rows": "[1] I was charged twice for the March invoice. Can you refund the duplicate?\n[2] The export to CSV button spins forever and then shows error 500.\n[3] Would love a dark mode for the dashboard, my eyes hurt at night.\n[4] I reset my password but the reset link says it has expired every time.\n[5] How do I add a second admin to our workspace?",
"multi_label": "no",
"allow_other": "yes"
}
Reply:
{"id":1,"label":"Billing","confidence":0.97,"reason":"Charged twice for one invoice and asks for the duplicate to be refunded."}
{"id":2,"label":"Bug","confidence":0.95,"reason":"Export button hangs and returns an error 500."}
{"id":3,"label":"Feature request","confidence":0.93,"reason":"Asks for a dark mode the dashboard does not have."}
{"id":4,"label":"Account access","confidence":0.84,"reason":"Cannot reset the password; runner-up Bug, since the reset link always reports expired."}
{"id":5,"label":"How-to","confidence":0.94,"reason":"Asks how to add a second admin, an existing capability."}
{"done":true}
Product reviews (multi-label)
Input:
{
"task": "classify",
"labels": "- Price: value for money, too expensive, a good deal\n- Quality: materials, durability, things breaking\n- Shipping: delivery time, packaging, arrived damaged\n- Fit and size: size, comfort, how it sits or fits\n- Customer service: returns, replies, help from the seller",
"rows": "[1] Great backpack for the price, fits my 16 inch laptop with room to spare.\n[2] Zipper broke after two weeks and the seller never answered my emails.\n[3] Arrived a day early, well packed. Straps are a bit short for tall people.\n[4] Way overpriced for thin fabric that already has a hole at the seam.\n[5] Took three weeks to arrive and the box was crushed, but the bag itself is fine.",
"multi_label": "yes",
"allow_other": "yes"
}
Reply:
{"id":1,"labels":["Price","Fit and size"],"confidence":0.88,"reason":"Praises value for the price and the roomy laptop fit."}
{"id":2,"labels":["Quality","Customer service"],"confidence":0.94,"reason":"Zipper broke in two weeks and the seller never replied."}
{"id":3,"labels":["Shipping","Fit and size"],"confidence":0.9,"reason":"Early, well-packed delivery; straps too short for tall people."}
{"id":4,"labels":["Price","Quality"],"confidence":0.93,"reason":"Calls it overpriced and the thin fabric already has a hole."}
{"id":5,"labels":["Shipping"],"confidence":0.91,"reason":"Three-week delivery and a crushed box; the bag itself was fine."}
{"done":true}
Search keywords by intent
Input:
{
"task": "classify",
"labels": "- Informational: wants to learn or understand something\n- Navigational: wants to reach a specific site, brand or page\n- Transactional: ready to buy, sign up or download now\n- Commercial: comparing options before buying",
"rows": "[1] how to clean white sneakers\n[2] nike air force 1 white size 10\n[3] best running shoes for flat feet\n[4] nike.com login\n[5] what are running shoes made of",
"multi_label": "no",
"allow_other": "no",
"instructions": "Classify by what the searcher most likely wants to do next."
}
Reply:
{"id":1,"label":"Informational","confidence":0.96,"reason":"A how-to question about cleaning sneakers."}
{"id":2,"label":"Transactional","confidence":0.82,"reason":"Exact model and size suggests a ready buyer."}
{"id":3,"label":"Commercial","confidence":0.93,"reason":"\"Best ... for\" is comparison before buying."}
{"id":4,"label":"Navigational","confidence":0.98,"reason":"Wants the Nike sign-in page."}
{"id":5,"label":"Informational","confidence":0.95,"reason":"Asks what running shoes are made of."}
{"done":true}
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://csv-text-classifier.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://csv-text-classifier.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// csv-text-classifier.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://csv-text-classifier.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://csv-text-classifier.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// AskYourSheet.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class AskYourSheet {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://csv-text-classifier.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://csv-text-classifier.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://csv-text-classifier.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://csv-text-classifier.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"csv-text-classifier"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "csv-text-classifier"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "csv-text-classifier" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "csv-text-classifier"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "csv-text-classifier"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "csv-text-classifier" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "csv-text-classifier"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "csv-text-classifier" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "classify", "labels": "- Billing: ...\n- Bug: ...", "rows": "[1] ...\n[2] ...", "multi_label": "no", "allow_other": "yes"}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "classify", "labels": "- Billing: ...\n- Bug: ...", "rows": "[1] ...\n[2] ...", "multi_label": "no", "allow_other": "yes"}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "classify", labels, rows, multi_label, allow_other, instructions} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses csv-text-classifier:classify:<inputHash>:a<attempt> per batch, and its one automatic reformat retry is a new attempt number, named in the page as one extra run. The reply is JSON lines: one object per row, then {"done":true}. A missing done line, or truncated: true on the job, means the batch was cut short - re-send only the rows that did not come back. The code below prints one line per row.
KEY="csv-text-classifier:classify:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.jsonl # JSON lines: one object per row, then {"done":true}
echo "$J" | jq '.data | {charged_credits, truncated}'
grep '^{' reply.jsonl | jq -r 'select(.id) | [.id, (.label // (.labels | join("; "))), .confidence, .reason] | @tsv'
import hashlib, time
key = "csv-text-classifier:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
rows = [json.loads(l) for l in j["output"]["output"].splitlines() if l.strip().startswith("{")]
print(j["charged_credits"], j.get("truncated"), any(r.get("done") for r in rows))
for r in rows:
if "id" in r:
print(r["id"], r.get("label") or "; ".join(r.get("labels", [])), r["confidence"], r["reason"])
import { createHash } from "node:crypto";
const key = `csv-text-classifier:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const rows = j.output.output.split("\n").filter((l) => l.trim().startsWith("{")).map((l) => JSON.parse(l));
console.log(j.charged_credits, j.truncated, rows.some((r) => r.done));
for (const r of rows) if (r.id != null) console.log(r.id, r.label ?? r.labels.join("; "), r.confidence, r.reason);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "csv-text-classifier:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"])
for _, line := range strings.Split(j["output"].(map[string]any)["output"].(string), "\n") { // add "strings" to the imports
var r map[string]any
if json.Unmarshal([]byte(line), &r) == nil && r["id"] != nil {
fmt.Println(r["id"], r["label"], r["labels"], r["confidence"], r["reason"])
}
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "csv-text-classifier:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
System.out.println(j.get("charged_credits"));
for (String line : j.get("output").get("output").asText().split("\n")) {
if (!line.trim().startsWith("{")) continue;
JsonNode r = M.readTree(line);
if (r.has("id")) System.out.println(r.get("id") + " " + r.path("label").asText(r.path("labels").toString()) + " " + r.get("confidence") + " " + r.path("reason").asText());
}
require "digest"
key = "csv-text-classifier:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
puts [j["charged_credits"], j["truncated"]].inspect
j["output"]["output"].each_line do |line|
next unless line.strip.start_with?("{")
r = JSON.parse(line)
puts [r["id"], r["label"] || r["labels"].join("; "), r["confidence"], r["reason"]].join(" | ") if r["id"]
end
$key = "csv-text-classifier:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
echo $j["charged_credits"], "\n";
foreach (explode("\n", $j["output"]["output"]) as $line) {
$r = json_decode(trim($line), true);
if (is_array($r) && isset($r["id"])) echo $r["id"], " | ", $r["label"] ?? implode("; ", $r["labels"]), " | ", $r["confidence"], " | ", $r["reason"], "\n";
}
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"csv-text-classifier:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
Console.WriteLine(j["charged_credits"]);
foreach (var line in j["output"]!["output"]!.GetValue<string>().Split('\n')) {
if (!line.TrimStart().StartsWith("{")) continue;
var r = JsonNode.Parse(line)!;
if (r["id"] != null) Console.WriteLine($"{r["id"]} {r["label"] ?? r["labels"]} {r["confidence"]} {r["reason"]}");
}
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: csv-text-classifier:classify:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "classify", "labels": "- Billing: ...\n- Bug: ...", "rows": "[1] ...\n[2] ...", "multi_label": "no", "allow_other": "yes"}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "csv-text-classifier:classify:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `csv-text-classifier:classify:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "csv-text-classifier:classify:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "csv-text-classifier:classify:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "csv-text-classifier:classify:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: csv-text-classifier:classify:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"csv-text-classifier:classify:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);