Drive Thought Reframing from your own code
Base URL https://api.skillsafe.ai/v1/app-api. Every response is a
{"data": ...} / {"error": ...} envelope. Pick a language once and
every example on the page follows it.
Before you build on this: Thought Reframing is a thinking aid, not therapy and not a diagnosis. If you are wrapping it in something of your own, carry the safety screen across too — the browser app refuses to analyse text indicating that someone may not be safe and shows crisis resources instead, and an API client that skips that step is a meaningfully worse product than the one it is calling.
1. A tiny client
Every call below is the same shape: a POST or GET to https://api.skillsafe.ai/v1/app-api with an
Authorization: Bearer header, returning a {data} / {error}
envelope. Write the envelope handling once.
# Everything here uses these two values.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="thought-reframing"
# A helper that unwraps the envelope and fails loudly on {error}.
call() { # call METHOD PATH [BODY]
resp=$(curl -sS -X "$1" "$BASE$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
${3:+--data "$3"})
echo "$resp" | python3 -c 'import sys,json;e=json.load(sys.stdin);\
sys.exit("API error: %s" % e["error"]) if e.get("error") else print(json.dumps(e["data"]))'
}import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "thought-reframing"
TOKEN = "YOUR_TOKEN" # from step 2, or from /tokens.html
def call(method, path, body=None, token=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + (token or TOKEN))
# The API returns a Cloudflare 1010 to the default urllib user agent.
req.add_header("User-Agent", "thought-reframing-client/1.0")
with urllib.request.urlopen(req) as r:
env = json.loads(r.read())
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "thought-reframing";
let TOKEN = "YOUR_TOKEN"; // from step 2, or from /tokens.html
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const env = await res.json();
if (env.error) throw new Error(JSON.stringify(env.error));
return env.data;
}package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const Base = "https://api.skillsafe.ai/v1/app-api"
const Slug = "thought-reframing"
var Token = "YOUR_TOKEN" // from step 2, or from /tokens.html
type envelope struct {
Data json.RawMessage `json:"data"`
Error json.RawMessage `json:"error"`
}
func call(method, path string, body any) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, Base+path, rdr)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+Token)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if len(env.Error) > 0 && string(env.Error) != "null" {
return nil, fmt.Errorf("API error: %s", env.Error)
}
return env.Data, nil
}import java.net.URI;
import java.net.http.*;
public class Reframe {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "thought-reframing";
static String token = "YOUR_TOKEN"; // from step 2, or from /tokens.html
static final HttpClient http = HttpClient.newHttpClient();
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = (body == null)
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.method(method, pub)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
String s = res.body();
if (s.contains("\"error\":") && !s.contains("\"error\":null")) {
throw new RuntimeException("API error: " + s);
}
return s; // parse the {"data": ...} envelope with your JSON library
}
}require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "thought-reframing"
TOKEN = "YOUR_TOKEN" # from step 2, or from /tokens.html
def call(method, path, body = nil, token: TOKEN)
uri = URI(BASE + path)
klass = method == "GET" ? Net::HTTP::Get : Net::HTTP::Post
req = klass.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}"
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "API error: #{env["error"]}" if env["error"]
env["data"]
end<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "thought-reframing";
$TOKEN = "YOUR_TOKEN"; // from step 2, or from /tokens.html
function call(string $method, string $path, $body = null) {
global $TOKEN;
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $TOKEN,
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!empty($env["error"])) {
throw new RuntimeException("API error: " . json_encode($env["error"]));
}
return $env["data"];
}using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public static class Reframe {
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "thought-reframing";
static string Token = "YOUR_TOKEN"; // from step 2, or from /tokens.html
static readonly HttpClient Http = new HttpClient();
public static async Task<JsonElement> Call(HttpMethod method, string path, object body = null) {
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Add("Authorization", "Bearer " + Token);
if (body != null) {
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
}
var res = await Http.SendAsync(req);
var env = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
if (env.TryGetProperty("error", out var err) && err.ValueKind != JsonValueKind.Null) {
throw new Exception("API error: " + err.ToString());
}
return env.GetProperty("data");
}
}
2. Get a token
A guest token is minted with no sign-in and is enough for
/me and /estimate. Working through a thought is metered, so it needs a
personal token — sign in at /tokens.html and copy it
from there. Guest identities are per-token: a fresh guest token cannot see records an earlier
guest saved.
TOKEN=$(curl -sS -X POST "$BASE/guest" \
-H "Content-Type: application/json" \
-d "{\"slug\":\"$SLUG\"}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["token"])')
echo "${TOKEN:0:8}..." # a personal token comes from /tokens.html insteaddef guest_token():
req = urllib.request.Request(
BASE + "/guest",
data=json.dumps({"slug": SLUG}).encode(),
method="POST",
)
req.add_header("Content-Type", "application/json")
req.add_header("User-Agent", "thought-reframing-client/1.0")
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["data"]["token"]
TOKEN = guest_token()
print(TOKEN[:8] + "...")async function guestToken() {
const res = await fetch(BASE + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: SLUG })
});
const env = await res.json();
if (env.error) throw new Error(JSON.stringify(env.error));
return env.data.token;
}
TOKEN = await guestToken();
console.log(TOKEN.slice(0, 8) + "...");func guestToken() (string, error) {
b, _ := json.Marshal(map[string]string{"slug": Slug})
res, err := http.Post(Base+"/guest", "application/json", bytes.NewReader(b))
if err != nil {
return "", err
}
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return "", err
}
return env.Data.Token, nil
}static String guestToken() throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/guest"))
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"" + SLUG + "\"}"))
.header("Content-Type", "application/json")
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
// {"data":{"token":"..."}} - pull .data.token with your JSON library
return res.body();
}def guest_token
uri = URI(BASE + "/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.dump({ slug: SLUG })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["data"]["token"]
end
token = guest_token
puts token[0, 8] + "..."<?php
function guest_token(): string {
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => SLUG]),
]);
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
return $env["data"]["token"];
}
$TOKEN = guest_token();
echo substr($TOKEN, 0, 8) . "...";static async Task<string> GuestToken() {
var body = new StringContent("{\"slug\":\"" + Slug + "\"}", Encoding.UTF8, "application/json");
var res = await Http.PostAsync(Base + "/guest", body);
var env = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
return env.GetProperty("data").GetProperty("token").GetString();
}
Token = await GuestToken();
Console.WriteLine(Token.Substring(0, 8) + "...");
3. Check the session — GET /me
Returns exactly three fields: subject_type,
subject_id and credits. There is no name or email on it, so the
signed-in test is subject_type == "user". A 401 here on a token you
never minted is the correct answer, not a fault.
call GET /me
# {"subject_type":"user","subject_id":"usr_...","credits":48210}me = call("GET", "/me")
print(me["subject_type"], me["credits"])
signed_in = me["subject_type"] == "user"const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
const signedIn = me.subject_type === "user";raw, err := call("GET", "/me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int64 `json:"credits"`
}
json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)String me = call("GET", "/me", null);
System.out.println(me);
// {"data":{"subject_type":"user","subject_id":"usr_...","credits":48210}}me = call("GET", "/me")
puts "#{me["subject_type"]} #{me["credits"]}"
signed_in = me["subject_type"] == "user"<?php
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
$signedIn = $me["subject_type"] === "user";var me = await Reframe.Call(HttpMethod.Get, "/me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
Console.WriteLine(me.GetProperty("credits").GetInt64());
4. Price it first — POST /estimate
Free, and it creates no job. It returns
hold_credits (what is reserved, priced against the full output cap),
min_credits and the model binding.
The body you post IS the input object — post the object itself, never a bare
string and never an {"input": ...} wrapper. The endpoint performs no body validation,
so a malformed body returns a plausible-looking estimate and no error at all; validate on your
side.
call POST /estimate '{"thought":"I completely humiliated myself in that meeting","situation":"","feeling":"ashamed","intensity":75,"evidence_for":"","evidence_against":"","what_i_would_tell_a_friend":"","scan":{"claims":[],"markers":[],"patterns":[],"counts":{}}}'
# {"hold_credits":3120,"min_credits":420,"model":"gpt-5.6-terra",
# "model_alias":"gpt-terra","markup_bps":1000}payload = {
"thought": "I completely humiliated myself in that meeting",
"situation": "",
"feeling": "ashamed",
"intensity": 75,
"evidence_for": "",
"evidence_against": "",
"what_i_would_tell_a_friend": "",
"scan": {"claims": [], "markers": [], "patterns": [], "counts": {}},
}
assert isinstance(payload, dict), "the body IS the input object"
est = call("POST", "/estimate", payload)
print(est["hold_credits"], est["model_alias"])const payload = {
thought: "I completely humiliated myself in that meeting",
situation: "",
feeling: "ashamed",
intensity: 75,
evidence_for: "",
evidence_against: "",
what_i_would_tell_a_friend: "",
scan: { claims: [], markers: [], patterns: [], counts: {} }
};
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
throw new TypeError("the body IS the input object");
}
const est = await call("POST", "/estimate", payload);
console.log(est.hold_credits, est.model_alias);payload := map[string]any{
"thought": "I completely humiliated myself in that meeting",
"feeling": "ashamed",
"intensity": 75,
"scan": map[string]any{"claims": []any{}, "markers": []any{}, "patterns": []any{}},
}
raw, err := call("POST", "/estimate", payload)
if err != nil {
panic(err)
}
var est struct {
HoldCredits int64 `json:"hold_credits"`
ModelAlias string `json:"model_alias"`
}
json.Unmarshal(raw, &est)
fmt.Println(est.HoldCredits, est.ModelAlias)String payload = """
{"thought":"I completely humiliated myself in that meeting",
"feeling":"ashamed","intensity":75,
"scan":{"claims":[],"markers":[],"patterns":[],"counts":{}}}
""";
String est = call("POST", "/estimate", payload);
System.out.println(est);payload = {
thought: "I completely humiliated myself in that meeting",
feeling: "ashamed",
intensity: 75,
scan: { claims: [], markers: [], patterns: [], counts: {} }
}
raise "the body IS the input object" unless payload.is_a?(Hash)
est = call("POST", "/estimate", payload)
puts "#{est["hold_credits"]} #{est["model_alias"]}"<?php
$payload = [
"thought" => "I completely humiliated myself in that meeting",
"feeling" => "ashamed",
"intensity" => 75,
"scan" => ["claims" => [], "markers" => [], "patterns" => [], "counts" => (object)[]],
];
$est = call("POST", "/estimate", $payload);
echo $est["hold_credits"], " ", $est["model_alias"], PHP_EOL;var payload = new {
thought = "I completely humiliated myself in that meeting",
feeling = "ashamed",
intensity = 75,
scan = new { claims = new object[0], markers = new object[0], patterns = new object[0] }
};
var est = await Reframe.Call(HttpMethod.Post, "/estimate", payload);
Console.WriteLine(est.GetProperty("hold_credits").GetInt64());
Console.WriteLine(est.GetProperty("model_alias").GetString());
5. Run it — POST /run, then poll
Metered, and it needs a personal token. Always
send an Idempotency-Key derived from the input: a network blip must not bill twice.
The reply is a JSON object matching the output contract below.
KEY="reframe-$(echo -n "$THOUGHT" | shasum | cut -c1-8)-1"
job=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d '{"thought":"I completely humiliated myself in that meeting","situation":"","feeling":"ashamed","intensity":75,"evidence_for":"","evidence_against":"","what_i_would_tell_a_friend":"","scan":{"claims":[],"markers":[],"patterns":[],"counts":{}}}')
echo "$job" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output_text"])'import hashlib
key = "reframe-" + hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()
).hexdigest()[:8] + "-1"
req = urllib.request.Request(
BASE + "/run", data=json.dumps(payload).encode(), method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
req.add_header("User-Agent", "thought-reframing-client/1.0")
with urllib.request.urlopen(req) as r:
out = json.loads(r.read())["data"]
result = json.loads(out["output_text"])
print(result["restatement"])
for rf in result["reframes"]:
print("-", rf["wording"])const key = "reframe-" + [...JSON.stringify(payload)]
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7)
.toString(16) + "-1";
const res = await fetch(BASE + "/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": key
},
body: JSON.stringify(payload)
});
const env = await res.json();
if (env.error) throw new Error(JSON.stringify(env.error));
const result = JSON.parse(env.data.output_text);
console.log(result.restatement);
result.reframes.forEach(r => console.log("-", r.wording));b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", Base+"/run", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Idempotency-Key", "reframe-abc12345-1")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct {
OutputText string `json:"output_text"`
Truncated bool `json:"truncated"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
fmt.Println(env.Data.OutputText)HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.POST(HttpRequest.BodyPublishers.ofString(payload))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "reframe-abc12345-1")
.build();
HttpResponse<String> res = http.send(run, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
// data.output_text holds the JSON described in the contract belowuri = URI(BASE + "/run")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = "reframe-abc12345-1"
req.body = JSON.dump(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
data = JSON.parse(res.body)["data"]
result = JSON.parse(data["output_text"])
puts result["restatement"]
result["reframes"].each { |r| puts "- #{r["wording"]}" }<?php
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $TOKEN,
"Idempotency-Key: reframe-abc12345-1",
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
$result = json_decode($env["data"]["output_text"], true);
echo $result["restatement"], PHP_EOL;
foreach ($result["reframes"] as $r) { echo "- ", $r["wording"], PHP_EOL; }var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run");
req.Headers.Add("Authorization", "Bearer " + Token);
req.Headers.Add("Idempotency-Key", "reframe-abc12345-1");
req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var env = JsonSerializer.Deserialize<JsonElement>(await res.Content.ReadAsStringAsync());
var text = env.GetProperty("data").GetProperty("output_text").GetString();
var result = JsonSerializer.Deserialize<JsonElement>(text);
Console.WriteLine(result.GetProperty("restatement").GetString());
6. Stream it — POST /run-stream
Server-sent events. Same body, same
Idempotency-Key. Accumulate the deltas and parse once the stream closes; if it stops
early, what you have is a truncated JSON object, so keep the partial rather than discarding it.
curl -N -sS -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d '{"thought":"I completely humiliated myself in that meeting","situation":"","feeling":"ashamed","intensity":75,"evidence_for":"","evidence_against":"","what_i_would_tell_a_friend":"","scan":{"claims":[],"markers":[],"patterns":[],"counts":{}}}'req = urllib.request.Request(
BASE + "/run-stream", data=json.dumps(payload).encode(), method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
req.add_header("User-Agent", "thought-reframing-client/1.0")
buf = ""
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
chunk = line[5:].strip()
if chunk and chunk != "[DONE]":
buf += json.loads(chunk).get("delta", "")
result = json.loads(buf)
print(result["restatement"])const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": key,
"Accept": "text/event-stream"
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const chunk = line.slice(5).trim();
if (chunk && chunk !== "[DONE]") text += (JSON.parse(chunk).delta || "");
}
}
console.log(JSON.parse(text).restatement);b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", Base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+Token)
req.Header.Set("Idempotency-Key", "reframe-abc12345-1")
req.Header.Set("Accept", "text/event-stream")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var sb strings.Builder
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
chunk := strings.TrimSpace(line[5:])
if chunk == "" || chunk == "[DONE]" {
continue
}
var d struct {
Delta string `json:"delta"`
}
json.Unmarshal([]byte(chunk), &d)
sb.WriteString(d.Delta)
}
fmt.Println(sb.String())HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.POST(HttpRequest.BodyPublishers.ofString(payload))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "reframe-abc12345-1")
.header("Accept", "text/event-stream")
.build();
StringBuilder sb = new StringBuilder();
http.send(stream, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data:"))
.map(l -> l.substring(5).trim())
.filter(l -> !l.isEmpty() && !l.equals("[DONE]"))
.forEach(sb::append); // each line is {"delta":"..."}
System.out.println(sb);uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = "reframe-abc12345-1"
req["Accept"] = "text/event-stream"
req.body = JSON.dump(payload)
buf = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |seg|
seg.each_line do |line|
next unless line.start_with?("data:")
chunk = line[5..].strip
next if chunk.empty? || chunk == "[DONE]"
buf << (JSON.parse(chunk)["delta"] || "")
end
end
end
end
puts JSON.parse(buf)["restatement"]<?php
$buf = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $TOKEN,
"Idempotency-Key: reframe-abc12345-1",
"Accept: text/event-stream",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $seg) use (&$buf) {
foreach (explode("\n", $seg) as $line) {
if (strpos($line, "data:") !== 0) continue;
$chunk = trim(substr($line, 5));
if ($chunk === "" || $chunk === "[DONE]") continue;
$d = json_decode($chunk, true);
$buf .= $d["delta"] ?? "";
}
return strlen($seg);
},
]);
curl_exec($ch);
curl_close($ch);
$result = json_decode($buf, true);
echo $result["restatement"], PHP_EOL;var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Authorization", "Bearer " + Token);
req.Headers.Add("Idempotency-Key", "reframe-abc12345-1");
req.Headers.Add("Accept", "text/event-stream");
req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var sb = new StringBuilder();
string line;
while ((line = await reader.ReadLineAsync()) != null) {
if (!line.StartsWith("data:")) continue;
var chunk = line.Substring(5).Trim();
if (chunk.Length == 0 || chunk == "[DONE]") continue;
var d = JsonSerializer.Deserialize<JsonElement>(chunk);
if (d.TryGetProperty("delta", out var delta)) sb.Append(delta.GetString());
}
Console.WriteLine(sb.ToString());
7. Saved reframings — the collection
Only records you chose to save exist. The
collection is reframings, scoped to the calling subject. Records nest under
doc: read rec.doc.thought, never rec.thought. The
saved_at field is a declared timestamp and accepts only ISO-8601
with a Z — epoch milliseconds are rejected on write.
# newest first
call POST "/collections/reframings/query" \
'{"sort":{"field":"saved_at","dir":"desc"},"limit":20}'
# delete one
call DELETE "/collections/reframings/records/rec_abc123"rows = call("POST", "/collections/reframings/query",
{"sort": {"field": "saved_at", "dir": "desc"}, "limit": 20})
for rec in rows["records"]:
doc = rec["doc"] # records nest under doc
print(doc["saved_at"][:10], doc["thought"][:70])
# writing one: only ISO-8601 with Z is accepted for saved_at
from datetime import datetime, timezone
stamp = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
call("DELETE", "/collections/reframings/records/rec_abc123")const rows = await call("POST", "/collections/reframings/query", {
sort: { field: "saved_at", dir: "desc" },
limit: 20
});
for (const rec of rows.records) {
const doc = rec.doc; // records nest under doc
console.log(doc.saved_at.slice(0, 10), doc.thought.slice(0, 70));
}
// writing one: Date.now() is rejected, only ISO-8601 with Z works
const saved_at = new Date().toISOString();
await call("DELETE", "/collections/reframings/records/rec_abc123");body := map[string]any{
"sort": map[string]string{"field": "saved_at", "dir": "desc"},
"limit": 20,
}
raw, _ := call("POST", "/collections/reframings/query", body)
var rows struct {
Records []struct {
RecordID string `json:"record_id"`
Doc struct {
Thought string `json:"thought"`
SavedAt string `json:"saved_at"`
} `json:"doc"`
} `json:"records"`
}
json.Unmarshal(raw, &rows)
for _, r := range rows.Records {
fmt.Println(r.Doc.SavedAt[:10], r.Doc.Thought)
}
// only ISO-8601 with Z is accepted on write
stamp := time.Now().UTC().Format(time.RFC3339)String q = """
{"sort":{"field":"saved_at","dir":"desc"},"limit":20}
""";
String rows = call("POST", "/collections/reframings/query", q);
System.out.println(rows);
// each record is {"record_id":"...","doc":{...}} - read fields off .doc
// only ISO-8601 with Z is accepted on write
String stamp = java.time.Instant.now().toString();
call("DELETE", "/collections/reframings/records/rec_abc123", null);rows = call("POST", "/collections/reframings/query",
{ sort: { field: "saved_at", dir: "desc" }, limit: 20 })
rows["records"].each do |rec|
doc = rec["doc"] # records nest under doc
puts "#{doc["saved_at"][0, 10]} #{doc["thought"][0, 70]}"
end
# only ISO-8601 with Z is accepted on write
stamp = Time.now.utc.iso8601
call("DELETE", "/collections/reframings/records/rec_abc123")<?php
$rows = call("POST", "/collections/reframings/query", [
"sort" => ["field" => "saved_at", "dir" => "desc"],
"limit" => 20,
]);
foreach ($rows["records"] as $rec) {
$doc = $rec["doc"]; // records nest under doc
echo substr($doc["saved_at"], 0, 10), " ", substr($doc["thought"], 0, 70), PHP_EOL;
}
// only ISO-8601 with Z is accepted on write
$stamp = gmdate("Y-m-d\TH:i:s\Z");
call("DELETE", "/collections/reframings/records/rec_abc123");var rows = await Reframe.Call(HttpMethod.Post, "/collections/reframings/query", new {
sort = new { field = "saved_at", dir = "desc" },
limit = 20
});
foreach (var rec in rows.GetProperty("records").EnumerateArray()) {
var doc = rec.GetProperty("doc"); // records nest under doc
Console.WriteLine(doc.GetProperty("thought").GetString());
}
// only ISO-8601 with Z is accepted on write
var stamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
await Reframe.Call(HttpMethod.Delete, "/collections/reframings/records/rec_abc123");
The input contract
Only thought is required. Everything else is optional and everything else makes
the answer less generic. scan is the browser lane's findings; an API client may
send an empty scan, and the model will do its own reading of the sentence.
{
"thought": string, // required - the sentence, in the person's own words
"situation": string, // what a camera would have caught
"feeling": string, // their words for how it felt
"intensity": number, // their own 0-100 rating, self-reported, not a measurement
"evidence_for": string,
"evidence_against": string,
"what_i_would_tell_a_friend": string,
"clipped_note": string, // present only if a long thought was trimmed
"scan": {
"claims": [ {"id","text","kind","testable","note"} ],
"markers": [ {"pattern","label","span"} ],
"patterns": [ string ],
"counts": { }
}
}
The output contract
One JSON object. patterns[].id is drawn from a fixed set of eleven, and
patterns[].span must be a verbatim substring of thought — the
app rejects a span it cannot find and shows the user the disagreement. Arrays under
evidence may legitimately be empty; an empty one is an honest answer and a
padded one is not.
{
"restatement": string,
"claims": [
{"id": string, "text": string,
"kind": "event" | "interpretation" | "prediction" | "evaluation",
"testable": boolean, "note": string}
],
"evidence": {"supports": [string], "complicates": [string], "missing": [string]},
"patterns": [
{"id": "absolute" | "mindread" | "forecast" | "selflabel" | "filter" | "shoulds"
| "catastrophe" | "personalise" | "feelingfact" | "compare" | "overgeneral",
"span": string, // verbatim from thought
"why": string} // describes the span, never the person
],
"reframes": [ {"grants": string, "disputes": string, "wording": string} ],
"check": {"not_saying": string, "still_true": string},
"next_step": string // "" is valid when nothing honest presents itself
}
There is one other shape the model can return. If what arrives indicates the person may not
be safe, it returns {"route_to_help": true} and nothing else, and the app shows
crisis resources rather than an analysis. Handle that case before you parse anything else.
Errors
| HTTP | error.code | What it means |
|---|---|---|
401 | unauthorized | No token, or a token that has expired or been revoked. Mint a guest token, or sign in at /tokens.html for a personal one. On a first-ever visit this is the correct response, not a fault. |
402 | insufficient_credits | The balance is below min_credits. Call /estimate first and compare against /me — a 402 after submit is a failure of the client, not of the user. |
404 | not_found | Wrong path, or a record id that does not belong to the calling subject. Collections are scoped per subject and every guest token is a new subject. |
409 | conflict | An Idempotency-Key already used with a different body. Derive the key from the body, and change it only when the body changes. |
422 | invalid_input | The run input failed the model's own contract. Note that /estimate does NOT validate the body, so a body that estimates cleanly can still fail here. |
429 | rate_limited | Back off and retry. Similarity search over the collection is limited to 30/min per IP, tighter than the other data endpoints. |
503 | unavailable | Upstream is briefly unavailable. Retry with the same Idempotency-Key — that is what the key is for. |