Flux Desk — API

Write the flux commentary from your general ledger export, your close checklist or a month-end cron job.

API tokens Open the app

Write the commentary from your own close pack

Send the measured facts — every flagged line with both variances in amount and percent, the favourability computed from the caption's polarity, the rule that flagged it, its share of the gross movement, the activity-note lines the engine matched to it, the subtotals that do not foot and the lines that are decisive on their own — and get back one JSON object: drivers (exactly one entry per id in facts.flagged_ids, each sourced either to the activity notes or explicitly unclear), period_summary, watch_items, questions and unverified. The binding constraint is the interesting one: a line the notes never mention may not be explained, only returned as unclear for the controller — and that is not a request, it is checked. All of it is mechanically checkable, and the checker ships with the app: /fluxkit.js is plain ES5 with no dependencies and no network calls, so your pipeline can compute the same facts and run the same reconciliation — the partition counted rather than sampled, every invented driver named, and every currency and percentage figure traced back to something measured at the precision it was written to — before a sentence ever reaches a close pack. Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#; pick a language once and the whole page follows.

Basics

Base URL https://api.skillsafe.ai/v1/app-api, app slug flux-desk. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"data": …} on success, {"error": {"code", "message"}} on failure. Drivers are written by the gpt-terra model alias (currently gpt-5.6-terra) at a publisher markup of 1000 bps — 10%. Credits are units of 1/10 000 of a US dollar, so 10 000 credits is $1.00. /estimate, /me and /guest are free; /run and /run-stream are metered. Run input caps at 1 MB of JSON.

POST /v1/app-api/guest
GET /v1/app-api/me
POST /v1/app-api/estimate
POST /v1/app-api/run
GET /v1/app-api/jobs/{id}
POST /v1/app-api/run-stream
POST /v1/app-api/collections/commentaries/records
POST /v1/app-api/collections/commentaries/query
POST /v1/app-api/collections/commentaries/similar

Error codes

codestatuswhat it means
unauthorized401Missing or stale token. Mint a guest token or sign in again.
forbidden403The token belongs to a different app.
payment_required402Balance below min_credits. Call /estimate first and compare against /me.
validation_error400Malformed body. error.details names the field. A where value that is not an operator object lands here.
rate_limited429Back off. /similar is 30 req/min per IP, tighter than the other data endpoints.
not_found404Unknown job or record id.
internal5xxRetry with the SAME Idempotency-Key — it returns the original job instead of billing again.

The whole deterministic half needs no API at all. /fluxkit.js parses the pack, computes every variance, applies materiality, foots the subtotals and writes the commentary document, both CSVs and the measurement JSON in a browser tab or in Node with no account and no network. Only the driver sentences are metered.

Step 1 · Get a token

Two ways in. A personal token bills your own wallet and can run; a guest token is free to mint, can call /me and the free /estimate, and is enough to assert the model binding in CI. Never paste a token into source control — read it from your shell or your secret store, and use the token page rather than the DevTools console to find the one this browser already holds.

# Option A - take the token this browser already holds: open /tokens.html,
# press "Copy shell export", and paste the line it prints.
export SKILLSAFE_TOKEN="aut_xxxxxxxxxxxxxxxxxxxx"

# Option B - mint a guest token with no browser at all. A guest can call /me and
# the free /estimate, which is enough to verify the model binding; writing the
# drivers needs a personal token so it bills your own wallet.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H 'Content-Type: application/json' \
  -d '{"slug":"flux-desk"}'
# => {"data":{"token":"aut_...","subject_type":"guest","credits":0}}
import json, urllib.request

BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "flux-desk"

def call(path, body=None, token=None, method=None):
    data = None if body is None else json.dumps(body).encode()
    req = urllib.request.Request(BASE + path, data=data,
                                 method=method or ("POST" if data else "GET"))
    req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", "Bearer " + token)
    with urllib.request.urlopen(req) as r:
        payload = json.load(r)
    if "error" in payload:
        raise RuntimeError(payload["error"]["code"] + ": " + payload["error"]["message"])
    return payload["data"]

# Paste a personal token from /tokens.html, or read it from your secret store.
# Falling back to a guest token keeps the free calls working with no account.
MY_TOKEN = "YOUR_TOKEN"
TOKEN = MY_TOKEN if MY_TOKEN != "YOUR_TOKEN" else call("/guest", {"slug": SLUG})["token"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "flux-desk";

async function call(path, body, opts = {}) {
  const res = await fetch(BASE + path, {
    method: opts.method || (body ? "POST" : "GET"),
    headers: {
      "Content-Type": "application/json",
      ...(opts.token ? { Authorization: "Bearer " + opts.token } : {}),
      ...(opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : {})
    },
    body: body ? JSON.stringify(body) : undefined
  });
  const payload = await res.json();
  if (payload.error) throw new Error(payload.error.code + ": " + payload.error.message);
  return payload.data;
}

// A personal token pasted from /tokens.html, or a guest token for the free calls.
const TOKEN = MY_TOKEN || (await call("/guest", { slug: SLUG })).token;
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
)

const base = "https://api.skillsafe.ai/v1/app-api"
const slug = "flux-desk"

type envelope struct {
	Data  json.RawMessage `json:"data"`
	Error *struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func call(path string, body any, token, idem string) (json.RawMessage, error) {
	var buf bytes.Buffer
	method := "GET"
	if body != nil {
		method = "POST"
		if err := json.NewEncoder(&buf).Encode(body); err != nil {
			return nil, err
		}
	}
	req, err := http.NewRequest(method, base+path, &buf)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	if idem != "" {
		req.Header.Set("Idempotency-Key", idem)
	}
	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 env.Error != nil {
		return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
	}
	return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*;

class Flux {
  static final String BASE = "https://api.skillsafe.ai/v1/app-api";
  static final String SLUG = "flux-desk";
  static final HttpClient HTTP = HttpClient.newHttpClient();
  static final ObjectMapper M = new ObjectMapper();

  static JsonNode call(String path, Object body, String token, String idem) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
        .header("Content-Type", "application/json");
    if (token != null) b.header("Authorization", "Bearer " + token);
    if (idem != null) b.header("Idempotency-Key", idem);
    b = body == null
        ? b.GET()
        : b.POST(HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
    JsonNode env = M.readTree(HTTP.send(b.build(),
        HttpResponse.BodyHandlers.ofString()).body());
    if (env.has("error")) {
      throw new RuntimeException(env.at("/error/code").asText()
          + ": " + env.at("/error/message").asText());
    }
    return env.get("data");
  }
}
require "json"
require "net/http"

BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "flux-desk"

def call(path, body = nil, token: nil, idem: nil, method: nil)
  uri = URI(BASE + path)
  klass = (method || (body ? "POST" : "GET")) == "POST" ? Net::HTTP::Post : Net::HTTP::Get
  req = klass.new(uri)
  req["Content-Type"] = "application/json"
  req["Authorization"] = "Bearer #{token}" if token
  req["Idempotency-Key"] = idem if idem
  req.body = JSON.dump(body) if body
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise "#{payload['error']['code']}: #{payload['error']['message']}" if payload["error"]
  payload["data"]
end

MY_TOKEN = "YOUR_TOKEN"
TOKEN = MY_TOKEN == "YOUR_TOKEN" ? call("/guest", { "slug" => SLUG })["token"] : MY_TOKEN
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "flux-desk";

function call(string $path, $body = null, ?string $token = null, ?string $idem = null) {
    $headers = ["Content-Type: application/json"];
    if ($token) { $headers[] = "Authorization: Bearer " . $token; }
    if ($idem)  { $headers[] = "Idempotency-Key: " . $idem; }
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_CUSTOMREQUEST  => $body === null ? "GET" : "POST",
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (isset($payload["error"])) {
        throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
    }
    return $payload["data"];
}

$myToken = "YOUR_TOKEN";
$token = $myToken === "YOUR_TOKEN" ? call("/guest", ["slug" => SLUG])["token"] : $myToken;
using System.Net.Http.Json;
using System.Text.Json;

static class Flux {
    const string Base = "https://api.skillsafe.ai/v1/app-api";
    const string Slug = "flux-desk";
    static readonly HttpClient Http = new();

    public static async Task<JsonElement> CallAsync(
        string path, object? body = null, string? token = null, string? idem = null) {
        var req = new HttpRequestMessage(
            body is null ? HttpMethod.Get : HttpMethod.Post, Base + path);
        if (body is not null) req.Content = JsonContent.Create(body);
        if (token is not null) req.Headers.Add("Authorization", "Bearer " + token);
        if (idem is not null) req.Headers.Add("Idempotency-Key", idem);
        var env = await (await Http.SendAsync(req)).Content.ReadFromJsonAsync<JsonElement>();
        if (env.TryGetProperty("error", out var err)) {
            throw new Exception(err.GetProperty("code").GetString()
                + ": " + err.GetProperty("message").GetString());
        }
        return env.GetProperty("data");
    }
}

Step 2 · Check the session and the balance

/me tells you whether the token is personal or guest and what the balance is. Compare it against min_credits from the estimate before you submit: a 402 after submitting is a failure of the caller, not of the wallet.

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# => {"data":{"subject_type":"user","credits":48210,"app_slug":"flux-desk"}}
# credits are 1/10 000 of a US dollar, so 48210 is $4.8210.
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"], "credits =",
      "${:.4f}".format(me["credits"] / 10000))
const me = await call("/me", null, { token: TOKEN });
console.log(me.subject_type, me.credits, "credits = $" + (me.credits / 10000).toFixed(4));
data, err := call("/me", nil, token, "")
if err != nil {
	panic(err)
}
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int64  `json:"credits"`
}
json.Unmarshal(data, &me)
fmt.Println(me.SubjectType, me.Credits)
JsonNode me = Flux.call("/me", null, token, null);
System.out.println(me.get("subject_type").asText() + " " + me.get("credits").asLong());
me = call("/me", token: TOKEN)
puts "#{me['subject_type']} #{me['credits']} credits"
$me = call("/me", null, $token);
printf("%s %d credits\n", $me["subject_type"], $me["credits"]);
var me = await Flux.CallAsync("/me", token: token);
Console.WriteLine($"{me.GetProperty("subject_type").GetString()} " +
                  $"{me.GetProperty("credits").GetInt64()} credits");

Step 3 · Estimate, and assert the model binding

/estimate costs nothing, creates no job, and is the authoritative proof that the app is wired to the model you think it is. hold_credits is a reservation priced at the full output cap — present it to a user as reserved, never as the price, because charged_credits after settlement is usually far lower. If the balance sits between min_credits and hold_credits the run still executes with a reduced output cap and comes back "truncated": true; treat the drivers that arrived as complete and report the rest as missing rather than presenting a short list as the whole commentary.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d @input.json
# => {"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
#             "hold_credits":2910,"min_credits":180,"sponsor_enabled":false}}
#
# hold_credits is a RESERVATION priced at the full output cap, not the price.
# charged_credits after the run is usually far lower. Assert the binding here:
# model must read gpt-5.6-terra, model_alias gpt-terra, markup_bps 1000.
est = call("/estimate", payload, token=TOKEN)
assert est["model"] == "gpt-5.6-terra", est["model"]
assert est["model_alias"] == "gpt-terra"
assert est["markup_bps"] == 1000
if me["credits"] < est["min_credits"]:
    raise SystemExit("balance below the model minimum - top up before running")
const est = await call("/estimate", payload, { token: TOKEN });
if (est.model !== "gpt-5.6-terra" || est.model_alias !== "gpt-terra" || est.markup_bps !== 1000) {
  throw new Error("unexpected model binding: " + JSON.stringify(est));
}
if (me.credits < est.min_credits) throw new Error("balance below the model minimum");
data, err = call("/estimate", payload, token, "")
if err != nil {
	panic(err)
}
var est struct {
	Model      string `json:"model"`
	ModelAlias string `json:"model_alias"`
	MarkupBps  int    `json:"markup_bps"`
	Hold       int64  `json:"hold_credits"`
	Min        int64  `json:"min_credits"`
}
json.Unmarshal(data, &est)
if est.Model != "gpt-5.6-terra" || est.MarkupBps != 1000 {
	panic("unexpected model binding")
}
JsonNode est = Flux.call("/estimate", payload, token, null);
if (!"gpt-5.6-terra".equals(est.get("model").asText())
    || est.get("markup_bps").asInt() != 1000) {
  throw new IllegalStateException("unexpected model binding: " + est);
}
est = call("/estimate", payload, token: TOKEN)
raise "unexpected model binding" unless est["model"] == "gpt-5.6-terra" &&
  est["model_alias"] == "gpt-terra" && est["markup_bps"] == 1000
abort "balance below the model minimum" if me["credits"] < est["min_credits"]
$est = call("/estimate", $payload, $token);
if ($est["model"] !== "gpt-5.6-terra" || $est["markup_bps"] !== 1000) {
    throw new RuntimeException("unexpected model binding");
}
if ($me["credits"] < $est["min_credits"]) {
    throw new RuntimeException("balance below the model minimum");
}
var est = await Flux.CallAsync("/estimate", payload, token);
if (est.GetProperty("model").GetString() != "gpt-5.6-terra"
    || est.GetProperty("markup_bps").GetInt32() != 1000) {
    throw new InvalidOperationException("unexpected model binding");
}

The input — what facts carries

Build this with FluxKit.factsForModel(FluxKit.analyze(ctx)) or assemble it yourself; the field names below are the contract, taken from fluxkit.js rather than from intent. Three of them bind the reply: flagged_ids is the exact set that must get a driver, unsourced_ids is the subset that may not be explained, and always_comment_ids is the firm's standing instruction. The excerpts are cut on whole rows with the header kept, and the cut is announced in-band — every figure in facts was measured over the complete pack either way.

Two fields sit outside facts and are easy to miss. current_datetime is an ISO-8601 stamp of when the request was made — it is the clock, not evidence: the prompt forbids dating the commentary from it or treating it as a transaction date, and the period being commented on always comes from facts and the period labels. retry_note is sent only on the reformat retry, carries the reason the previous reply was rejected, and must go out under the same Idempotency-Key base as the first attempt.

{
  "facts": {
    "period": "May 2026",
    "prior_label": "April 2026",
    "budget_label": "May budget",
    "entity": "Northwind Cloud, consolidated",
    "currency": "$",
    "materiality": "the greater of 5% of the comparative and $50,000",

    "totals": {
      "lines_read": 14,
      "flagged": 11,
      "below_threshold": 3,
      "gross_movement_vs_prior": 1120000,
      "net_movement_vs_prior": -380000,
      "net_movement_vs_budget": -304000,
      "balance_sheet_movement_vs_prior": -1450000,
      "subtotals_not_footing": 0
    },

    "flagged_ids":        ["L1","L2","L3","L4","L6","L7","L9","L10","L12","L13","L14"],
    "unsourced_ids":      [],
    "always_comment_ids": ["L1","L2","L3","L6","L7","L9","L14"],
    "breaker_ids":        [],

    "flagged_lines": [
      {
        "id": "L4",
        "line": "Hosting and cloud",
        "statement": "P&L",
        "polarity": "expense",
        "subtotal": false,
        "current": 1880000, "prior": 1690000, "budget": 1720000,
        "delta_vs_prior": 190000,  "pct_vs_prior": "11.2%",
        "delta_vs_budget": 160000, "pct_vs_budget": "9.3%",
        "favourability_vs_prior": "unfavourable",
        "favourability_vs_budget": "unfavourable",
        "why_flagged": "vs prior + vs budget",
        "share_of_gross_movement": "17%",
        "activity_notes": [
          "Hosting and cloud: 60 additional GPU reservations were taken on one-year committed-use terms for the May model-serving launch."
        ],
        "sourced": true
      }
    ],

    "integrity":    [],
    "load_bearing": [],
    "data_notes":   []
  },

  "table_excerpt": "Line,Current,Prior,Budget\nSubscription revenue,8420000,...",
  "notes_excerpt": "Subscription revenue: 41 net new logos closed in the month, ...",
  "current_datetime": "2026-05-31T18:00:00.000Z"
}

Step 4 · Run and poll

Submit with an Idempotency-Key derived from a hash of the input plus an attempt counter, and reuse the same key on any retry: a network blip or a malformed first reply must never bill twice. The reply arrives as a JSON string in output.output.

# The Idempotency-Key is a hash of the input plus an attempt counter. Retrying
# with the SAME key returns the original job instead of billing a second run -
# which is exactly what the app's reformat-retry lane relies on.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: flux-desk:9bdddab9:696:a1' \
  -d @input.json
# => {"data":{"job_id":"job_...","status":"queued"}}

curl -s https://api.skillsafe.ai/v1/app-api/jobs/job_xxx \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# => {"data":{"status":"succeeded","output":{"output":"{\"title\":...}"},
#             "charged_credits":624,"truncated":false}}
import time

job = call("/run", payload, token=TOKEN)
while True:
    j = call("/jobs/" + job["job_id"], token=TOKEN)
    if j["status"] in ("succeeded", "failed", "canceled"):
        break
    time.sleep(1.5)

if j["status"] != "succeeded":
    raise RuntimeError(j.get("error") or j["status"])
commentary = json.loads(j["output"]["output"])
if j.get("truncated"):
    print("reply cut short by the balance - the drivers that arrived are complete, "
          "the rest are missing and should be reported as missing")
const job = await call("/run", payload, { token: TOKEN, idempotencyKey: key });
let j;
do {
  await new Promise((r) => setTimeout(r, 1500));
  j = await call("/jobs/" + job.job_id, null, { token: TOKEN });
} while (!["succeeded", "failed", "canceled"].includes(j.status));

if (j.status !== "succeeded") throw new Error(j.error || j.status);
const commentary = JSON.parse(j.output.output);
if (j.truncated) console.warn("reply cut short by the balance");
data, err = call("/run", payload, token, idemKey)
if err != nil {
	panic(err)
}
var job struct {
	JobID string `json:"job_id"`
}
json.Unmarshal(data, &job)

for {
	time.Sleep(1500 * time.Millisecond)
	data, err = call("/jobs/"+job.JobID, nil, token, "")
	if err != nil {
		panic(err)
	}
	var j struct {
		Status string `json:"status"`
		Output struct {
			Output string `json:"output"`
		} `json:"output"`
	}
	json.Unmarshal(data, &j)
	if j.Status == "succeeded" {
		fmt.Println(j.Output.Output)
		break
	}
	if j.Status == "failed" || j.Status == "canceled" {
		panic(j.Status)
	}
}
JsonNode job = Flux.call("/run", payload, token, idemKey);
JsonNode j;
do {
  Thread.sleep(1500);
  j = Flux.call("/jobs/" + job.get("job_id").asText(), null, token, null);
} while (!java.util.List.of("succeeded", "failed", "canceled")
    .contains(j.get("status").asText()));

if (!"succeeded".equals(j.get("status").asText())) throw new RuntimeException(j.toString());
JsonNode commentary = M.readTree(j.at("/output/output").asText());
job = call("/run", payload, token: TOKEN, idem: idem_key)
loop do
  sleep 1.5
  j = call("/jobs/#{job['job_id']}", token: TOKEN)
  next unless %w[succeeded failed canceled].include?(j["status"])
  raise j["status"] unless j["status"] == "succeeded"
  commentary = JSON.parse(j["output"]["output"])
  warn "reply cut short by the balance" if j["truncated"]
  break
end
$job = call("/run", $payload, $token, $idemKey);
do {
    sleep(2);
    $j = call("/jobs/" . $job["job_id"], null, $token);
} while (!in_array($j["status"], ["succeeded", "failed", "canceled"], true));

if ($j["status"] !== "succeeded") { throw new RuntimeException($j["status"]); }
$commentary = json_decode($j["output"]["output"], true);
var job = await Flux.CallAsync("/run", payload, token, idemKey);
JsonElement j;
do {
    await Task.Delay(1500);
    j = await Flux.CallAsync("/jobs/" + job.GetProperty("job_id").GetString(), token: token);
} while (j.GetProperty("status").GetString() is not ("succeeded" or "failed" or "canceled"));

if (j.GetProperty("status").GetString() != "succeeded") throw new Exception(j.ToString());
var commentary = JsonDocument.Parse(
    j.GetProperty("output").GetProperty("output").GetString()!);

The output contract

One JSON object, no prose and no code fence. drivers is an exact partition of facts.flagged_ids. source is either "activity-notes" or "unclear", and every id in facts.unsourced_ids must take the second with the driver text "driver unclear - flag for controller" verbatim. A driver sourced to the activity notes for a line the notes never mention is reported by FluxKit.reconcile as an invention, by name.

{
  "title": "Flux commentary - May 2026",

  "period_summary": "Three to five sentences. Names the largest measured movement, states the
                     period's direction, says if the movement hangs on one line, and states any
                     integrity break plainly and first.",

  "drivers": [
    {
      "line_id": "L4",
      "driver":  "60 additional GPU reservations were taken on one-year committed-use terms for the May model-serving launch.",
      "source":  "activity-notes"
    },
    {
      "line_id": "L7",
      "driver":  "driver unclear - flag for controller",
      "source":  "unclear"
    }
  ],

  "watch_items": ["A line just under the threshold worth watching next period."],
  "questions":   ["A question for the controller."],
  "unverified":  ["Something this pass could not confirm from what it was given."]
}

Step 5 · Stream it instead

/run-stream is the same billed run with server-sent events. The frame name arrives on the event: line — job, delta, done — so parse that rather than assuming every frame carries text. A stream that dies mid-reply usually still carries the earlier fields; close the JSON and keep what arrived, then report every flagged line with no driver as missing rather than assuming it was fine.

# Server-sent events. The frame name arrives on the "event:" line, so parse that
# rather than assuming every frame is a delta.
curl -N -s -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -H 'Idempotency-Key: flux-desk:9bdddab9:696:a1' \
  -d @input.json
# event: job
# data: {"job_id":"job_..."}
# event: delta
# data: {"text":"{\"title\":\"Flux commentary"}
# event: done
# data: {"status":"succeeded","charged_credits":624,"truncated":false}
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(payload).encode())
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "text/event-stream")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", idem_key)

raw, event = "", None
with urllib.request.urlopen(req) as stream:
    for line in stream:
        line = line.decode().rstrip("\n")
        if line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:"):
            frame = json.loads(line[5:].strip())
            if event == "delta":
                raw += frame["text"]
            elif event == "done":
                print("charged", frame.get("charged_credits"))
commentary = json.loads(raw)
const res = await fetch(BASE + "/run-stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Accept: "text/event-stream",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": idemKey
  },
  body: JSON.stringify(payload)
});

const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null;
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += dec.decode(value, { stream: true });
  let nl;
  while ((nl = buf.indexOf("\n")) !== -1) {
    const line = buf.slice(0, nl);
    buf = buf.slice(nl + 1);
    if (line.startsWith("event:")) event = line.slice(6).trim();
    else if (line.startsWith("data:")) {
      const frame = JSON.parse(line.slice(5).trim());
      if (event === "delta") raw += frame.text;
    }
  }
}
const commentary = JSON.parse(raw);
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", idemKey)

res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
var raw strings.Builder
event := ""
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event:"):
		event = strings.TrimSpace(line[6:])
	case strings.HasPrefix(line, "data:") && event == "delta":
		var f struct {
			Text string `json:"text"`
		}
		json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &f)
		raw.WriteString(f.Text)
	}
}
HttpRequest req = HttpRequest.newBuilder(URI.create(Flux.BASE + "/run-stream"))
    .header("Content-Type", "application/json")
    .header("Accept", "text/event-stream")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", idemKey)
    .POST(HttpRequest.BodyPublishers.ofString(M.writeValueAsString(payload)))
    .build();

StringBuilder raw = new StringBuilder();
String[] event = { "" };
Flux.HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
  if (line.startsWith("event:")) {
    event[0] = line.substring(6).trim();
  } else if (line.startsWith("data:") && event[0].equals("delta")) {
    try {
      raw.append(M.readTree(line.substring(5).trim()).get("text").asText());
    } catch (Exception e) { throw new RuntimeException(e); }
  }
});
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = idem_key
req.body = JSON.dump(payload)

raw = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.chomp
        if line.start_with?("event:")
          event = line[6..].strip
        elsif line.start_with?("data:") && event == "delta"
          raw << JSON.parse(line[5..].strip)["text"]
        end
      end
    end
  end
end
commentary = JSON.parse(raw)
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Accept: text/event-stream",
        "Authorization: Bearer " . $token,
        "Idempotency-Key: " . $idemKey,
    ],
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
        foreach (explode("\n", $chunk) as $line) {
            $line = rtrim($line);
            if (str_starts_with($line, "event:")) {
                $event = trim(substr($line, 6));
            } elseif (str_starts_with($line, "data:") && $event === "delta") {
                $raw .= json_decode(trim(substr($line, 5)), true)["text"];
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$commentary = json_decode($raw, true);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream") {
    Content = JsonContent.Create(payload)
};
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Authorization", "Bearer " + token);
req.Headers.Add("Idempotency-Key", idemKey);

using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null, line;
while ((line = await reader.ReadLineAsync()) is not null) {
    if (line.StartsWith("event:")) evt = line[6..].Trim();
    else if (line.StartsWith("data:") && evt == "delta") {
        raw.Append(JsonDocument.Parse(line[5..].Trim())
            .RootElement.GetProperty("text").GetString());
    }
}
var commentary = JsonDocument.Parse(raw.ToString());

Step 6 · Store and search past commentaries

The commentaries collection is the app's system of record, with acl_read: "owner" and acl_write: "user". Records are created at /collections/{name}/records — note the trailing segment. where takes operator objects only, and order_by is silently ignored in favour of a sort object (the default is created_at desc). Documents cap at 64 KB, which is why the app trims the pasted pack before the rendered document. The declared embed fields are title, period, entity and summary; they were chosen before the app had users because the platform never backfills vectors.

# Create. Note the path: records are created at /collections/{name}/records.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/commentaries/records \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Flux commentary - May 2026","period":"May 2026",
       "entity":"Northwind Cloud, consolidated","summary":"Cloud infrastructure...",
       "state":"fully-sourced","flagged":11,"unsourced":0,"foot_breaks":0,
       "ran_at":"2026-05-31T18:00:00Z","doc_md":"# Flux commentary ..."}'

# Query. `where` takes OPERATOR OBJECTS, and order_by is ignored in favour of
# `sort`; the default is created_at desc.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/commentaries/query \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"where":{"state":{"eq":"partly-sourced"}},
       "sort":{"field":"ran_at","dir":"desc"},"limit":12}'

# Semantic search over the declared embed fields: title, period, entity, summary.
# 30 req/min per IP and about ten times the cost of a where filter, so use it
# only when an exact match will not do.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/commentaries/similar \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"text":"the month the cloud bill doubled","limit":8}'
call("/collections/commentaries/records", {
    "title": commentary["title"],
    "period": facts["period"],
    "entity": facts["entity"],
    "summary": commentary["period_summary"][:500],
    "state": state,                      # fully-sourced / partly-sourced / unsourced
    "flagged": facts["totals"]["flagged"],
    "unsourced": len(facts["unsourced_ids"]),
    "foot_breaks": facts["totals"]["subtotals_not_footing"],
    "ran_at": datetime.now(timezone.utc).isoformat(),
    "doc_md": document_markdown[:32000],
}, token=TOKEN)

recent = call("/collections/commentaries/query", {
    "where": {"state": {"eq": "partly-sourced"}},
    "sort": {"field": "ran_at", "dir": "desc"},
    "limit": 12,
}, token=TOKEN)["records"]
await call("/collections/commentaries/records", {
  title: commentary.title,
  period: facts.period,
  entity: facts.entity,
  summary: commentary.period_summary.slice(0, 500),
  state,
  flagged: facts.totals.flagged,
  unsourced: facts.unsourced_ids.length,
  foot_breaks: facts.totals.subtotals_not_footing,
  ran_at: new Date().toISOString(),
  doc_md: documentMarkdown.slice(0, 32000)
}, { token: TOKEN });

// similar() resolves to the record ARRAY; query() resolves to { records }.
// Accept either shape rather than trusting one - reading .records off an array
// is how a semantic search silently returns nothing forever.
const recordsOf = (r) => (Array.isArray(r) ? r : (r && r.records) || []);
const hits = recordsOf(await call("/collections/commentaries/similar",
  { text: "the month the cloud bill doubled", limit: 8 }, { token: TOKEN }));
record := map[string]any{
	"title":       commentary.Title,
	"period":      facts.Period,
	"entity":      facts.Entity,
	"summary":     commentary.PeriodSummary,
	"state":       state,
	"flagged":     facts.Totals.Flagged,
	"unsourced":   len(facts.UnsourcedIDs),
	"foot_breaks": facts.Totals.SubtotalsNotFooting,
	"ran_at":      time.Now().UTC().Format(time.RFC3339),
	"doc_md":      documentMarkdown,
}
if _, err := call("/collections/commentaries/records", record, token, ""); err != nil {
	panic(err)
}

query := map[string]any{
	"where": map[string]any{"state": map[string]any{"eq": "partly-sourced"}},
	"sort":  map[string]any{"field": "ran_at", "dir": "desc"},
	"limit": 12,
}
data, err = call("/collections/commentaries/query", query, token, "")
Map<String, Object> record = Map.of(
    "title", commentary.get("title").asText(),
    "period", facts.get("period").asText(),
    "entity", facts.get("entity").asText(),
    "summary", commentary.get("period_summary").asText(),
    "state", state,
    "flagged", facts.at("/totals/flagged").asInt(),
    "unsourced", facts.get("unsourced_ids").size(),
    "foot_breaks", facts.at("/totals/subtotals_not_footing").asInt(),
    "ran_at", java.time.Instant.now().toString(),
    "doc_md", documentMarkdown);
Flux.call("/collections/commentaries/records", record, token, null);

JsonNode recent = Flux.call("/collections/commentaries/query", Map.of(
    "where", Map.of("state", Map.of("eq", "partly-sourced")),
    "sort", Map.of("field", "ran_at", "dir", "desc"),
    "limit", 12), token, null);
call("/collections/commentaries/records", {
  "title" => commentary["title"],
  "period" => facts["period"],
  "entity" => facts["entity"],
  "summary" => commentary["period_summary"][0, 500],
  "state" => state,
  "flagged" => facts["totals"]["flagged"],
  "unsourced" => facts["unsourced_ids"].length,
  "foot_breaks" => facts["totals"]["subtotals_not_footing"],
  "ran_at" => Time.now.utc.iso8601,
  "doc_md" => document_markdown[0, 32000]
}, token: TOKEN)

recent = call("/collections/commentaries/query", {
  "where" => { "state" => { "eq" => "partly-sourced" } },
  "sort" => { "field" => "ran_at", "dir" => "desc" },
  "limit" => 12
}, token: TOKEN)["records"]
call("/collections/commentaries/records", [
    "title"       => $commentary["title"],
    "period"      => $facts["period"],
    "entity"      => $facts["entity"],
    "summary"     => substr($commentary["period_summary"], 0, 500),
    "state"       => $state,
    "flagged"     => $facts["totals"]["flagged"],
    "unsourced"   => count($facts["unsourced_ids"]),
    "foot_breaks" => $facts["totals"]["subtotals_not_footing"],
    "ran_at"      => gmdate("c"),
    "doc_md"      => substr($documentMarkdown, 0, 32000),
], $token);

$recent = call("/collections/commentaries/query", [
    "where" => ["state" => ["eq" => "partly-sourced"]],
    "sort"  => ["field" => "ran_at", "dir" => "desc"],
    "limit" => 12,
], $token)["records"];
await Flux.CallAsync("/collections/commentaries/records", new {
    title       = title,
    period      = period,
    entity      = entity,
    summary     = periodSummary,
    state       = state,
    flagged     = flaggedCount,
    unsourced   = unsourcedCount,
    foot_breaks = footBreaks,
    ran_at      = DateTime.UtcNow.ToString("o"),
    doc_md      = documentMarkdown
}, token);

var recent = await Flux.CallAsync("/collections/commentaries/query", new {
    where = new { state = new { eq = "partly-sourced" } },
    sort  = new { field = "ran_at", dir = "desc" },
    limit = 12
}, token);

Check the reply yourself

This is the part worth copying. The app does not ask you to trust the model, and neither should your pipeline: the same engine that produced facts re-measures the reply in public. Vendor /fluxkit.js — plain ES5, no dependencies, no network — and run the identical reconciliation on your own machine.

// fluxkit.js is plain ES5 with no dependencies and no network calls, so the same
// reconciliation the app runs in the tab runs in your pipeline.
const an = FluxKit.analyze({
  period_label: "May 2026", prior_label: "April 2026", budget_label: "May budget",
  currency: "$", pct_threshold: 5, abs_floor: 50000,
  always: "revenue, headcount cost, cash",
  table: packCsv, notes: activityNotes
});

const facts = FluxKit.factsForModel(an);            // exactly what you send
const model = FluxKit.normalizeModel(commentary);   // exactly what came back
const findings = FluxKit.reconcile(model, an);
const summary = FluxKit.summarize(findings);        // { pass, warn, fail, info, fails }

if (summary.fails) {
  // Each failure is separate and named: missing ids, duplicated ids, ids the
  // engine measured as below threshold, ids that are not lines at all, a driver
  // sourced to notes that never mention the line, an always-comment line with no
  // driver, and any figure that matches nothing measured.
  findings.filter((f) => f.level === "fail").forEach((f) => console.error(f.id, f.text));
  process.exitCode = 1;
}

// The rendered document, and the guard that reads it back before it leaves.
const md = FluxKit.renderDoc(an, model);
const guard = FluxKit.exportGuard(md, an);
if (!guard.ok) throw new Error("the rendered commentary no longer matches the measurement");

What the checker actually asserts: exactly one driver per flagged line, with missing, duplicated, off-contract and not-even-a-line ids reported separately rather than as one count mismatch; every id the engine measured as unsourced returned as unclear rather than explained; no driver claiming the activity notes for a line they never mention; no always-comment line left without a driver; any driver that is only the caption, a direction and a figure flagged as a restatement; any narrative that calls a balance-sheet line the largest movement of the period, since a balance is a stock and a variance is a flow; and every currency and percentage figure traced back to something measured, at the precision it was written to, so a permitted rounding of $1,153,000 to $1.2M passes while $1,153,204 does not.