Drive Clear to Share from your own code
The one metered lane, screen, is available over HTTP. It takes a conversation split into messages and returns a verdict - keep-private, redact-first, clear or needs-chat - with one finding per personal or business detail: the message it sits in (msg), the exact characters (quote), its kind (secret, contact, whereabouts, health, money, work, people), its risk (high, medium, low), a reason and a neutral placeholder (swap). A natural use is a support tool that screens a ticket thread before it is pasted into a public bug tracker.
The division of labour first: the page splits the chat and scans the patterns; the model finds the details a pattern cannot see; the page builds the redacted copy and re-checks the reply. All of it comes from sharekit.js, which runs in node as well as in the browser. ShareKit.buildInput splits on speaker labels (or blank lines), keeps up to 40 messages of up to 1,500 characters, and lists the patterns found in each. ShareKit.redacted swaps every quote for its placeholder everywhere it appears and masks any pattern the reply missed. ShareKit.check holds the reply to the chat: quotes that are not in their message, patterns no finding covers, the verdict recomputed from the reply's own findings, and a flagged detail repeated in the reply's prose.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"clear-to-share"}), 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 field such as task or messages 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: the screening lane is metered and needs a personal token. |
| 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 and the verdicts
Every run sends task: screen, the only lane; a missing or unknown task is treated as screen. Each finding gets a kind and a risk first (both checked in the order listed in the prompt, the first that fits wins); the verdict then follows from them by four ordered, non-overlapping rules:
| Order | Verdict | When |
|---|---|---|
| 1 | needs-chat | All the messages together hold fewer than twelve words. findings is empty; tips holds two or three suggestions for what to paste. |
| 2 | keep-private | At least one finding is a secret (a password, code, key, card or account number), or at least three findings are high risk. |
| 3 | redact-first | At least one finding is high or medium risk. |
| 4 | clear | Everything else: no findings, or only low ones. |
The declared input schema carries scalars only, so the array-valued messages field travels JSON-encoded as a string (ShareKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | screen. Required. |
messages | string (JSON) | [{id, who, text, hits}] - ids M1.., up to 40 messages of up to 1,500 characters; who is the speaker label or empty; hits are the page's pattern hints: email, phone, card, password, code, key, address, link-token, ip, handle, birthdate. Required. |
ok_to_show | string | Details the sharer wants left in, comma separated, as typed. Never flagged. Optional. |
clip_note, retry_note | string | What the page cut, when it cut something; and only on a reformat retry, what was wrong. |
The reply
The job's output.output is a string holding one JSON object: task, verdict, title, summary, findings ([{msg, quote, kind, risk, why, swap}] - in message order, each detail once at its first appearance, at most 24) and tips (only for needs-chat). The prose fields never repeat a flagged detail. Parse with ShareKit.parseReply, then normalize(reply), then check(reply, form) for the page's notes and redacted(form, reply) for the copy to share.
Worked examples
Replies produced against the app's prompt for the four sample chats (made-up people, places and numbers; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict and every risk level.
screen - wifi help with a router password, an address and a phone number: keep-private
Input:
{
"task": "screen",
"ok_to_show": "WrenNest",
"messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"My parents' wifi keeps dropping and I'm setting it up for them this weekend. The router admin password is Harbor-Lantern-58 and the network is called WrenNest.\",\"hits\":[\"password\"]},{\"id\":\"M2\",\"who\":\"Assistant\",\"text\":\"Thanks. First, log in to the router and check which channel it uses. Crowded channels are the usual cause of drops.\",\"hits\":[]},{\"id\":\"M3\",\"who\":\"You\",\"text\":\"Ok. Their house is 27 Wren Close and the router sits in the back bedroom. If the provider calls, their landlin ... (720 characters)"
}
Reply:
{
"task": "screen",
"verdict": "keep-private",
"title": "Setting up parents' wifi password",
"summary": "This chat has three findings, one of them a password, so it's best kept private. The street address and phone number would let a stranger find or contact the parents directly, and that password is worth changing anyway, since this chat already holds it.",
"findings": [
{
"msg": "M1",
"quote": "Harbor-Lantern-58",
"kind": "secret",
"risk": "high",
"why": "Anyone who read this could log into the router and change its settings or network.",
"swap": "[router password]"
},
{
"msg": "M3",
"quote": "27 Wren Close",
"kind": "contact",
"risk": "high",
"why": "A stranger would know exactly which house to go to.",
"swap": "[street address]"
},
{
"msg": "M3",
"quote": "01632 960 418",
"kind": "contact",
"risk": "high",
"why": "A stranger could call the house directly.",
"swap": "[phone number]"
}
],
"tips": []
}
screen - advice about a coworker, with names, an employer, an unannounced plan and an email: redact-first
Input:
{
"task": "screen",
"ok_to_show": "",
"messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"I need help writing a message to my coworker Dana Quellwick. Dana keeps taking credit for my reports in the Monday meeting.\",\"hits\":[]},{\"id\":\"M2\",\"who\":\"Assistant\",\"text\":\"That sounds frustrating. Do you want the message to be direct or gentle?\",\"hits\":[]},{\"id\":\"M3\",\"who\":\"You\",\"text\":\"Direct but kind. We both work at Brindlequay Freight and the team is about to be split in a reorg that hasn't been announced yet. My work email is sam.k@brindlequayfreight.example if that matters.\",\"hits\":[\"ema ... (710 characters)"
}
Reply:
{
"task": "screen",
"verdict": "redact-first",
"title": "Writing a direct but kind message",
"summary": "This chat names the coworker in two forms, identifies the employer, spells out an unannounced company plan, includes a work email address, and describes a workplace dispute - six findings in all, none of them a secret. The placeholders make the chat safe to share.",
"findings": [
{
"msg": "M1",
"quote": "Dana Quellwick",
"kind": "people",
"risk": "medium",
"why": "A stranger could identify the coworker by name and link this dispute to a real person.",
"swap": "[the coworker]"
},
{
"msg": "M1",
"quote": "Dana",
"kind": "people",
"risk": "low",
"why": "The first name alone still points back to the same coworker if it isn't masked too.",
"swap": "[the coworker]"
},
{
"msg": "M1",
"quote": "taking credit for my reports in the Monday meeting",
"kind": "work",
"risk": "medium",
"why": "This spells out a specific internal dispute that could embarrass the coworker if it became public.",
"swap": "[the workplace dispute]"
},
"... (6 in all)"
],
"tips": []
}
screen - a squeaky door hinge with one town named: clear
Input:
{
"task": "screen",
"ok_to_show": "",
"messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"My bedroom door squeaks every time I open it. It's an old flat in Leeds, so every door squeaks. What can I use?\",\"hits\":[]},{\"id\":\"M2\",\"who\":\"Assistant\",\"text\":\"Lift the hinge pin out, wipe it, and coat it with a little petroleum jelly or a drop of light machine oil. Tap it back in and swing the door a few times.\",\"hits\":[]},{\"id\":\"M3\",\"who\":\"You\",\"text\":\"Does cooking oil work?\",\"hits\":[]},{\"id\":\"M4\",\"who\":\"Assistant\",\"text\":\"It can for a while, but it turns sticky and gathers dust. ... (577 characters)"
}
Reply:
{
"task": "screen",
"verdict": "clear",
"title": "Bedroom door squeaks and oil",
"summary": "This is a short how-to chat about quieting a squeaky door, with a reply comparing household oils. A city name shows roughly where the flat is, but on its own it's too general to single anyone out. Nothing in the chat points to a person, so it looks fine to share as is.",
"findings": [
{
"msg": "M1",
"quote": "Leeds",
"kind": "whereabouts",
"risk": "low",
"why": "A stranger could learn roughly which city the sharer's home is in.",
"swap": "[the city]"
}
],
"tips": []
}
screen - one line only: needs-chat
Input:
{
"task": "screen",
"ok_to_show": "",
"messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"can you check this before I post it?\",\"hits\":[]}]"
}
Reply:
{
"task": "screen",
"verdict": "needs-chat",
"title": "Check this before you post",
"summary": "There's not enough here yet to screen for anything risky. Paste the full conversation so it can be checked before you post it.",
"findings": [],
"tips": [
"Paste the whole conversation, not just one line.",
"Keep each message's speaker label when you paste it.",
"Include the part you actually plan to share."
]
}
Build the input yourself
Load the script in node (global.window = global; require("./sharekit.js")), then:
const K = ShareKit;
const form = K.form({ ok: "my first name",
chat: "You: Can you help me reply to my landlord?\nAssistant: Sure - what is it about?\nYou: The boiler. My number is 01632 960 000." });
console.log(K.flags(form), K.allHits(form), K.lean(form)); // the flags, every pattern found, the page's lean
console.log(K.redacted(form, null)); // the free quick mask - patterns only
const wire = K.toWire(K.buildInput("screen", form).input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, form); // the page's re-check of the reply
// K.redacted(form, reply) // the copy to share
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. 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://clear-to-share.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://clear-to-share.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"]
// clear-to-share.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://clear-to-share.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://clear-to-share.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
}
// ClearToShare.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 ClearToShare {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://clear-to-share.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://clear-to-share.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://clear-to-share.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://clear-to-share.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":"clear-to-share"}' | 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": "clear-to-share"})["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: "clear-to-share" })).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": "clear-to-share"}, 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", "clear-to-share"), 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: "clear-to-share" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "clear-to-share"])["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 = "clear-to-share" }))!["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": "screen", "ok_to_show": "", "messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"I need help writing a message to my coworker Dana Quellwick. Dana keeps taking credit for my reports in the Monday meeting.\",\"hits\":[]}, ...]"}
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": "screen", "ok_to_show": "", "messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"I need help writing a message to my coworker Dana Quellwick. Dana keeps taking credit for my reports in the Monday meeting.\",\"hits\":[]}, ...]"}
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: "screen", ok_to_show, messages, ... (see the field 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 clear-to-share:<task>:<inputHash>:a<attempt> (the one lane is screen), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="clear-to-share:screen:$(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.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "clear-to-share:" + 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)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `clear-to-share:${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 reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "clear-to-share:" + 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"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "clear-to-share:" + 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()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "clear-to-share:#{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
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "clear-to-share:" . $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"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"clear-to-share:{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>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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: clear-to-share:screen:$(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": "screen", "ok_to_show": "", "messages": "[{\"id\":\"M1\",\"who\":\"You\",\"text\":\"I need help writing a message to my coworker Dana Quellwick. Dana keeps taking credit for my reports in the Monday meeting.\",\"hits\":[]}, ...]"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "clear-to-share:screen:" + 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": `clear-to-share:screen:${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", "clear-to-share:screen:"+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", "clear-to-share:screen:" + 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" => "clear-to-share:screen:#{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: clear-to-share:screen:" . 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", $"clear-to-share:screen:{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);