Customs Desk — API & tutorial Open the app

Drive the compliance read from your own code

Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS with optional streaming. One run takes your shipment paste and returns the full compliance read: the verdict, the lead HS candidate, candidate classifications with GRI reasoning, duty and program findings, documentation and red flags, actions and open questions. Examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.

Not a binding customs ruling, and not a substitute for a licensed customs broker or trade counsel. This API returns AI-generated decision support derived from the text you send and nothing else — no goods and no entry documents are examined, and no licensed broker reviews the output. If you are classifying at volume, treat every response as a candidate for human review, not as a filing position: verify each classification, duty rate and origin claim against the current tariff of the importing country, and obtain a binding ruling where the money or the exposure warrants it. Misclassification carries real financial and legal consequences for the importer of record — not for this app.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api. 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. Runs execute the app's agent and are billed in SkillSafe credits to the calling token, with a worst-case hold up front and the actual cost settled when the job finishes (the /estimate response names the resolved model).

StatusMeaning
401Missing or expired token — create a new session.
402Not enough credits — top up at skillsafe.ai/account/billing.
403The token isn't allowed to do this.
404Unknown job or record id.
5xxTransient platform error — retry with backoff.

Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend. The free in-browser lanes (the code scanner, the line-items grid, the duty and RVC calculators) are client-side JavaScript and have no API surface; the run itself is the only billed call.

Step 0 — A tiny client

Every step below is one or two HTTP calls, so start with a small helper that adds the auth header, sends JSON and unwraps the data envelope. The later steps reuse this helper.

export API="https://api.skillsafe.ai/v1/app-api"
export SKILLSAFE_TOKEN="YOUR_TOKEN"      # see step 1

# every call looks like:
#   curl -s "$API/…" -H "Authorization: Bearer $SKILLSAFE_TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": …} envelope
import json, os, requests

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")  # see step 1

def api(method, path, body=None, **headers):
    res = requests.request(method, API + path, json=body,
                           headers={"Authorization": f"Bearer {TOKEN}", **headers})
    payload = res.json()
    if not res.ok:
        raise RuntimeError(payload.get("error", {}).get("message", res.reason))
    return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 — read it from your environment in real code

async function api(method, path, body, extraHeaders = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
  return json.data;
}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

const API = "https://api.skillsafe.ai/v1/app-api"

var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1

func call(method, path string, body, out any) error {
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req, _ := http.NewRequest(method, API+path, &buf)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var env struct {
		Data  json.RawMessage `json:"data"`
		Error *struct{ Message string `json:"message"` } `json:"error"`
	}
	json.NewDecoder(res.Body).Decode(&env)
	if res.StatusCode >= 400 {
		return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SkillSafe {
    static final String API = "https://api.skillsafe.ai/v1/app-api";
    static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String api(String method, String path, String jsonBody) throws Exception {
        var req = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + TOKEN)
            .header("Content-Type", "application/json")
            .method(method, jsonBody == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();
        var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException(res.body());
        return res.body(); // envelope: {"data": …}
    }
}
require "net/http"
require "json"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1

def api(method, path, body = nil)
  uri = URI(API + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1

function api(string $method, string $path, ?array $body = null): mixed {
    global $TOKEN;
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer $TOKEN",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    $status  = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new Exception($payload["error"]["message"] ?? "HTTP $status");
    }
    return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;

static class SkillSafe
{
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    static readonly HttpClient Http = new();

    static SkillSafe() =>
        Http.DefaultRequestHeaders.Authorization =
            new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1

    public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(method, Api + path);
        if (body != null) req.Content = JsonContent.Create(body);
        var res = await Http.SendAsync(req);
        var json = await res.Content.ReadFromJsonAsync<JsonElement>();
        if (!res.IsSuccessStatusCode)
            throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
        return json.GetProperty("data");
    }
}

Step 1 — Get a token

POST /guest

For scripted use, the simplest reliable path is your personal token: open the token page, sign in, and hit "Copy shell export" — it puts export SKILLSAFE_TOKEN="…" on your clipboard, which every example below reads. Treat the token like a password — it can spend your credits. For fully headless scripts, POST /guest (below) mints a guest token with no browser involved; guests can always call /me and /estimate, but whether a guest can afford an actual run depends on the app's daily sponsorship budget, so don't build on it.

curl -s -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"customs-desk"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "customs-desk"})["token"]
const { token } = await api("POST", "/guest", { slug: "customs-desk" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "customs-desk"}, &guest)
String envelope = api("POST", "/guest", """
    {"slug":"customs-desk"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "customs-desk" })["token"]
$token = api("POST", "/guest", ["slug" => "customs-desk"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
    new { slug = "customs-desk" });
var token = guest.GetProperty("token").GetString();

Step 2 — Check who you are and your balance

GET /me

Returns subject_type ("user" or "guest"), subject_id and your credits balance. Check this before an expensive run — the app itself blocks a run whose worst-case hold exceeds the balance.

curl -s "$API/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int64  `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");

Step 3 — Estimate the cost

POST /estimate

Send the same input you would send to a run; the response's hold_credits is the worst-case cost and min_credits the floor. Nothing is charged and no job is created. The response also reports the resolved model and whether guest sponsorship is active.

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -d '{"data":"5,000 smart watches from Shenzhen to Los Angeles, DDP, $62/unit…","task":"Full compliance review"}' | jq '.data'
est = api("POST", "/estimate", {"data": shipment, "task": "Full compliance review"})
print("worst case:", est["hold_credits"], "credits on", est["model"])
const est = await api("POST", "/estimate", { data: shipment, task: "Full compliance review" });
console.log("worst case:", est.hold_credits, "credits on", est.model);
var est struct {
	HoldCredits int64  `json:"hold_credits"`
	Model       string `json:"model"`
}
err := call("POST", "/estimate", map[string]string{
	"data": shipment, "task": "Full compliance review",
}, &est)
String envelope = api("POST", "/estimate", """
    {"data": %s, "task": "Full compliance review"}
    """.formatted(toJsonString(shipment)));
// worst-case cost is at data.hold_credits
est = api("POST", "/estimate", { data: shipment, task: "Full compliance review" })
puts "worst case: #{est["hold_credits"]} credits on #{est["model"]}"
$est = api("POST", "/estimate", [
    "data" => $shipment,
    "task" => "Full compliance review",
]);
echo "worst case: {$est['hold_credits']} credits on {$est['model']}\n";
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", new {
    data = shipment, task = "Full compliance review" });
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");

Step 4 — Run the compliance read and wait

POST /run
GET /jobs/{job_id}

/run places a credit hold and returns a job_id; poll /jobs/{job_id} every 1–2 seconds until status is succeeded or failed. Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The agent replies with plain text delivered at output.output — the tagged, sectioned shape below.

Input fieldTypeNotes
datastring, requiredThe shipment or product scenario: the good and what it is made of and does, the lane, quantities and values, the Incoterm, the parties, any proposed HS code. Prose is fine. The app truncates at 40,000 chars with a [data truncated] marker.
notesstring, optionalWhat the specialist should know: the decision you face, deadlines, what the supplier or broker suggested, what worries you. Up to 6,000 chars.
taskstring, optionalOne of Full compliance review (default), Classification focus, FTA and duty savings, Docs and entry readiness, Red-flag screen.
factsstring, optionalA mechanical pre-scan of the paste (the app sends its browser scanner's output: HS-like codes with format checks, Incoterms spotted, missing origin/lane/value, the line-item count). A hint for cross-checking, never a verdict — omit it freely.
retry_notestring, optionalFeedback about a previous malformed reply; the agent obeys it exactly.

The reply at output.output follows this contract exactly:

VERDICT: <Ready to proceed | Proceed with caveats | Not assessable>
HS: <best 6-digit candidate like 8517.62, or: Not determinable>
CONFIDENCE: <integer 0-100>
SUMMARY: <2-4 sentences, ends at the first blank line>

## Shipment assessment          (plain "- " bullets)
## Candidate classifications    (bullets "code and label | heading terms | GRI basis | rationale")
## Duty and program findings    (bullets "program or issue | effect | evidence | next step")
## Documentation and red flags  (plain bullets)
## Recommended actions          (plain bullets)
## Open questions               (plain bullets; any section may be "- None.")

The two pipe-delimited sections are four fields separated by " | "; a field the paste cannot support says not computable. If VERDICT is Not assessable, both table sections are - None. and Open questions says what to paste.

# input.json: {"data":"…","notes":"…","task":"Full compliance review"}
JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: customs-$(date +%s)" \
  -d @input.json | jq -r '.data.job_id')

while :; do
  JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $SKILLSAFE_TOKEN")
  STATUS=$(echo "$JOB" | jq -r '.data.status')
  [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
  sleep 2
done

echo "$JOB" | jq -r '.data.output.output' | head -4   # VERDICT / HS / CONFIDENCE / SUMMARY
import time

job_id = api("POST", "/run", {
    "data": shipment,
    "notes": "Broker suggested splitting into $750 parcels - is that legal?",
    "task": "Full compliance review",
}, **{"Idempotency-Key": "customs-001"})["job_id"]

while True:
    job = api("GET", f"/jobs/{job_id}")
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)

if job["status"] == "failed":
    raise RuntimeError(job.get("error", "run failed"))

text = job["output"]["output"]
verdict = next(l for l in text.splitlines() if l.startswith("VERDICT:"))
hs = next(l for l in text.splitlines() if l.startswith("HS:"))
print(verdict, "|", hs)
const { job_id } = await api("POST", "/run", {
  data: shipment,
  task: "Full compliance review",
}, { "Idempotency-Key": crypto.randomUUID() });

let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");

if (job.status === "failed") throw new Error(job.error ?? "run failed");

const text = job.output?.output ?? job.output;
const verdict = text.split("\n").find((l) => l.startsWith("VERDICT:"));
const hs = text.split("\n").find((l) => l.startsWith("HS:"));
console.log(verdict, "|", hs);
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", map[string]string{
	"data": shipment, "task": "Full compliance review",
}, &started)
if err != nil {
	log.Fatal(err)
}

var job struct {
	Status string `json:"status"`
	Error  string `json:"error"`
	Output struct {
		Output string `json:"output"`
	} `json:"output"`
}
for {
	if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
		log.Fatal(err)
	}
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(1500 * time.Millisecond)
}
for _, line := range strings.Split(job.Output.Output, "\n") {
	if strings.HasPrefix(line, "VERDICT:") || strings.HasPrefix(line, "HS:") {
		fmt.Println(line)
	}
}
String envelope = api("POST", "/run", """
    {"data": %s, "task": "Full compliance review"}
    """.formatted(toJsonString(shipment)));
String jobId = /* data.job_id via your JSON library */;

while (true) {
    String job = api("GET", "/jobs/" + jobId, null);
    String status = /* data.status */;
    if (status.equals("succeeded") || status.equals("failed")) break;
    Thread.sleep(1500);
}
// the read is the plain-text string at data.output.output —
// the first four lines are VERDICT: / HS: / CONFIDENCE: / SUMMARY:
started = api("POST", "/run", { data: shipment, task: "Full compliance review" })

job = nil
loop do
  job = api("GET", "/jobs/#{started["job_id"]}")
  break if %w[succeeded failed].include?(job["status"])
  sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"

text = job["output"]["output"]
puts text.lines.grep(/\A(VERDICT|HS|CONFIDENCE):/)
$started = api("POST", "/run", [
    "data" => $shipment,
    "task" => "Full compliance review",
]);

do {
    sleep(2);
    $job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));

if ($job["status"] === "failed") {
    throw new Exception($job["error"] ?? "run failed");
}

$text = $job["output"]["output"];
foreach (explode("\n", $text) as $line) {
    if (preg_match('/^(VERDICT|HS):/', $line)) echo $line . "\n";
}
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", new {
    data = shipment, task = "Full compliance review" });
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true)
{
    job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
    var status = job.GetProperty("status").GetString();
    if (status is "succeeded" or "failed") break;
    await Task.Delay(1500);
}
var text = job.GetProperty("output").GetProperty("output").GetString()!;
foreach (var line in text.Split('\n'))
    if (line.StartsWith("VERDICT:") || line.StartsWith("HS:"))
        Console.WriteLine(line);

If the reply ever fails to match the contract, retry once with the same input plus a retry_note field describing the problem — the agent is instructed to obey it. That's exactly what the app itself does, and the retry is a second billed run, so keep the same Idempotency-Key base and bump an attempt counter.

Step 5 — The same run, streamed

POST /run-stream

Identical input to /run, but the response is text/event-stream, so you can show the read as it generates (the app's live panel does exactly this). Events:

EventData
job{job_id} — the run was accepted.
delta{text} — the next chunk of agent output.
done / pendingFinal payload: {job_id, status, charged_credits, truncated, output}. Authoritative — deltas can drop the tail, so always read the result from here.
error{code, message, job_id}.
curl -sN -X POST "$API/run-stream" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: customs-stream-$(date +%s)" \
  -d @input.json
# event: job    data: {"job_id":"job_…"}
# event: delta  data: {"text":"VERDICT: Proceed with caveats\n"}
# …
# event: done   data: {"job_id":"…","status":"succeeded","charged_credits":412,
#                      "output":{"output":"…the full read…"}}
res = requests.post(API + "/run-stream", json=payload, stream=True,
                    headers={"Authorization": f"Bearer {TOKEN}"})
event, done = None, None
for line in res.iter_lines(decode_unicode=True):
    if line.startswith("event:"):
        event = line[6:].strip()
    elif line.startswith("data:"):
        data = json.loads(line[5:])
        if event == "delta":
            print(data.get("text", ""), end="", flush=True)
        elif event in ("done", "pending"):
            done = data
        elif event == "error":
            raise RuntimeError(data.get("message"))

text = done["output"]["output"]  # authoritative full read
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = "message", out = "", done;
for (;;) {
  const chunk = await reader.read();
  if (chunk.done) break;
  buf += decoder.decode(chunk.value, { stream: true });
  let i;
  while ((i = buf.indexOf("\n")) >= 0) {
    const line = buf.slice(0, i); buf = buf.slice(i + 1);
    if (line.startsWith("event:")) event = line.slice(6).trim();
    else if (line.startsWith("data:")) {
      const data = JSON.parse(line.slice(5));
      if (event === "delta") out += data.text ?? "";
      else if (event === "done" || event === "pending") done = data;
      else if (event === "error") throw new Error(data.message);
    }
  }
}
const text = done.output.output; // authoritative full read
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
event, done := "", []byte(nil)
for sc.Scan() {
	line := sc.Text()
	if strings.HasPrefix(line, "event:") {
		event = strings.TrimSpace(line[6:])
	} else if strings.HasPrefix(line, "data:") {
		data := strings.TrimSpace(line[5:])
		if event == "delta" {
			// unmarshal {"text": …} and append
		} else if event == "done" || event == "pending" {
			done = []byte(data)
		}
	}
}
// unmarshal done → .output.output (the full plain-text read)
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payloadJson))
    .build();
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();

final String[] event = {""};
StringBuilder doneData = new StringBuilder();
lines.forEach(line -> {
    if (line.startsWith("event:")) event[0] = line.substring(6).trim();
    else if (line.startsWith("data:")) {
        if (event[0].equals("delta")) { /* parse {"text"} and append */ }
        else if (event[0].equals("done")) doneData.append(line.substring(5).trim());
    }
});
// parse doneData → output.output (the full plain-text read)
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = payload.to_json

event, done, buf = nil, nil, ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      buf << chunk
      while (i = buf.index("\n"))
        line = buf.slice!(0..i).chomp
        if line.start_with?("event:") then event = line[6..].strip
        elsif line.start_with?("data:")
          data = JSON.parse(line[5..])
          print data["text"] if event == "delta"
          done = data if %w[done pending].include?(event)
        end
      end
    end
  end
end
text = done["output"]["output"]  # authoritative full read
$event = ""; $done = null; $buf = "";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done, &$buf) {
        $buf .= $chunk;
        while (($i = strpos($buf, "\n")) !== false) {
            $line = rtrim(substr($buf, 0, $i)); $buf = substr($buf, $i + 1);
            if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
            elseif (str_starts_with($line, "data:")) {
                $data = json_decode(substr($line, 5), true);
                if ($event === "delta") echo $data["text"] ?? "";
                if ($event === "done" || $event === "pending") $done = $data;
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$text = $done["output"]["output"]; // authoritative full read
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream")
    { Content = JsonContent.Create(payload) };
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());

string? line; string ev = ""; JsonElement doneEl = default;
while ((line = await reader.ReadLineAsync()) != null)
{
    if (line.StartsWith("event:")) ev = line[6..].Trim();
    else if (line.StartsWith("data:"))
    {
        var data = JsonDocument.Parse(line[5..]).RootElement.Clone();
        if (ev == "delta") Console.Write(
            data.TryGetProperty("text", out var t) ? t.GetString() : "");
        else if (ev is "done" or "pending") doneEl = data;
    }
}
var text = doneEl.GetProperty("output").GetProperty("output").GetString()!;

The grounding contract applies over the API exactly as in the app: every factual claim in the read is traceable to your paste or to named tariff logic, gaps come back under Open questions instead of being filled with industry defaults, and duty rates are structural answers to verify against the live tariff — that's the product, not a limitation.