SOX Desk — API

Size the sample, seed the selection, get the review — from your own tools.

API tokens Open the app

Drive a SOX control test from your own pipeline

SOX Desk splits control testing in two. The deterministic half — parsing the population, deriving the sample size line by line, seeding the selection, matching risk attributes, measuring coverage, evaluating deviations — runs in the browser and is free. This API is the other half: the metered review that writes the test procedures for your control, names the evidence, judges every risk attribute the arithmetic flagged, and argues likelihood and magnitude behind the classification. You compute the prescan yourself (or lift the free lane's own module, see the last section) and send it in.

This API assists with a SOX testing workflow. It is not audit or legal advice. Everything it returns must be reviewed by a qualified financial professional before it is relied upon or included in audit documentation.

Basics

Base URL https://api.skillsafe.ai/v1/app-api. Every response is the same envelope: {"ok":true,"data":{...}} on success, {"ok":false,"error":{"code":"...","message":"...","details":{...}}} on failure. Check ok before touching data.

POST /guest GET /me POST /estimate POST /run GET /jobs/{id} POST /run-stream POST /collections/workpapers/query POST /collections/workpapers/records

Money. Credits are ten-thousandths of a dollar. The app runs on the gpt-terra alias at markup_bps: 1000 and price_credits: 0 — you pay the model's metered cost plus the publisher's 10%, and nothing to open the page. /estimate is free and creates no job.

Error codes

HTTPcodeWhat to do
400VALIDATION_ERRORThe body is not the shape the app expects. On /guest this is almost always a missing slug in the body - an X-App-Slug header is not accepted.
401UNAUTHORIZEDNo bearer token, or an expired guest token. Mint a new one.
402PAYMENT_REQUIREDThe balance is below min_credits. Call /estimate first and check it against /me: a 402 after submit is a client bug.
404NOT_FOUNDWrong slug, wrong job id, or a collection this app did not declare.
409CONFLICTAn Idempotency-Key replay whose body differs from the first use. Change the key when the input changes.
422CONTRACT_ERRORThe model replied with something that is not the single JSON object the contract requires. Retry once reusing the SAME idempotency key so the retry cannot double-bill.
429RATE_LIMITEDBack off and retry; do not tight-loop.
500INTERNALTransient. Retry with the same idempotency key.

Step 1 · Get a token

Two ways in. A personal token is the one this browser already holds — open /tokens.html and copy it, or the shell export line, so you never have to open a DevTools console. A guest token is minted by POST /guest with the slug in the body; guests can browse and estimate but cannot run unless the app sponsors them, and each new guest token is a new identity with an empty workpapers collection.

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

# Option B - mint a guest token. The slug goes in the BODY. An X-App-Slug
# header is not accepted and answers 400 "slug is required".
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H "Content-Type: application/json" \
  -d '{"slug":"sox-desk"}'
# -> {"ok":true,"data":{"token":"aut_...","subject_type":"guest","credits":0}}

# Every later call sends it as a bearer token:
#   -H "Authorization: Bearer $SKILLSAFE_TOKEN"
import json, os, urllib.request

BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "sox-desk"
TOKEN = os.environ.get("SKILLSAFE_TOKEN")   # from /tokens.html, or minted below


def call(path, body=None, method=None, token=None):
    """Every endpoint in this API is JSON in, {data}/{error} out."""
    data = json.dumps(body).encode() if body is not None else None
    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 not payload.get("ok"):
        raise RuntimeError(payload.get("error", {}).get("code", "unknown"))
    return payload["data"]


if not TOKEN:
    TOKEN = call("/guest", {"slug": SLUG})["token"]   # slug in the body, not a header
print(TOKEN[:12] + "...")
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "sox-desk";
let TOKEN = "YOUR_TOKEN";   // from /tokens.html, or minted below

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

if (TOKEN === "YOUR_TOKEN") {
  TOKEN = "";                                  // no bearer on the guest call
  TOKEN = (await call("/guest", { slug: SLUG })).token;
}
package main

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

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

var token = "" // from /tokens.html, or minted by guest() below

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

func call(path string, body any, method string) (json.RawMessage, error) {
	var rdr io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		rdr = bytes.NewReader(b)
		if method == "" {
			method = "POST"
		}
	}
	if method == "" {
		method = "GET"
	}
	req, _ := http.NewRequest(method, base+path, rdr)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		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 !env.OK {
		return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
	}
	return env.Data, nil
}

func guest() error {
	raw, err := call("/guest", map[string]string{"slug": slug}, "")
	if err != nil {
		return err
	}
	var out struct{ Token string }
	if err := json.Unmarshal(raw, &out); err != nil {
		return err
	}
	token = out.Token
	return nil
}
import java.net.URI;
import java.net.http.*;
import java.util.*;

public class SoxDesk {
  static final String BASE = "https://api.skillsafe.ai/v1/app-api";
  static final String SLUG = "sox-desk";
  static String token = System.getenv("SKILLSAFE_TOKEN");   // or minted by guest()
  static final HttpClient HTTP = HttpClient.newHttpClient();

  static String call(String path, String jsonBody, String method) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
        .header("Content-Type", "application/json");
    if (token != null && !token.isEmpty()) b.header("Authorization", "Bearer " + token);
    if (jsonBody != null) b.method(method == null ? "POST" : method,
        HttpRequest.BodyPublishers.ofString(jsonBody));
    else b.method(method == null ? "GET" : method, HttpRequest.BodyPublishers.noBody());
    HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() >= 400) throw new RuntimeException(res.body());
    return res.body();   // {"ok":true,"data":{...}} - parse with your JSON library
  }

  static void guest() throws Exception {
    token = "";
    String body = call("/guest", "{\"slug\":\"" + SLUG + "\"}", null);
    token = body.split("\"token\":\"")[1].split("\"")[0];
  }
}
require "json"
require "net/http"

BASE = URI("https://api.skillsafe.ai/v1/app-api")
SLUG = "sox-desk"
TOKEN = ENV["SKILLSAFE_TOKEN"]   # from /tokens.html, or minted below

def call(path, body = nil, method: nil, token: TOKEN)
  uri = URI(BASE.to_s + path)
  klass = method == "DELETE" ? Net::HTTP::Delete : (body ? Net::HTTP::Post : Net::HTTP::Get)
  req = klass.new(uri, "Content-Type" => "application/json")
  req["Authorization"] = "Bearer #{token}" if token && !token.empty?
  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']}" unless payload["ok"]
  payload["data"]
end

TOKEN2 = TOKEN || call("/guest", { "slug" => SLUG }, token: nil)["token"]
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "sox-desk";
$token = getenv("SKILLSAFE_TOKEN") ?: "";   // from /tokens.html, or minted below

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

if ($token === "") {
    $token = call("/guest", ["slug" => SLUG])["token"];   // slug in the body
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

class SoxDesk {
  const string Base = "https://api.skillsafe.ai/v1/app-api";
  const string Slug = "sox-desk";
  static string Token = "YOUR_TOKEN";   // from /tokens.html, or minted by GuestAsync()
  static readonly HttpClient Http = new();

  static async Task<JsonElement> CallAsync(string path, object? body = null, HttpMethod? method = null) {
    var req = new HttpRequestMessage(method ?? (body is null ? HttpMethod.Get : HttpMethod.Post), Base + path);
    if (body is not null)
      req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    if (Token is { Length: > 0 } and not "YOUR_TOKEN")
      req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
    var res = await Http.SendAsync(req);
    var payload = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
    if (!payload.GetProperty("ok").GetBoolean())
      throw new Exception(payload.GetProperty("error").GetProperty("code").GetString());
    return payload.GetProperty("data");
  }

  static async Task GuestAsync() {
    Token = "";
    Token = (await CallAsync("/guest", new { slug = Slug })).GetProperty("token").GetString()!;
  }
}

Step 2 · Check who you are and what you can spend

GET /me tells you the subject type, the balance, and the app's own model and markup. Compare the balance against /estimate before you submit — a 402 after submit is a client-side failure, not a user error.

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"ok":true,"data":{
#      "subject_type":"user","credits":184203,"app":{"slug":"sox-desk",
#      "model":"gpt-terra","markup_bps":1000,"price_credits":0}}}
#
# credits are in ten-thousandths of a dollar: 184203 = $18.42.
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"] / 10000, "USD")
const me = await call("/me");
console.log(me.subject_type, me.credits / 10000, "USD");
raw, err := call("/me", nil, "")
// raw is {"subject_type":"user","credits":184203,...}
String me = call("/me", null, null);
System.out.println(me);
me = call("/me")
puts "#{me['subject_type']} #{me['credits'] / 10_000.0} USD"
$me = call("/me");
printf("%s %.2f USD\n", $me["subject_type"], $me["credits"] / 10000);
var me = await CallAsync("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits").GetInt32() / 10000.0} USD");

Step 3 · Price the run, and prove the model binding

POST /estimate costs nothing and creates no job. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. Assert on the first three in CI: they are the authoritative proof that the app is wired to the right model at the right markup. hold_credits is what gets reserved — it prices the full output cap and is usually far more than the charged_credits you end up paying.

# /estimate is free: no job is created, no credits are held, nothing is charged.
# It is also the authoritative proof of the model binding, so assert on it in CI.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d @body.json
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#      "markup_bps":1000,"hold_credits":3140,"min_credits":420,
#      "sponsor_enabled":false}}
#
# hold_credits is RESERVED, not charged: it prices the full output cap. What you
# pay is charged_credits on the finished job, and it is usually far lower.
est = call("/estimate", body, token=TOKEN)          # free, no job created
assert est["model_alias"] == "gpt-terra"
assert est["markup_bps"] == 1000
print("reserved up to", est["hold_credits"] / 10000, "USD")
if me["credits"] < est["min_credits"]:
    raise SystemExit("top up first - a 402 after submit is a UI failure, not a user error")
const est = await call("/estimate", body);              // free, no job created
console.assert(est.model_alias === "gpt-terra" && est.markup_bps === 1000);
if (me.credits < est.min_credits) throw new Error("top up first");
raw, err = call("/estimate", body, "")
var est struct {
	Model       string `json:"model"`
	ModelAlias  string `json:"model_alias"`
	MarkupBps   int    `json:"markup_bps"`
	HoldCredits int    `json:"hold_credits"`
	MinCredits  int    `json:"min_credits"`
}
json.Unmarshal(raw, &est)
// est.ModelAlias == "gpt-terra", est.MarkupBps == 1000
String est = call("/estimate", bodyJson, null);
// assert est.contains("\"model_alias\":\"gpt-terra\"");
est = call("/estimate", body)
raise "wrong model" unless est["model_alias"] == "gpt-terra" && est["markup_bps"] == 1000
puts "reserved up to #{est['hold_credits'] / 10_000.0} USD"
$est = call("/estimate", $body);
assert($est["model_alias"] === "gpt-terra" && $est["markup_bps"] === 1000);
var est = await CallAsync("/estimate", body);
if (est.GetProperty("model_alias").GetString() != "gpt-terra") throw new Exception("wrong model");

Step 4 · Run the review and poll for it

POST /run is metered and returns {"job_id":"job_..."}; poll GET /jobs/{id} until status is succeeded or failed. Always send an Idempotency-Key derived from the input — a network blip, a retry or a reformat attempt with the same key returns the same job instead of billing twice. output.output is the single JSON object documented in The output contract below.

# Metered. Always send Idempotency-Key: a retry with the same key returns the
# same job instead of billing twice. Derive it from the input, not from a clock.
KEY="sox-desk:$(python3 -c 'import hashlib,sys;print(hashlib.sha256(open("body.json","rb").read()).hexdigest()[:16])'):a1"

JOB=$(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: $KEY" \
  -d @body.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll until terminal.
while :; do
  OUT=$(curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
    -H "Authorization: Bearer $SKILLSAFE_TOKEN")
  ST=$(echo "$OUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["status"])')
  [ "$ST" = "succeeded" ] || [ "$ST" = "failed" ] && break
  sleep 2
done
echo "$OUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'
# The output field is one JSON object - the review contract documented below.
import hashlib, time

key = "sox-desk:" + hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()[:16] + ":a1"
req = urllib.request.Request(BASE + "/run", data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)          # a retry with this key never double-bills
with urllib.request.urlopen(req) as r:
    job_id = json.load(r)["data"]["job_id"]

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

review = json.loads(job["output"]["output"])     # the review contract, below
print(review["conclusion"], review["risk_rating"])
print("charged", job.get("charged_credits", 0) / 10000, "USD")
const enc = new TextEncoder().encode(JSON.stringify(body));
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
  .map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 16);

const started = await fetch(BASE + "/run", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": "sox-desk:" + digest + ":a1"
  },
  body: JSON.stringify(body)
}).then(r => r.json());

let job;
do {
  await new Promise(r => setTimeout(r, 2000));
  job = await call("/jobs/" + started.data.job_id);
} while (job.status !== "succeeded" && job.status !== "failed");

const review = JSON.parse(job.output.output);
console.log(review.conclusion, review.risk_rating);
// POST /run needs the Idempotency-Key header, so build the request directly.
b, _ := json.Marshal(body)
sum := sha256.Sum256(b)
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", "sox-desk:"+hex.EncodeToString(sum[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
// decode {"data":{"job_id":"..."}} then poll GET /jobs/{id} until status is terminal
String key = "sox-desk:" + Integer.toHexString(bodyJson.hashCode()) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
    .build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
// extract job_id, then poll GET /jobs/{id} every two seconds until terminal
require "digest"

key = "sox-desk:#{Digest::SHA256.hexdigest(JSON.dump(body))[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
                          "Authorization" => "Bearer #{TOKEN2}",
                          "Idempotency-Key" => key)
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]

job = nil
loop do
  job = call("/jobs/#{job_id}", token: TOKEN2)
  break if %w[succeeded failed].include?(job["status"])
  sleep 2
end
review = JSON.parse(job["output"]["output"])
$key = "sox-desk:" . substr(hash("sha256", json_encode($body)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer $token",
        "Idempotency-Key: $key",
    ],
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);

do {
    sleep(2);
    $job = call("/jobs/$jobId");
} while (!in_array($job["status"], ["succeeded", "failed"], true));
$review = json_decode($job["output"]["output"], true);
var json = JsonSerializer.Serialize(body);
var key = "sox-desk:" + Convert.ToHexString(
    System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..16] + ":a1";

var run = new HttpRequestMessage(HttpMethod.Post, Base + "/run") {
  Content = new StringContent(json, Encoding.UTF8, "application/json")
};
run.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
run.Headers.Add("Idempotency-Key", key);
var started = JsonDocument.Parse(await (await Http.SendAsync(run)).Content.ReadAsStringAsync()).RootElement;
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();

JsonElement job;
do {
  await Task.Delay(2000);
  job = await CallAsync("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));

Step 5 · Or stream it

POST /run-stream is the same run over server-sent events. Frame names arrive on the event: line, not as a type field inside the payload — switch on the event name. Concatenate every delta.text in order to rebuild the JSON object. If the final done frame reports truncated: true, the balance capped the output: render whatever parsed and say so, rather than presenting a clipped review as complete.

# Server-sent events. Frame names arrive on the `event:` line, not as a field in
# the payload - switch on the event name, not on data.type.
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 "Idempotency-Key: $KEY" \
  -d @body.json
#
# event: job      data: {"job_id":"job_..."}
# event: delta    data: {"text":"{\"conclusion\":\"signi"}
# event: delta    data: {"text":"ficant-deficiency\",..."}
# event: done     data: {"status":"succeeded","charged_credits":1980,"truncated":false}
#
# Concatenate every delta.text in order: the result is the JSON object. If `done`
# reports truncated:true the reply was capped by the balance - render what parsed
# and tell the user, rather than presenting a clipped review as complete.
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", 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[7:].strip()             # the frame NAME lives here
        elif line.startswith("data: "):
            payload = json.loads(line[6:])
            if event == "delta":
                raw += payload.get("text", "")
            elif event == "done":
                if payload.get("truncated"):
                    print("cut short by the balance - showing what arrived")

review = json.loads(raw)
const res = await fetch(BASE + "/run-stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": "sox-desk:" + digest + ":a1"
  },
  body: JSON.stringify(body)
});

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 });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (line.startsWith("event: ")) event = line.slice(7).trim();
    else if (line.startsWith("data: ")) {
      const p = JSON.parse(line.slice(6));
      if (event === "delta") raw += p.text || "";
      if (event === "done" && p.truncated) console.warn("truncated - keep the partial");
    }
  }
}
const review = JSON.parse(raw);
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", idemKey)
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
var raw, event string
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event: "):
		event = strings.TrimSpace(line[7:])
	case strings.HasPrefix(line, "data: ") && event == "delta":
		var d struct{ Text string }
		json.Unmarshal([]byte(line[6:]), &d)
		raw += d.Text
	}
}
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
    .build();

StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
  if (line.startsWith("event: ")) event[0] = line.substring(7).trim();
  else if (line.startsWith("data: ") && "delta".equals(event[0])) {
    String d = line.substring(6);
    int i = d.indexOf("\"text\":\"");
    if (i >= 0) raw.append(d.substring(i + 8, d.lastIndexOf("\"")));   // use a JSON library
  }
});
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
                          "Authorization" => "Bearer #{TOKEN2}",
                          "Idempotency-Key" => key)
req.body = JSON.dump(body)

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.chomp!
        if line.start_with?("event: ") then event = line[7..].strip
        elsif line.start_with?("data: ") && event == "delta"
          raw << (JSON.parse(line[6..])["text"] || "")
        end
      end
    end
  end
end
review = JSON.parse(raw)
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer $token",
        "Idempotency-Key: $key",
    ],
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
        foreach (explode("\n", $chunk) as $line) {
            if (str_starts_with($line, "event: ")) {
                $event = trim(substr($line, 7));
            } elseif (str_starts_with($line, "data: ") && $event === "delta") {
                $raw .= json_decode(substr($line, 6), true)["text"] ?? "";
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$review = json_decode($raw, true);
var stream = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream") {
  Content = new StringContent(json, Encoding.UTF8, "application/json")
};
stream.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
stream.Headers.Add("Idempotency-Key", key);

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

Step 6 · Read the workpaper history and answer the re-test question

The app stores one record per run in a declared collection called workpapers, which is what makes the only question an audit committee asks on a re-test — did the remediation hold? — a single query. Indexed fields: title, control_id, control_area, period, conclusion, risk_rating, readiness, grade, population_size, sample_size, exception_count, deviation_pct, ran_at. Operators: eq, ne, lt, lte, gt, gte, in, contains — and every where entry must be an operator object, never a bare value. Records are owner-scoped and capped at 64 KB, so the raw population is never stored.

# The workpaper history lives in a declared collection called `workpapers`, so a
# re-test can be diffed against the last conclusion for the same control. Records
# are scoped to the calling subject (acl_read: owner) - reuse ONE token across
# create and query, because every POST /guest mints a new identity with an empty
# collection.

# Newest first:
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/workpapers/query \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"order_by":[{"field":"ran_at","dir":"desc"}],"limit":20}'

# Every earlier test of one control - the re-test question, answered in one call.
# NOTE: every where entry must be an OPERATOR OBJECT. The bare shorthand
# {"control_id":"P2P-04"} is rejected with "where.control_id must be an object
# of operators".
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/workpapers/query \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"where":{"control_id":{"eq":"P2P-04"}},
       "order_by":[{"field":"ran_at","dir":"desc"}],"limit":8}'

# Everything that did not conclude effective, worst first:
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/workpapers/query \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"where":{"conclusion":{"in":["deficiency","significant-deficiency","material-weakness"]}},
       "order_by":[{"field":"deviation_pct","dir":"desc"}],"limit":50}'

# Write one yourself (the app does this after every run). Records live under
# /records; the query endpoint is the only one that hangs off the collection root.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/collections/workpapers/records \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"doc":{"title":"Procure to pay - 2026-Q3","control_id":"P2P-04",
             "control_area":"procure-to-pay","period":"2026-Q3",
             "conclusion":"significant-deficiency","risk_rating":"high",
             "readiness":"gaps-to-close","grade":"A","population_size":98,
             "sample_size":41,"exception_count":5,"deviation_pct":12.2,
             "ran_at":"2026-10-01T09:00:00.000Z"}}'

# Read one back, replace it, or delete it:
#   GET    /collections/workpapers/records/{record_id}
#   PUT    /collections/workpapers/records/{record_id}   body {"doc":{...}}
#   DELETE /collections/workpapers/records/{record_id}
#
# Documents are capped at 64 KB, so never store the raw population - store the
# selection, the attribute summary, the evaluation and the review.
# Declared (indexed) fields: title, control_id, control_area, period, conclusion,
# risk_rating, readiness, grade, population_size, sample_size, exception_count,
# deviation_pct, ran_at. Undeclared keys round-trip fine but are not filterable.
history = call("/collections/workpapers/query", {
    "where": {"control_id": {"eq": "P2P-04"}},        # operator objects only
    "order_by": [{"field": "ran_at", "dir": "desc"}],
    "limit": 8,
}, token=TOKEN)

for rec in history["records"]:
    d = rec["doc"]
    print(d["period"], d["conclusion"], d["deviation_pct"], "%")

# Did the remediation hold?
if len(history["records"]) >= 2:
    now, before = (r["doc"] for r in history["records"][:2])
    print("deviation rate moved", before["deviation_pct"], "->", now["deviation_pct"])
const history = await call("/collections/workpapers/query", {
  where: { conclusion: { in: ["deficiency", "significant-deficiency", "material-weakness"] } },
  order_by: [{ field: "deviation_pct", dir: "desc" }],
  limit: 50
});
for (const r of history.records) console.log(r.doc.control_id, r.doc.conclusion, r.doc.deviation_pct);
raw, err = call("/collections/workpapers/query", map[string]any{
	"where":    map[string]any{"control_id": map[string]any{"eq": "P2P-04"}},
	"order_by": []map[string]string{{"field": "ran_at", "dir": "desc"}},
	"limit":    8,
}, "")
// -> {"records":[{"record_id":"...","doc":{...}}],"next_cursor":null}
String q = "{\"where\":{\"control_id\":{\"eq\":\"P2P-04\"}},"
         + "\"order_by\":[{\"field\":\"ran_at\",\"dir\":\"desc\"}],\"limit\":8}";
String history = call("/collections/workpapers/query", q, null);
history = call("/collections/workpapers/query", {
  "where" => { "control_id" => { "eq" => "P2P-04" } },
  "order_by" => [{ "field" => "ran_at", "dir" => "desc" }],
  "limit" => 8
}, token: TOKEN2)

history["records"].each { |r| puts "#{r['doc']['period']} #{r['doc']['conclusion']}" }
$history = call("/collections/workpapers/query", [
    "where" => ["control_id" => ["eq" => "P2P-04"]],
    "order_by" => [["field" => "ran_at", "dir" => "desc"]],
    "limit" => 8,
]);
foreach ($history["records"] as $r) {
    echo $r["doc"]["period"], " ", $r["doc"]["conclusion"], PHP_EOL;
}
var history = await CallAsync("/collections/workpapers/query", new {
  where = new { control_id = new { eq = "P2P-04" } },
  order_by = new[] { new { field = "ran_at", dir = "desc" } },
  limit = 8
});
foreach (var r in history.GetProperty("records").EnumerateArray())
  Console.WriteLine(r.GetProperty("doc").GetProperty("conclusion"));

The input schema

One object. Everything the model is asked to judge is already computed in prescan; the two free-text fields are the ones only a human has.

FieldTypeNotes
control_descriptionstringThe control in the user's own words. The single highest-value field: the test steps are written for this, not for the area. Say who performs it, how often, on what evidence, at what threshold, and how the population was established as complete.
remediation_contextstringWhat changed since the last test. Only meaningful on a re-test.
current_datetimestringThe caller's local date and time.
prior_runobject or nullThe previous run for the same control_id: period, conclusion, deviation_pct, exception_count, sample_size, tested_at. Present it and the review answers the re-test question directly.
prescan.controlobjectcontrol_id, description, area, type, frequency, key, risk, assertion, period.
prescan.populationobjectcount, value, from, to, span_days, the columns recognised and not, per-field blanks, and the largest items.
prescan.sample_sizeobjectsize, capped, and a ledger of one {label, delta, why} per adjustment. The automated-control override replaces the frequency base; the lines are not cumulative with it.
prescan.selectionobjectmethod, seed, seed_hash, high_value_threshold, the three disjoint counts, total, disjoint, count_coverage_pct, value_coverage_pct.
prescan.samplearrayThe selected items with no, ref, date, amount, basis, stratum and matched attrs.
prescan.risk_attributesarrayid (T1…T12), label, severity, count, value, real evidence references and why. Every id must come back exactly once in risk_verdicts.
prescan.evaluationobjecttested, exception_count, deviation_pct, tolerable_pct, upper_bound_pct and its basis, extrapolated_misstatement, materiality, and the arithmetic-only classification.
prescan.checklistarrayFifteen readiness checks with pass and a detail each.
retry_notestringOnly present when a previous reply was malformed. Reuse the same idempotency key on that retry.

A complete body

Abbreviated in the long arrays, otherwise exactly what the app sends.

{
  "control_description": "Each weekly payment run is reviewed by the AP manager against the approved invoice list and released in the banking portal; the person who prepared the run cannot release it. The population is the system-generated disbursement register for the quarter, agreed to the AP subledger and then to the general ledger.",
  "remediation_context": "",
  "current_datetime": "2026-10-01T09:12:44+01:00 (Thursday)",
  "prior_run": {
    "control_id": "P2P-04",
    "period": "2026-Q2",
    "conclusion": "deficiency",
    "deviation_pct": 7.4,
    "exception_count": 2,
    "sample_size": 27,
    "tested_at": "2026-07-04T10:02:00.000Z"
  },
  "prescan": {
    "control": {
      "control_id": "P2P-04",
      "description": "Each weekly payment run is reviewed by the AP manager ...",
      "area": "Procure to pay (purchasing and accounts payable)",
      "type": "manual",
      "frequency": "weekly",
      "key": "Yes",
      "risk": "high",
      "assertion": "E, O",
      "period": "2026-Q3",
      "conclusion": "significant-deficiency"
    },
    "readiness": "gaps-to-close",
    "grade": "A",
    "format": "CSV",
    "population": {
      "count": 98,
      "value": 2000491.44,
      "from": "2026-07-01",
      "to": "2026-09-29",
      "span_days": 91,
      "columns_recognised": ["date", "ref", "party", "amount", "account", "preparer", "approver", "desc"],
      "columns_unrecognised": [],
      "blanks": [{ "field": "approver", "count": 2, "pct": 2 }],
      "largest": [{ "ref": "PV-269040", "date": "2026-07-30", "amount": 412500, "party": "Lumen Utilities" }]
    },
    "sample_size": {
      "size": 27,
      "capped": false,
      "ledger": [
        { "label": "Base for a weekly control at high risk", "delta": 15, "why": "Attribute sampling: ..." },
        { "label": "Prior-year deficiency", "delta": 8, "why": "Half the base again, because ..." },
        { "label": "External auditor reliance", "delta": 4, "why": "A quarter of the base again, because ..." }
      ]
    },
    "selection": {
      "method": "systematic",
      "seed": "Q3-2026-round-1",
      "seed_hash": "6f2b91ac",
      "high_value_threshold": 250000,
      "stratum_count": 2,
      "targeted_count": 12,
      "statistical_count": 27,
      "total": 41,
      "disjoint": true,
      "double_counted": 0,
      "count_coverage_pct": 41.8,
      "value_coverage_pct": 63,
      "sampled_value": 1260309.4
    },
    "sample": [
      {
        "no": 1, "ref": "PV-260412", "date": "2026-07-09", "amount": 18300,
        "party": "Northwind Logistics", "preparer": "j.okafor", "approver": "s.nakamura",
        "basis": "Risk-targeted (T3)", "stratum": "targeted", "attrs": ["T3", "T4"]
      }
    ],
    "sub_periods": [{ "key": "2026-07", "count": 34, "value": 812004.1, "sampled": 15 }],
    "uncovered_sub_periods": [],
    "risk_attributes": [
      {
        "id": "T1", "label": "Preparer is also the approver", "severity": "critical",
        "count": 3, "pct": 3.1, "value": 104850, "value_pct": 5.2,
        "evidence": "PV-269001 on 2026-07-17 for $31,200; PV-269002 on 2026-08-14 for $44,750",
        "why": "A segregation-of-duties breach in the population itself ..."
      }
    ],
    "exceptions": [{ "no": 1, "ref": "PV-260412", "date": "2026-07-09", "amount": 18300, "attrs": ["T3", "T4"] }],
    "unmatched_exceptions": [],
    "evaluation": {
      "entered": true, "tested": 41, "exception_count": 5, "deviation_pct": 12.2,
      "tolerable_pct": 5, "upper_bound_pct": 22.4,
      "upper_bound_basis": "normal approximation at roughly two standard errors",
      "exception_value": 122390, "extrapolated_misstatement": 292541.95,
      "materiality": 750000, "classification": "significant-deficiency",
      "rationale": "5 deviations in 41 items is a 12.2% rate against a 5% tolerable rate ..."
    },
    "checklist": [{ "id": "C1", "group": "Population", "label": "The population has more than one item", "pass": true, "detail": "98 items parsed." }],
    "area_procedures": ["Agree the population of disbursements ..."],
    "area_evidence": ["The disbursement register for the period ..."],
    "warnings": [],
    "clipped": ""
  }
}

The output contract

The model returns one JSON object and nothing else. These are the rules the app's own render path enforces, so a client that parses the same way will not be surprised:

{
  "conclusion": "significant-deficiency",
  "risk_rating": "high",
  "headline": "Three of 41 payment releases were approved by the person who prepared them, and the prior-year fix did not hold.",
  "summary": "41 of 98 disbursements were tested, covering 63% of the value ...",
  "test_procedures": [
    {
      "step": "Validate the population",
      "procedure": "Re-run the disbursement register for the quarter with the parameters retained and agree its total to the AP subledger and the general ledger.",
      "evidence": "The register with its parameter page, and the two-way reconciliation with sign-off.",
      "why": "A sample from an unvalidated population supports no conclusion."
    }
  ],
  "evidence_requests": ["The banking portal release log for each selected run, showing releaser and timestamp"],
  "risk_verdicts": [
    { "id": "T1", "verdict": "confirmed", "note": "Three runs show preparer and releaser as the same user id; the portal log will confirm." },
    { "id": "T3", "verdict": "false-positive", "note": "PV-260412 appears twice because the portal timed out and the voucher was re-keyed; the reversal pair T10 nets it to nil." }
  ],
  "sample_review": {
    "adequacy": "27 statistical items plus 12 targeted and 2 high-value supports a conclusion on operation ...",
    "coverage_note": "63% of value and every sub-period represented; the 2% of items with no approver ...",
    "extension_needed": false,
    "extension_reason": ""
  },
  "deficiency_evaluation": {
    "classification": "significant-deficiency",
    "likelihood": "More than remote: the same user id both prepared and released on three occasions ...",
    "magnitude": "The extrapolated error of $292,542 is 39% of the $750,000 materiality ...",
    "compensating_controls": "The monthly bank reconciliation would detect a fictitious payee but not an unauthorised amount to a real vendor.",
    "rationale": "..."
  },
  "remediation_plan": [{ "week": 1, "actions": ["Remove release rights from the three preparer accounts."] }],
  "review_notes": ["The two items with no approver recorded were not resolved before the conclusion was drawn."],
  "missing_data": ["The portal release log was not in the population, so releaser identity rests on the register field."],
  "next_steps": ["Obtain the portal release log for the three same-person runs."]
}

The free lane is client-side, and you can have it too

Everything in prescan is computed by one vendored module in the bundle, /soxscan.js, with no network access and no dependencies. It exposes window.SoxScan.analyze(populationText, options), which returns the whole object this API takes as prescan, plus buildWorkpaper(scan, review) for the workpaper text. The companion /report.js turns the same object into the sample CSV, the attribute CSV, the evaluation CSV, Markdown and JSON.

So a pipeline that wants the sample without paying for a review does not need this API at all: load those two files in a browser or a JS runtime, call analyze, and take the CSV. The metered endpoint is only for the judgement half — the procedures, the evidence, the attribute verdicts and the severity argument.

<!-- In a browser: two plain script tags, no bundler, no network. -->
<script src="/soxscan.js"></script>
<script src="/report.js"></script>

// In Node: both files are plain scripts that assign to `window`, so pointing
// `window` at the global object and requiring them is all it takes. No bundler,
// no dynamic evaluation, no network.
const fs = require("fs");
global.window = global;
require("./soxscan.js");
require("./report.js");

const scan = window.SoxScan.analyze(fs.readFileSync("register.csv", "utf8"), {
  area: "procure-to-pay",
  frequency: "weekly",
  controlType: "manual",
  risk: "high",
  keyControl: true,
  priorDeficiency: true,
  auditorReliance: true,
  controlId: "P2P-04",
  period: "2026-Q3",
  controlDesc: "Each weekly payment run is reviewed by the AP manager ...",
  seed: "Q3-2026-round-1",
  threshold: 25000,
  materiality: 750000,
  highValue: 250000,
  exceptions: "",            // or "PV-269001, PV-269002"
  noExceptions: false        // true means "worked, nothing failed"
});

console.log(scan.sample_size.size, scan.selection.total, scan.selection.disjoint);
fs.writeFileSync("sample.csv", window.SoxReport.sampleCsv(scan));
fs.writeFileSync("workpaper.txt", window.SoxScan.buildWorkpaper(scan, null));

The selection is seeded, so the same population, the same options and the same seed produce the same items in your pipeline as in the browser. That reproducibility is the property that makes a random selection acceptable in a workpaper at all.