Drive Need to Know from your own code
The one metered task, plan, is available over HTTP. It takes what the surprise is, who it is kept from, the days left until the reveal, the three need-to-know rings, the shared channels you ticked, your worries, your cover story and the page's flags. It returns a threat model of the secret: a leak per channel with a risk level and a plug, a verdict, briefs, six field rules, a check of the cover story, a countdown and a fallback.
The division of labour first: the page owns the rings, the flags, the dial's loudness estimate and the verdict rule; the model owns the risk reading and the wording of the plan; the page re-checks the reply.
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":"need-to-know"}), 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 (task, surprise, for_whom, days_left, planning, helping, told, channels, worries, cover, page_scan) 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: a plan 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: plan (the one task is plan); a missing or different one is still answered as plan. The verdict is a rule on the leaks rated high: lockdown for 3 or more, patch for 1 or 2, airtight for none. A high leak is one the person kept in the dark would meet in the normal course of their day without looking.
Input fields (every value is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always plan. |
surprise | yes | What the surprise is (at most 140 characters). |
for_whom | yes | Who it is kept from (at most 60), or (not given). |
days_left | yes | Whole days until the reveal, 0 to 365. |
planning / helping / told | yes | The three rings: one person per line as Name or Name - note (at most 12 per ring), or (nobody). |
channels | yes | One ticked channel per line, <id> | <label>, or (none ticked). Ids: calendar, card, orders, parcels, inbox, location, groupchat, photos, device, speaker, lockscreen, social, kids, phone. |
worries / cover | yes | Anything else that worries you, and your cover story (at most 300 each), or (not given). |
page_scan | yes | The page's flags, <n> | <child|housemate|two-rings|big-circle|long-fuse> | <name or detail> per line, or (none). |
clip_note | no | What the page cut, if anything. |
retry_note | no | Only on the automatic reformat retry. |
The reply
One JSON object, in output.output of the finished job:
{"task": "plan", "title": "...", "codename": "Operation ...",
"leaks": [{"channel": "<channel id> | other", "risk": "high | medium | low", "how": "...", "plug": "..."}],
"verdict": "airtight | patch | lockdown", "verdict_reason": "...",
"circle": [{"name": "...", "ring": "planning | helping | told", "brief": "..."}],
"field_rules": ["...", "...", "...", "...", "...", "..."],
"cover_story": "...", "cover_holes": ["..."],
"countdown": [{"days_before": 7, "do": "..."}], "if_rumbled": "..."}
| Field | What it holds |
|---|---|
leaks | One per ticked channel in order, plus up to two other leaks from the worries; each with a risk, how it leaks (at most 22 words) and a plug (at most 22). |
verdict / verdict_reason | The rule above, and one sentence naming the high leaks. |
circle | A brief (at most 20 words) for every person the page flagged as a child, a housemate or listed twice, then up to three more. |
field_rules | Exactly six spy-style rules of at most 12 words. |
cover_story / cover_holes | Your cover tightened into one line (or a note that none was given) and 1 to 3 places it could crack (none when no cover was given). |
countdown | 3 to 6 steps, integer days_before from largest down to 0, no weekday or clock time. |
if_rumbled / codename / title | The fallback if the person gets suspicious (at most 40 words), an Operation ... codename and a playful title. |
The page then checks the reply against the form (HushKit.check): the verdict against the rule, a leak for every ticked channel and none for an unticked one, briefs for every flagged person in the ring you gave them, six field rules, cover holes only when a cover was given, the countdown inside the days you have and ending on the reveal day, the word limits, no he or she and no internal field names.
Worked examples, one per verdict
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover lockdown, patch and airtight.
The fortieth - lockdown
Input:
{
"task": "plan",
"surprise": "A surprise 40th birthday party at the community hall",
"for_whom": "My partner, Sam",
"days_left": "24",
"planning": "Priya - co-host, Sam's sister\nTom - booking the hall",
"helping": "Leo - our son, 7, making a banner\nAna - baking the cake",
"told": "Ben\nChloe\nDev\nEllie\nFemi\nGus\nHana\nIvo",
"channels": "calendar | A shared calendar\ncard | A joint bank account or card\norders | A shared shopping account\ngroupchat | A group chat they are in\nlockscreen | Notifications on a lock screen they can see\nsocial | Guests who post or tag online\nkids | Children who know",
"worries": "Sam sometimes borrows my laptop for work emails.",
"cover": "Priya takes Sam out for a quiet dinner, then needs to stop by the hall to pick something up.",
"page_scan": "1 | child | Leo\n2 | big-circle | 12 people know"
}
Reply:
{
"task": "plan",
"title": "Keeping Sam's fortieth a surprise at the hall",
"codename": "Operation Forty Hall",
"leaks": [
{
"channel": "calendar",
"risk": "high",
"how": "A calendar entry about the hall booking or party could appear on the shared calendar Sam also uses.",
"plug": "Keep the hall booking and party plans off the shared calendar; Tom should track dates in a private note instead."
},
{
"channel": "card",
"risk": "medium",
"how": "Anything bought or paid for the surprise could appear as a line on the joint card statement.",
"plug": "Keep anything bought for the surprise off the joint card; use a separate account instead."
},
{
"channel": "orders",
"risk": "medium",
"how": "Anything bought for the surprise could sit in the shared shopping account's order history if Sam happens to browse it.",
"plug": "Keep anything bought for the surprise off the shared shopping account; use a separate account instead."
},
{
"channel": "groupchat",
"risk": "high",
"how": "A message about the party sent in the group chat Sam is also in could reveal the surprise.",
"plug": "Move planning talk for the party out of that group chat into a separate thread without Sam."
},
{
"channel": "lockscreen",
"risk": "high",
"how": "A notification preview about the party could appear on a lock screen Sam can see.",
"plug": "Turn off notification previews on any device whose lock screen Sam can see before the reveal."
},
{
"channel": "social",
"risk": "medium",
"how": "A guest could post or tag something about the party online where Sam might see it.",
"plug": "Ask the wider group who know not to post or tag anything online before the reveal."
},
{
"channel": "kids",
"risk": "high",
"how": "Leo already knows about the party and could mention it to Sam without meaning to.",
"plug": "Give Leo a simple reason to keep the banner secret, or keep Leo away from Sam while making it."
},
{
"channel": "other",
"risk": "medium",
"how": "Sam sometimes borrows the laptop for work emails and could come across the party planning open on it.",
"plug": "Keep party planning off the laptop Sam borrows, or close everything and sign out before Sam uses it."
}
],
"verdict": "lockdown",
"verdict_reason": "Four leaks are high: the shared calendar, the group chat, lock-screen notifications, and Leo already knowing.",
"circle": [
{
"name": "Leo",
"ring": "helping",
"brief": "Make the banner where Sam won't see it, and keep it secret for now, like a surprise game."
},
{
"name": "Priya",
"ring": "planning",
"brief": "Lead the quiet dinner cover story and keep Sam away from the hall until it is time."
},
{
"name": "Tom",
"ring": "planning",
"brief": "Keep the hall booking off the shared calendar and don't mention it in the group chat."
},
{
"name": "Ana",
"ring": "helping",
"brief": "Keep baking the cake quiet, and avoid mentioning it in chats or on shared accounts Sam sees."
}
],
"field_rules": [
"Assume every screen Sam uses could show the surprise.",
"Keep the hall booking and party talk off the calendar.",
"Never discuss the party in the shared group chat.",
"Silence lock-screen previews on every device Sam can see.",
"Keep spending on the surprise off the joint card.",
"Trust Leo with only the banner, nothing more."
],
"cover_story": "Priya is taking Sam out for a quiet dinner, and they'll need to swing by the hall afterward to pick something up.",
"cover_holes": [
"Stopping by the hall right before the reveal day is a pretty specific detour for just picking something up.",
"If Sam asks what's being picked up, Priya needs a consistent answer ready."
],
"countdown": [
{
"days_before": 24,
"do": "Agree the dinner-and-hall cover story with Priya and Tom before anyone else."
},
{
"days_before": 14,
"do": "Tom keeps the hall booking off the shared calendar and group chat."
},
{
"days_before": 7,
"do": "Turn off lock-screen notification previews on devices Sam can see."
},
{
"days_before": 3,
"do": "Move any spending on the party off the joint card and shopping account."
},
{
"days_before": 1,
"do": "Remind the wider group not to post or tag anything online."
},
{
"days_before": 0,
"do": "Priya leads Sam to the hall for the reveal under the dinner cover."
}
],
"if_rumbled": "If Sam seems suspicious, Priya can lean harder on the dinner cover story, or Priya and Tom can quietly bring Sam in early and finish the party as a smaller, planned reveal."
}
The ring - patch
Input:
{
"task": "plan",
"surprise": "A marriage proposal on our weekend away",
"for_whom": "Jo",
"days_left": "40",
"planning": "Mia - Jo's best friend, checked the ring size",
"helping": "(nobody)",
"told": "Jo's parents",
"channels": "card | A joint bank account or card\nparcels | Parcels delivered to a shared home\nlocation | Location sharing",
"worries": "Jo usually opens the post when it arrives.",
"cover": "It is just the weekend away we already booked for our anniversary.",
"page_scan": "(none)"
}
Reply:
{
"task": "plan",
"title": "A Weekend Away With A Secret Ring",
"codename": "Operation Ring Weekender",
"leaks": [
{
"channel": "card",
"risk": "medium",
"how": "Anything bought for the proposal could show as a line on the joint bank account or card Jo also sees.",
"plug": "Keep any purchase for the surprise on a separate account, or check the joint statement before Jo does."
},
{
"channel": "parcels",
"risk": "high",
"how": "Jo usually opens the post, so anything delivered for the surprise to your shared home could be found before the reveal.",
"plug": "Have anything delivered for the surprise sent to Mia's address or held for pickup instead of home."
},
{
"channel": "location",
"risk": "medium",
"how": "If location sharing is on, Jo could notice trips linked to planning the proposal or the ring.",
"plug": "Turn off location sharing, or pause it before any trip connected to planning the proposal."
}
],
"verdict": "patch",
"verdict_reason": "One leak is high risk: deliveries to your shared home, since Jo usually opens the post before anyone else does.",
"circle": [
{
"name": "Mia",
"ring": "planning",
"brief": "Keep the ring and proposal secret; receive any deliveries for it instead of the shared home."
},
{
"name": "Jo's parents",
"ring": "told",
"brief": "Know about the proposal but say nothing to Jo, and avoid mentioning it near Jo."
}
],
"field_rules": [
"Never mention the ring or proposal near the joint card.",
"Keep any purchase for it off the joint account.",
"Redirect deliveries for it away from your shared home.",
"Turn off location sharing before any proposal-related trip.",
"Only the planning ring discusses the ring or proposal.",
"Assume Jo opens post before anyone else does."
],
"cover_story": "It's just the weekend away we already booked for our anniversary.",
"cover_holes": [
"Jo's parents already know, so an odd comment from them could puncture the anniversary story.",
"Mia checked the ring size, so a slip from Mia would not fit a plain anniversary trip.",
"Anything delivered for the proposal to your shared home would not fit a plain anniversary trip either."
],
"countdown": [
{
"days_before": 30,
"do": "Keep the ring and proposal off the joint card."
},
{
"days_before": 14,
"do": "Redirect any deliveries for the proposal to Mia instead of your shared home."
},
{
"days_before": 3,
"do": "Turn off location sharing and agree the anniversary cover story with Mia."
},
{
"days_before": 0,
"do": "Make the proposal and let the cover story go once the reveal happens."
}
],
"if_rumbled": "If Jo grows suspicious, lean on the anniversary cover story first; if that doesn't hold, let Jo in on the proposal early rather than risk an awkward reveal."
}
The bike - airtight
Input:
{
"task": "plan",
"surprise": "A restored vintage bicycle as a birthday present",
"for_whom": "Grandad",
"days_left": "12",
"planning": "Nina - my sister, splitting the cost",
"helping": "Raj - fixing the brakes",
"told": "(nobody)",
"channels": "photos | A shared photo library\nphone | Vendors who call a home number",
"worries": "(not given)",
"cover": "(not given)",
"page_scan": "(none)"
}
Reply:
{
"task": "plan",
"title": "A Quiet Birthday Bicycle for Grandad",
"codename": "Operation Silent Spokes",
"leaks": [
{
"channel": "photos",
"risk": "medium",
"how": "A photo of the bicycle being restored or its brakes fixed could land in the shared library Grandad can browse.",
"plug": "Ask Nina and Raj to keep any bicycle or brake photos out of the shared photo library until the reveal."
},
{
"channel": "phone",
"risk": "medium",
"how": "A vendor calling about the restored bicycle on the home phone could mention it if Grandad happens to answer.",
"plug": "Ask vendors calling about the bicycle to reach Nina or Raj directly instead of the home phone."
}
],
"verdict": "airtight",
"verdict_reason": "Zero leaks are high risk; the photos and phone channels are medium, needing a look or a chance call rather than everyday noticing.",
"circle": [
{
"name": "Nina",
"ring": "planning",
"brief": "Keep bicycle or brake photos off the shared photo library and tell vendors to call you, not the home phone."
},
{
"name": "Raj",
"ring": "helping",
"brief": "Avoid posting brake repair photos to the shared library and have any vendor call you instead of the home phone."
}
],
"field_rules": [
"Keep bicycle and brake photos out of shared albums.",
"Tell vendors to call Nina or Raj, not home.",
"Say nothing about the bicycle near a shared phone.",
"Assume any shared photo library can be seen anytime.",
"Keep repair talk between Nina and Raj only.",
"Vary no routine that Grandad would notice."
],
"cover_story": "No cover story was given. Nina should agree one ordinary reason for the reveal day before Grandad can ask.",
"cover_holes": [],
"countdown": [
{
"days_before": 12,
"do": "Ask Nina and Raj to keep bicycle and brake photos off the shared library."
},
{
"days_before": 8,
"do": "Have vendors call Nina or Raj instead of the home phone."
},
{
"days_before": 3,
"do": "Decide on an ordinary reason for the reveal day."
},
{
"days_before": 0,
"do": "Reveal the restored bicycle to Grandad."
}
],
"if_rumbled": "If Grandad grows suspicious, Nina and Raj should stick to the plan: keep photos off the shared library, keep vendor calls off the home phone, and say nothing more than necessary."
}
Build the input yourself
Load the script in node (global.window = global; require("./hushkit.js")), then:
const K = HushKit;
const form = K.form({ surprise: "A restored bicycle as a birthday present", forWhom: "Grandad", days: "12",
planning: "Nina - splitting the cost", channels: ["photos", "phone"] });
K.flags(form); // [] - no child, housemate, double listing, big circle or long fuse
const built = K.buildInput(form);
built.provisional; // {verdict: "airtight", loud: [], why: "0 loud channels on the dial ..."}
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the plan
// K.ics(built, reply, new Date()) // the countdown as a calendar file
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://need-to-know.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://need-to-know.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"]
// need-to-know.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://need-to-know.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://need-to-know.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
}
// NeedToKnow.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 NeedToKnow {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://need-to-know.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://need-to-know.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://need-to-know.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://need-to-know.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":"need-to-know"}' | 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": "need-to-know"})["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: "need-to-know" })).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": "need-to-know"}, 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", "need-to-know"), 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: "need-to-know" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "need-to-know"])["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 = "need-to-know" }))!["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": "plan", "surprise": "A restored bicycle as a birthday present", "for_whom": "Grandad", "days_left": "12", "planning": "Nina - splitting the cost", "helping": "(nobody)", "told": "(nobody)", "channels": "photos | A shared photo library", "worries": "(not given)", "cover": "(not given)", "page_scan": "(none)"}
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": "plan", "surprise": "A restored bicycle as a birthday present", "for_whom": "Grandad", "days_left": "12", "planning": "Nina - splitting the cost", "helping": "(nobody)", "told": "(nobody)", "channels": "photos | A shared photo library", "worries": "(not given)", "cover": "(not given)", "page_scan": "(none)"}
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: "plan", surprise, for_whom, days_left, planning, helping, told, channels, worries, cover, page_scan, ...} (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 need-to-know:<task>:<inputHash>:a<attempt> (the one task is plan), 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="need-to-know:plan:$(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 = "need-to-know:" + 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("title"))
import { createHash } from "node:crypto";
const key = `need-to-know:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "need-to-know:" + 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 = "need-to-know:" + 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 = "need-to-know:#{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 = "need-to-know:" . $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 = $"need-to-know:{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: need-to-know:plan:$(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": "plan", "surprise": "A restored bicycle as a birthday present", "for_whom": "Grandad", "days_left": "12", "planning": "Nina - splitting the cost", "helping": "(nobody)", "told": "(nobody)", "channels": "photos | A shared photo library", "worries": "(not given)", "cover": "(not given)", "page_scan": "(none)"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "need-to-know:plan:" + 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": `need-to-know:plan:${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", "need-to-know:plan:"+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", "need-to-know:plan:" + 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" => "need-to-know:plan:#{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: need-to-know:plan:" . 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", $"need-to-know:plan:{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);