← Thought Reframing / API
Your token

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.

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.

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.

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.

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.

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.

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.

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

HTTPerror.codeWhat it means
401unauthorizedNo 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.
402insufficient_creditsThe 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.
404not_foundWrong 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.
409conflictAn Idempotency-Key already used with a different body. Derive the key from the body, and change it only when the body changes.
422invalid_inputThe 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.
429rate_limitedBack off and retry. Similarity search over the collection is limited to 30/min per IP, tighter than the other data endpoints.
503unavailableUpstream is briefly unavailable. Retry with the same Idempotency-Key — that is what the key is for.