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).
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/billing. |
403 | The token isn't allowed to do this. |
404 | Unknown job or record id. |
5xx | Transient 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
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
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
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
/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 field | Type | Notes |
|---|---|---|
data | string, required | The 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. |
notes | string, optional | What the specialist should know: the decision you face, deadlines, what the supplier or broker suggested, what worries you. Up to 6,000 chars. |
task | string, optional | One of Full compliance review (default), Classification focus, FTA and duty savings, Docs and entry readiness, Red-flag screen. |
facts | string, optional | A 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_note | string, optional | Feedback 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
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:
| Event | Data |
|---|---|
job | {job_id} — the run was accepted. |
delta | {text} — the next chunk of agent output. |
done / pending | Final 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.