Everything the web app does, you can do from your own code: paste a freight exception
— a damage report, a lost-shipment trace, an OS&D dump, a detention dispute, a
carrier email thread — and get it worked. The exception classified, the severity
scored, the absorb-or-claim call argued from the money, the evidence that decides it, the
carrier handled by mode, and the standard filing windows computed from the dates you send.
Useful for sweeping an OS&D queue overnight, triaging a claims backlog before an analyst
opens it, or refusing to close anything that comes back File a claim. Base URL
https://api.skillsafe.ai/v1/app-api.
The deadlines this API returns are statutory and industry defaults — the Carmack Amendment, COGSA, the Montreal Convention and the industry five-day concealed-damage window — applied as calendar arithmetic to the dates you send. They are estimates for working a file, not commitments any carrier has made and not a prediction of what a carrier or a court will accept. The governing bill of lading, the carrier’s tariff and any transportation agreement can set shorter windows and they control. Do not wire this into a system that treats a returned date as an operative legal deadline. Not legal advice.
The object you send — and it is the input object directly, not wrapped in {"input": ...}. Only exception is required.
| Field | Type | Meaning |
|---|---|---|
exception | string | Required. The freight exception as pasted: tracking events, the delivery receipt and its notations, the OS&D report, PRO and BOL numbers, piece counts and weights, values and repair quotes, the carrier email thread, the customer's escalation. Clipped at 40,000 characters — from the middle, keeping both ends, because a freight worksheet carries its case at both: the head holds the carrier, PRO, BOL, dates and amounts, and the tail holds the carrier's latest position and the customer escalation. The cut is announced in-band with how many characters were removed, and the model is told to treat that span as unread rather than absent. |
context | string | Optional, clipped at 6,000 characters. The mode, the customer at risk, the SLA or penalty exposure and the decision you actually need. It materially changes the answer: LTL, truckload, parcel, intermodal, ocean and air owe different notice windows and different liability limits, and the same damage reads differently when a top-five customer's burn-in window is at stake. |
facts | string | Optional. The deterministic output of the app's browser-side calculator — the mode and type selected, the delivery and discovery dates, the amount at stake with its settlement band and financial severity axis, every computed filing window with days remaining, and the mode's liability note. Treated as a hint, not a fact: every figure is reconciled against exception before it is repeated, and the model is explicitly allowed to reject a calculator date it cannot square with the paperwork. The app's renderer then names any computed date the assessment did not repeat, rather than trusting whichever came last. |
retry_note | string | Optional, and not for humans. The app sends it only on its automatic reformat retry, restating the required output shape after a reply failed to parse. It is a formatting instruction only: it can never change the type, the severity, the action or any number. It is deliberately excluded from the idempotency key material, because it is the app talking to the model about formatting, not a change to the question. |
https://api.skillsafe.ai/v1/app-api/<route> — /guest,
/me, /estimate, /run, /run-stream. There
is no /apps/{slug}/ segment: freight-desk is bound to the token
once, in the body of POST /guest. Sending it as an
X-App-Slug header instead is rejected with a 400.
Every call carries Authorization: Bearer <token>. Open the token page to sign in, reveal your token and copy a ready-made shell export — it never asks you to open the DevTools console. If you would rather script it, POST /guest mints a guest token for a named slug, and that body is where the slug is bound. A guest token works for reading and estimating; whether a guest can afford a run depends on this app's sponsorship budget, so a signed-in token is the reliable way to run.
# Every call below reuses these. Get the token from the token page linked
# above - never paste it into a shared shell history.
export SKILLSAFE_TOKEN="YOUR_TOKEN"
export SKILLSAFE_BASE="https://api.skillsafe.ai/v1/app-api"
# Or mint a guest token in one unauthenticated call. The app slug is bound to
# the token HERE, in the body, which is why no later path carries an
# /apps/{slug}/ segment. An X-App-Slug header is not accepted and 400s.
curl -s -X POST "$SKILLSAFE_BASE/guest" \
-H "Content-Type: application/json" \
-d '{"slug": "freight-desk"}'
# -> {"ok":true,"data":{"token":"...","expires_at":"..."}}
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(method, path, json_body=None, headers=None):
data = json.dumps(json_body).encode() if json_body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data: req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items(): req.add_header(k, v)
with urllib.request.urlopen(req) as r:
env = json.loads(r.read())
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"].get("message", ""))
return env["data"]
# Guest token, if you do not have one yet - the slug is bound to the token:
# TOKEN = call("POST", "/guest", {"slug": "freight-desk"})["token"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
// Read it from your own secret store; never hard-code a real token.
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body, extraHeaders) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(extraHeaders ?? {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message ?? ""}`);
return env.data;
}
// Guest token, if you do not have one yet - the slug is bound to the token:
// const { token } = await call("POST", "/guest", { slug: "freight-desk" });
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body any, hdr map[string]string) (map[string]any, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if body != nil { req.Header.Set("Content-Type", "application/json") }
for k, v := range hdr { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
if err := json.NewDecoder(res.Body).Decode(&env); err != nil { return nil, err }
if !env.OK { return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message) }
return env.Data, nil
}
// Guest token, if you do not have one yet - the slug is bound to the token:
// call("POST", "/guest", map[string]any{"slug": "freight-desk"}, nil)
import java.net.URI;
import java.net.http.*;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
class FreightDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static String call(String method, String path, String body, String idemKey) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (idemKey != null) b.header("Idempotency-Key", idemKey);
if (body != null) {
b.header("Content-Type", "application/json");
b.method(method, HttpRequest.BodyPublishers.ofString(body));
} else {
b.method(method, HttpRequest.BodyPublishers.noBody());
}
var res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}} - decode with your JSON library
}
}
// Guest token, if you do not have one yet - the slug is bound to the token:
// call("POST", "/guest", JSON.writeValueAsString(Map.of("slug", "freight-desk")), null);
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(method, path, body = nil, headers = {})
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
headers.each { |k, v| req[k] = v }
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env["error"]["code"]}: #{env["error"]["message"]}" unless env["ok"]
env["data"]
end
# Guest token, if you do not have one yet - the slug is bound to the token:
# TOKEN = call("POST", "/guest", { "slug" => "freight-desk" })["token"]
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call($method, $path, $body = null, $extra = []) {
global $TOKEN;
$headers = array_merge(["Authorization: Bearer $TOKEN"], $extra);
if ($body !== null) $headers[] = "Content-Type: application/json";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) throw new Exception($env["error"]["code"]);
return $env["data"];
}
// Guest token, if you do not have one yet - the slug is bound to the token:
// $TOKEN = call("POST", "/guest", ["slug" => "freight-desk"])["token"];
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
async Task<string> Call(string method, string path, string? body, string? idemKey = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (idemKey != null) req.Headers.Add("Idempotency-Key", idemKey);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync(); // {"ok":true,"data":{...}}
}
// Guest token, if you do not have one yet - the slug is bound to the token:
// await Call("POST", "/guest", JsonSerializer.Serialize(new { slug = "freight-desk" }));
Confirms who the token belongs to and how many credits are available. Do this before a run: a 402 after submitting is avoidable.
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"ok":true,"data":{"subject_type":"user","subject_id":"...","credits":12400}}
me = call("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
me, err := call("GET", "/me", nil, nil)
if err != nil { log.Fatal(err) }
fmt.Println(me["subject_type"], me["credits"])
String me = call("GET", "/me", null, null);
System.out.println(me);
me = call("GET", "/me")
puts "#{me["subject_type"]} #{me["credits"]}"
$me = call("GET", "/me");
print_r($me);
var me = await Call("GET", "/me", null);
Console.WriteLine(me);
Returns the credit hold a run would reserve, plus the resolved model and markup. It creates no job and charges nothing, so it is safe to call on every keystroke — the app debounces it behind the reserve meter. The response carries model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"exception": "OS&D EXCEPTION WORKSHEET - internal copy\nCARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872\nMODE: LTL, class 85, no appointment held\nPIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed\nDELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.\nDISCOVERED: 2026-07-30 while uncrating for burn-in.\nCrates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.\nINVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).\nCarrier email 08/03: declines to open a file, cites the clean delivery receipt.\nConsignee wants the racks staged Monday or the burn-in window slips.", "context": "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.", "facts": "Mode selected: LTL\nException type selected: Concealed damage\nDelivery/ETA date entered: July 27, 2026\nDiscovery date entered: July 30, 2026\nAmount at stake entered: $18,700\nStandard band for that amount: Full claim with documentation. Well above the settlement floor.\nFinancial severity axis alone: Level 4 (customer and time axes not scored here).\nDeadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left\nDeadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left\nLiability note: LTL liability is limited by the governing tariff and the released value on the BOL."}'
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":1400,"min_credits":200,"sponsor_enabled":true}}
INPUT = {
"exception": (
"OS&D EXCEPTION WORKSHEET - internal copy\n"
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872\n"
"MODE: LTL, class 85, no appointment held\n"
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed\n"
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.\n"
"DISCOVERED: 2026-07-30 while uncrating for burn-in.\n"
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.\n"
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).\n"
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.\n"
"Consignee wants the racks staged Monday or the burn-in window slips.\n"
),
"context": (
"I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with."
),
"facts": (
"Mode selected: LTL\n"
"Exception type selected: Concealed damage\n"
"Delivery/ETA date entered: July 27, 2026\n"
"Discovery date entered: July 30, 2026\n"
"Amount at stake entered: $18,700\n"
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.\n"
"Financial severity axis alone: Level 4 (customer and time axes not scored here).\n"
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left\n"
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left\n"
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL.\n"
),
}
est = call("POST", "/estimate", INPUT)
print(est["model"], est["model_alias"], est["markup_bps"], est["hold_credits"])
assert est["model_alias"] == "gpt-terra" # the alias is the stable part
const EXCEPTION = [
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips."
].join("\n");
const FACTS = [
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL."
].join("\n");
const INPUT = {
exception: EXCEPTION,
context:
"I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
facts: FACTS, // a hint from the browser calculator, not a fact
};
const est = await call("POST", "/estimate", INPUT);
console.log(est.model, est.model_alias, est.markup_bps, est.hold_credits);
exception := strings.Join([]string{
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips.",
}, "\n")
facts := strings.Join([]string{
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL.",
}, "\n")
input := map[string]any{
"exception": exception,
"context": "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
"facts": facts, // a hint from the browser calculator, not a fact
}
est, err := call("POST", "/estimate", input, nil)
if err != nil { log.Fatal(err) }
fmt.Println(est["model"], est["model_alias"], est["markup_bps"], est["hold_credits"])
String exception = String.join("\n",
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips.");
String facts = String.join("\n",
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL.");
// The input object goes on the wire DIRECTLY - it is never wrapped in {"input": ...}.
var input = Map.of(
"exception", exception,
"context", "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
"facts", facts);
String inputJson = JSON.writeValueAsString(input);
String est = call("POST", "/estimate", inputJson, null);
System.out.println(est); // model, model_alias, markup_bps, hold_credits
EXCEPTION = [
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips."
].join("\n")
FACTS = [
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL."
].join("\n")
INPUT = {
"exception" => EXCEPTION,
"context" => "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
"facts" => FACTS # a hint from the browser calculator, not a fact
}
est = call("POST", "/estimate", INPUT)
puts "#{est["model"]} #{est["model_alias"]} #{est["markup_bps"]} #{est["hold_credits"]}"
<?php
$EXCEPTION = implode("\n", [
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips."
]);
$FACTS = implode("\n", [
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL."
]);
$INPUT = [
"exception" => $EXCEPTION,
"context" => "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
"facts" => $FACTS, // a hint from the browser calculator, not a fact
];
$est = call("POST", "/estimate", $INPUT);
echo $est["model"], " ", $est["model_alias"], " ", $est["hold_credits"], "\n";
var exception = string.Join("\n", new[] {
"OS&D EXCEPTION WORKSHEET - internal copy",
"CARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872",
"MODE: LTL, class 85, no appointment held",
"PIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed",
"DELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.",
"DISCOVERED: 2026-07-30 while uncrating for burn-in.",
"Crates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.",
"INVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).",
"Carrier email 08/03: declines to open a file, cites the clean delivery receipt.",
"Consignee wants the racks staged Monday or the burn-in window slips."
});
var facts = string.Join("\n", new[] {
"Mode selected: LTL",
"Exception type selected: Concealed damage",
"Delivery/ETA date entered: July 27, 2026",
"Discovery date entered: July 30, 2026",
"Amount at stake entered: $18,700",
"Standard band for that amount: Full claim with documentation. Well above the settlement floor.",
"Financial severity axis alone: Level 4 (customer and time axes not scored here).",
"Deadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left",
"Deadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left",
"Liability note: LTL liability is limited by the governing tariff and the released value on the BOL."
});
// Serialized DIRECTLY as the request body - no {"input": ...} wrapper.
var inputJson = JsonSerializer.Serialize(new {
exception,
context = "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.",
facts,
});
var est = await Call("POST", "/estimate", inputJson);
Console.WriteLine(est); // model, model_alias, markup_bps, hold_credits
gpt-terra
alias, which resolves to gpt-5.6-terra at markup_bps 1000. Read
model and hold_credits from the estimate rather than assuming
either — the alias is the stable part, the resolved model is not.
hold_credits is what is reserved before the run, not the price:
the amount actually spent comes back as charged_credits on the finished job
(and on the SSE done event), and the unspent remainder of the hold is released.
Creates a job and returns {"job_id": "..."} immediately; poll GET /jobs/{job_id} until status is terminal, then read data.output.output. Always send an Idempotency-Key: a retried request with the same key returns the original job instead of billing twice.
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: fd1-9f3ac214b8e05d17-g1770043" \
-d '{"exception": "OS&D EXCEPTION WORKSHEET - internal copy\nCARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872\nMODE: LTL, class 85, no appointment held\nPIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed\nDELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.\nDISCOVERED: 2026-07-30 while uncrating for burn-in.\nCrates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.\nINVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).\nCarrier email 08/03: declines to open a file, cites the clean delivery receipt.\nConsignee wants the racks staged Monday or the burn-in window slips.", "context": "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.", "facts": "Mode selected: LTL\nException type selected: Concealed damage\nDelivery/ETA date entered: July 27, 2026\nDiscovery date entered: July 30, 2026\nAmount at stake entered: $18,700\nStandard band for that amount: Full claim with documentation. Well above the settlement floor.\nFinancial severity axis alone: Level 4 (customer and time axes not scored here).\nDeadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left\nDeadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left\nLiability note: LTL liability is limited by the governing tariff and the released value on the BOL."}'
# -> {"ok":true,"data":{"job_id":"job_..."}}
# Then poll until the job is terminal:
curl -s -X GET "https://api.skillsafe.ai/v1/app-api/jobs/job_..." \
-H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"ok":true,"data":{"status":"succeeded","charged_credits":1180,
# "output":{"output":"TYPE: Concealed damage\n..."}}}
import time
# INPUT is the object built in step 3 - sent DIRECTLY, not as {"input": INPUT}.
job = call("POST", "/run", INPUT, {"Idempotency-Key": "fd1-9f3ac214b8e05d17-g1770043"})
job_id = job["job_id"]
while True:
j = call("GET", "/jobs/" + job_id)
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
print(j["output"]["output"])
print("charged:", j.get("charged_credits")) # the actual cost, not the hold
// INPUT is the object built in step 3 - sent DIRECTLY, not as { input: INPUT }.
const { job_id } = await call("POST", "/run", INPUT, {
"Idempotency-Key": "fd1-9f3ac214b8e05d17-g1770043",
});
let j;
for (;;) {
j = await call("GET", `/jobs/${job_id}`);
if (["succeeded", "failed", "cancelled"].includes(j.status)) break;
await new Promise((r) => setTimeout(r, 2000));
}
console.log(j.output.output);
console.log("charged:", j.charged_credits); // the actual cost, not the hold
// input is the map built in step 3 - sent DIRECTLY, not wrapped.
job, err := call("POST", "/run", input, map[string]string{
"Idempotency-Key": "fd1-9f3ac214b8e05d17-g1770043",
})
if err != nil { log.Fatal(err) }
jobID := job["job_id"].(string)
var j map[string]any
for {
j, err = call("GET", "/jobs/"+jobID, nil, nil)
if err != nil { log.Fatal(err) }
status, _ := j["status"].(string)
if status == "succeeded" || status == "failed" || status == "cancelled" { break }
time.Sleep(2 * time.Second)
}
out := j["output"].(map[string]any)
fmt.Println(out["output"])
// inputJson is the serialized object from step 3 - sent DIRECTLY, not wrapped.
String job = call("POST", "/run", inputJson, "fd1-9f3ac214b8e05d17-g1770043");
String jobId = JSON.readTree(job).at("/data/job_id").asText();
String status = "";
com.fasterxml.jackson.databind.JsonNode j = null;
while (!status.equals("succeeded") && !status.equals("failed") && !status.equals("cancelled")) {
Thread.sleep(2000);
j = JSON.readTree(call("GET", "/jobs/" + jobId, null, null)).get("data");
status = j.get("status").asText();
}
System.out.println(j.at("/output/output").asText());
# INPUT is the hash built in step 3 - sent DIRECTLY, not as { "input" => INPUT }.
job = call("POST", "/run", INPUT, { "Idempotency-Key" => "fd1-9f3ac214b8e05d17-g1770043" })
job_id = job["job_id"]
j = nil
loop do
j = call("GET", "/jobs/#{job_id}")
break if %w[succeeded failed cancelled].include?(j["status"])
sleep 2
end
puts j["output"]["output"]
puts "charged: #{j["charged_credits"]}"
<?php
// $INPUT is the array built in step 3 - sent DIRECTLY, not wrapped.
$job = call("POST", "/run", $INPUT, ["Idempotency-Key: fd1-9f3ac214b8e05d17-g1770043"]);
$jobId = $job["job_id"];
do {
sleep(2);
$j = call("GET", "/jobs/" . $jobId);
} while (!in_array($j["status"], ["succeeded", "failed", "cancelled"], true));
echo $j["output"]["output"], "\n";
echo "charged: ", $j["charged_credits"], "\n";
// inputJson is the serialized object from step 3 - sent DIRECTLY, not wrapped.
var job = await Call("POST", "/run", inputJson, "fd1-9f3ac214b8e05d17-g1770043");
var jobId = JsonDocument.Parse(job).RootElement
.GetProperty("data").GetProperty("job_id").GetString();
JsonElement data;
string status;
do {
await Task.Delay(2000);
var polled = await Call("GET", $"/jobs/{jobId}", null);
data = JsonDocument.Parse(polled).RootElement.GetProperty("data");
status = data.GetProperty("status").GetString()!;
} while (status is not ("succeeded" or "failed" or "cancelled"));
Console.WriteLine(data.GetProperty("output").GetProperty("output").GetString());
Same job, delivered as server-sent events. job carries the job id, delta events carry incremental text, and done carries the authoritative full output plus charged_credits — trust done over the concatenated deltas, which can drop the tail. This is the lane the web app uses. Send the same Idempotency-Key discipline here as on /run.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fd1-9f3ac214b8e05d17-g1770043" \
-d '{"exception": "OS&D EXCEPTION WORKSHEET - internal copy\nCARRIER: Sierra Vantage Freight Lines (SVFL) PRO: 847-2290613 BOL: CDS-BOL-114872\nMODE: LTL, class 85, no appointment held\nPIECES: 12 crates (42U rack enclosures, crated, on skids), 9,840 lbs actual / 10,100 lbs billed\nDELIVERED: 2026-07-27 10:42, dock 4. POD signed CLEAN by K. Ortega, no exceptions noted.\nDISCOVERED: 2026-07-30 while uncrating for burn-in.\nCrates 3, 5 and 8 of 12: corner posts crushed inward, top rails bowed, one rack out of square.\nINVOICE VALUE: $62,300.00. REPAIR QUOTE: $18,700.00 (rework plus 4 replacement frame kits).\nCarrier email 08/03: declines to open a file, cites the clean delivery receipt.\nConsignee wants the racks staged Monday or the burn-in window slips.", "context": "I run claims for the shipper. Halberd is a top-five colocation customer and the burn-in window starts Monday. I need the absorb-or-claim call, and to know what a clean POD leaves us to argue with.", "facts": "Mode selected: LTL\nException type selected: Concealed damage\nDelivery/ETA date entered: July 27, 2026\nDiscovery date entered: July 30, 2026\nAmount at stake entered: $18,700\nStandard band for that amount: Full claim with documentation. Well above the settlement floor.\nFinancial severity axis alone: Level 4 (customer and time axes not scored here).\nDeadline - Concealed damage notice (industry 5-day standard from delivery): August 1, 2026, 0 days left\nDeadline - Carmack claim filing (9 months from delivery): April 27, 2027, 263 days left\nLiability note: LTL liability is limited by the governing tariff and the released value on the BOL."}'
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"TYPE: Concealed damage\nSEVERITY: Level 4..."}
# event: done data: {"output":{"output":"TYPE: ..."},"charged_credits":1180}
import json, urllib.request
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "fd1-9f3ac214b8e05d17-g1770043")
raw = []
with urllib.request.urlopen(req) as r:
event = None
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "delta":
raw.append(payload.get("text", ""))
elif event == "done":
raw = [payload["output"]["output"]] # authoritative
print("".join(raw))
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "fd1-9f3ac214b8e05d17-g1770043",
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "", raw = "", event = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const payload = JSON.parse(line.slice(5).trim());
if (event === "delta") raw += payload.text ?? "";
if (event === "done") raw = payload.output.output; // authoritative
}
}
}
console.log(raw);
// SSE: read the body line by line rather than decoding it as one JSON document.
inputJSON, _ := json.Marshal(input)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(inputJSON))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "fd1-9f3ac214b8e05d17-g1770043")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event, raw string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
var p struct {
Text string `json:"text"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &p)
if event == "delta" { raw += p.Text }
if event == "done" { raw = p.Output.Output } // authoritative
}
}
fmt.Println(raw)
// Stream the response body and split on SSE line prefixes.
var req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "fd1-9f3ac214b8e05d17-g1770043")
.POST(HttpRequest.BodyPublishers.ofString(inputJson))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
var raw = new StringBuilder();
final String[] event = { null };
res.body().forEach(line -> {
if (line.startsWith("event:")) event[0] = line.substring(6).trim();
else if (line.startsWith("data:") && "delta".equals(event[0])) {
// decode {"text":"..."} with your JSON library and append
raw.append(extractText(line.substring(5).trim()));
}
// on "done", replace raw with output.output - it is the authoritative copy
});
System.out.println(raw);
require "net/http"
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "fd1-9f3ac214b8e05d17-g1770043"
req.body = JSON.generate(INPUT)
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|
if line.start_with?("event:")
event = line[6..].strip
elsif line.start_with?("data:")
p = JSON.parse(line[5..].strip)
raw << p["text"].to_s if event == "delta"
raw = p["output"]["output"] if event == "done" # authoritative
end
end
end
end
end
puts raw
<?php
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Idempotency-Key: fd1-9f3ac214b8e05d17-g1770043"],
CURLOPT_POSTFIELDS => json_encode($INPUT),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) {
$p = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") $raw .= $p["text"] ?? "";
if ($event === "done") $raw = $p["output"]["output"]; // authoritative
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo $raw;
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Idempotency-Key", "fd1-9f3ac214b8e05d17-g1770043");
req.Content = new StringContent(inputJson, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
string? evt = null;
var raw = new StringBuilder();
while (!reader.EndOfStream) {
var line = await reader.ReadLineAsync();
if (line is null) continue;
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:") && evt == "delta") {
// decode {"text":"..."} with System.Text.Json and append
raw.Append(ExtractText(line[5..].Trim()));
}
// on "done", replace raw with output.output - it is the authoritative copy
}
Console.WriteLine(raw);
None., because inventing an all-clear for a Deadlines section the run never
reached would be worse than showing nothing. If you build your own client, make the same
distinction — an empty section and an absent section are different facts.
Nothing on the server parses the reply for you: data.output.output is plain text and the shape below is the whole interface. These are working decoders that validate the five header lines, require all six ## sections in order, and collapse the - None. convention into an empty list.
# There is no server-side parser: data.output.output is plain text and you
# decode it yourself. A shell-level smoke test of the five header lines:
OUT=$(curl -s -X GET "https://api.skillsafe.ai/v1/app-api/jobs/job_..." \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" | jq -r '.data.output.output')
printf '%s\n' "$OUT" | grep -E '^TYPE: (Delay|Visible damage|Concealed damage|Temperature damage|Shortage|Overage|Refusal|Misdelivery|Loss|Contamination|Carrier dispute)$'
printf '%s\n' "$OUT" | grep -E '^SEVERITY: Level [1-5]$'
printf '%s\n' "$OUT" | grep -E '^ACTION: (File a claim|Absorb the cost|Dispute the charge|Investigate first|Insufficient information)$'
printf '%s\n' "$OUT" | grep -E '^CONFIDENCE: [0-9]{1,3}$'
# All six headings must be present, in order:
printf '%s\n' "$OUT" | grep -c '^## ' # -> 6
import re
TYPES = ["Delay", "Visible damage", "Concealed damage", "Temperature damage",
"Shortage", "Overage", "Refusal", "Misdelivery", "Loss",
"Contamination", "Carrier dispute"]
ACTIONS = ["File a claim", "Absorb the cost", "Dispute the charge",
"Investigate first", "Insufficient information"]
SECTIONS = ["Immediate actions", "Evidence and documentation", "Claim and recovery", "Carrier and escalation", "Deadlines", "Open questions"]
def parse(text):
lines = text.strip().split("\n")
out = {"sections": {}}
current = None
summary = []
for line in lines:
h = re.match(r"^#{2,3}\s+(.*)$", line.strip())
if h:
current = h.group(1).strip()
out["sections"].setdefault(current, [])
continue
for tag in ("TYPE", "SEVERITY", "ACTION", "CONFIDENCE", "SUMMARY"):
m = re.match(tag + r"\s*:\s*(.*)$", line.strip(), re.I)
if m and tag.lower() not in out:
out[tag.lower()] = m.group(1).strip()
current = "__summary__" if tag == "SUMMARY" else None
break
else:
if current == "__summary__":
if not line.strip(): # SUMMARY ends at the first blank line
current = None
else:
summary.append(line.strip())
elif current in out["sections"]:
b = re.match(r"^\s*-\s+(.*)$", line)
if b: out["sections"][current].append(b.group(1).strip())
out["summary"] = " ".join([out.get("summary", "")] + summary).strip()
out["confidence"] = int(out.get("confidence", "-1"))
assert out.get("type") in TYPES, out.get("type")
assert re.fullmatch(r"Level [1-5]", out.get("severity", ""))
assert out.get("action") in ACTIONS, out.get("action")
assert 0 <= out["confidence"] <= 100
assert list(out["sections"]) == SECTIONS, "all six sections, in order"
# "- None." is how an empty section is spelled; it decodes to an empty list.
for k, v in out["sections"].items():
if v == ["None."]: out["sections"][k] = []
return out
const TYPES = ["Delay", "Visible damage", "Concealed damage", "Temperature damage",
"Shortage", "Overage", "Refusal", "Misdelivery", "Loss", "Contamination",
"Carrier dispute"];
const ACTIONS = ["File a claim", "Absorb the cost", "Dispute the charge",
"Investigate first", "Insufficient information"];
const SECTIONS = [
"Immediate actions",
"Evidence and documentation",
"Claim and recovery",
"Carrier and escalation",
"Deadlines",
"Open questions"
];
function parse(text) {
const out = { sections: {} };
const summary = [];
let current = null;
for (const line of text.trim().split("\n")) {
const h = line.trim().match(/^#{2,3}\s+(.*)$/);
if (h) { current = h[1].trim(); out.sections[current] = []; continue; }
const t = line.trim().match(/^(TYPE|SEVERITY|ACTION|CONFIDENCE|SUMMARY)\s*:\s*(.*)$/i);
if (t && out[t[1].toLowerCase()] === undefined) {
out[t[1].toLowerCase()] = t[2].trim();
current = t[1].toUpperCase() === "SUMMARY" ? "__summary__" : null;
continue;
}
if (current === "__summary__") {
if (!line.trim()) current = null; // SUMMARY ends at the first blank line
else summary.push(line.trim());
continue;
}
if (current && out.sections[current]) {
const b = line.match(/^\s*-\s+(.*)$/);
if (b) out.sections[current].push(b[1].trim());
}
}
out.summary = [out.summary ?? "", ...summary].join(" ").trim();
out.confidence = parseInt(out.confidence, 10);
if (!TYPES.includes(out.type)) throw new Error("bad TYPE: " + out.type);
if (!/^Level [1-5]$/.test(out.severity)) throw new Error("bad SEVERITY");
if (!ACTIONS.includes(out.action)) throw new Error("bad ACTION: " + out.action);
if (!(out.confidence >= 0 && out.confidence <= 100)) throw new Error("bad CONFIDENCE");
if (String(Object.keys(out.sections)) !== String(SECTIONS)) throw new Error("bad sections");
for (const k of SECTIONS) {
if (out.sections[k].length === 1 && /^none\.?$/i.test(out.sections[k][0])) {
out.sections[k] = []; // "- None." is an empty section
}
}
return out;
}
var types = map[string]bool{"Delay": true, "Visible damage": true,
"Concealed damage": true, "Temperature damage": true, "Shortage": true,
"Overage": true, "Refusal": true, "Misdelivery": true, "Loss": true,
"Contamination": true, "Carrier dispute": true}
var actions = map[string]bool{"File a claim": true, "Absorb the cost": true,
"Dispute the charge": true, "Investigate first": true,
"Insufficient information": true}
var sections = []string{"Immediate actions", "Evidence and documentation", "Claim and recovery", "Carrier and escalation", "Deadlines", "Open questions"}
type Report struct {
Type, Severity, Action, Summary string
Confidence int
Sections map[string][]string
Order []string
}
func parse(text string) (*Report, error) {
r := &Report{Sections: map[string][]string{}, Confidence: -1}
current := ""
for _, line := range strings.Split(strings.TrimSpace(text), "\n") {
t := strings.TrimSpace(line)
if strings.HasPrefix(t, "## ") {
current = strings.TrimSpace(t[3:])
r.Sections[current] = []string{}
r.Order = append(r.Order, current)
continue
}
switch {
case strings.HasPrefix(t, "TYPE:") && r.Type == "":
r.Type, current = strings.TrimSpace(t[5:]), ""
case strings.HasPrefix(t, "SEVERITY:") && r.Severity == "":
r.Severity, current = strings.TrimSpace(t[9:]), ""
case strings.HasPrefix(t, "ACTION:") && r.Action == "":
r.Action, current = strings.TrimSpace(t[7:]), ""
case strings.HasPrefix(t, "CONFIDENCE:") && r.Confidence < 0:
fmt.Sscanf(strings.TrimSpace(t[11:]), "%d", &r.Confidence)
current = ""
case strings.HasPrefix(t, "SUMMARY:") && r.Summary == "":
r.Summary, current = strings.TrimSpace(t[8:]), "__summary__"
case current == "__summary__":
if t == "" { current = "" } else { r.Summary += " " + t } // ends at a blank line
case current != "" && strings.HasPrefix(t, "- "):
r.Sections[current] = append(r.Sections[current], strings.TrimSpace(t[2:]))
}
}
if !types[r.Type] { return nil, fmt.Errorf("bad TYPE: %q", r.Type) }
if !actions[r.Action] { return nil, fmt.Errorf("bad ACTION: %q", r.Action) }
if r.Confidence < 0 || r.Confidence > 100 { return nil, fmt.Errorf("bad CONFIDENCE") }
if strings.Join(r.Order, "|") != strings.Join(sections, "|") {
return nil, fmt.Errorf("all six sections must appear in order")
}
for k, v := range r.Sections {
if len(v) == 1 && strings.EqualFold(strings.TrimSuffix(v[0], "."), "None") {
r.Sections[k] = nil // "- None." is an empty section
}
}
return r, nil
}
import java.util.*;
import java.util.regex.*;
static final List<String> TYPES = List.of("Delay", "Visible damage",
"Concealed damage", "Temperature damage", "Shortage", "Overage", "Refusal",
"Misdelivery", "Loss", "Contamination", "Carrier dispute");
static final List<String> ACTIONS = List.of("File a claim", "Absorb the cost",
"Dispute the charge", "Investigate first", "Insufficient information");
static final List<String> SECTIONS = List.of("Immediate actions",
"Evidence and documentation",
"Claim and recovery",
"Carrier and escalation",
"Deadlines",
"Open questions");
static Map<String, Object> parse(String text) {
var out = new LinkedHashMap<String, Object>();
var sections = new LinkedHashMap<String, List<String>>();
var summary = new StringBuilder();
String current = null;
for (String line : text.strip().split("\n")) {
String t = line.strip();
if (t.startsWith("## ")) {
current = t.substring(3).strip();
sections.put(current, new ArrayList<>());
continue;
}
Matcher m = Pattern.compile("^(TYPE|SEVERITY|ACTION|CONFIDENCE|SUMMARY)\\s*:\\s*(.*)$",
Pattern.CASE_INSENSITIVE).matcher(t);
if (m.matches() && !out.containsKey(m.group(1).toUpperCase())) {
out.put(m.group(1).toUpperCase(), m.group(2).strip());
current = m.group(1).equalsIgnoreCase("SUMMARY") ? "__summary__" : null;
continue;
}
if ("__summary__".equals(current)) {
if (t.isEmpty()) current = null; // SUMMARY ends at the first blank line
else summary.append(" ").append(t);
continue;
}
if (current != null && sections.containsKey(current) && t.startsWith("- ")) {
sections.get(current).add(t.substring(2).strip());
}
}
out.put("SUMMARY", (out.getOrDefault("SUMMARY", "") + summary).toString().strip());
int confidence = Integer.parseInt((String) out.get("CONFIDENCE"));
if (!TYPES.contains(out.get("TYPE"))) throw new IllegalStateException("bad TYPE");
if (!((String) out.get("SEVERITY")).matches("Level [1-5]")) throw new IllegalStateException("bad SEVERITY");
if (!ACTIONS.contains(out.get("ACTION"))) throw new IllegalStateException("bad ACTION");
if (confidence < 0 || confidence > 100) throw new IllegalStateException("bad CONFIDENCE");
if (!new ArrayList<>(sections.keySet()).equals(SECTIONS))
throw new IllegalStateException("all six sections must appear in order");
sections.replaceAll((k, v) ->
v.size() == 1 && v.get(0).replaceAll("\\.$", "").equalsIgnoreCase("None")
? List.of() : v); // "- None." is an empty section
out.put("sections", sections);
return out;
}
TYPES = ["Delay", "Visible damage", "Concealed damage", "Temperature damage",
"Shortage", "Overage", "Refusal", "Misdelivery", "Loss",
"Contamination", "Carrier dispute"].freeze
ACTIONS = ["File a claim", "Absorb the cost", "Dispute the charge",
"Investigate first", "Insufficient information"].freeze
SECTIONS = ["Immediate actions",
"Evidence and documentation",
"Claim and recovery",
"Carrier and escalation",
"Deadlines",
"Open questions"].freeze
def parse(text)
out = { "sections" => {} }
summary = []
current = nil
text.strip.split("\n").each do |line|
t = line.strip
if (h = t.match(/\A\#{2,3}\s+(.*)\z/))
current = h[1].strip
out["sections"][current] = []
next
end
if (m = t.match(/\A(TYPE|SEVERITY|ACTION|CONFIDENCE|SUMMARY)\s*:\s*(.*)\z/i)) &&
!out.key?(m[1].downcase)
out[m[1].downcase] = m[2].strip
current = m[1].casecmp?("SUMMARY") ? "__summary__" : nil
next
end
if current == "__summary__"
t.empty? ? (current = nil) : summary << t # SUMMARY ends at a blank line
next
end
if current && out["sections"].key?(current) && (b = t.match(/\A-\s+(.*)\z/))
out["sections"][current] << b[1].strip
end
end
out["summary"] = ([out["summary"].to_s] + summary).join(" ").strip
out["confidence"] = Integer(out["confidence"], 10)
raise "bad TYPE: #{out["type"]}" unless TYPES.include?(out["type"])
raise "bad SEVERITY" unless out["severity"] =~ /\ALevel [1-5]\z/
raise "bad ACTION: #{out["action"]}" unless ACTIONS.include?(out["action"])
raise "bad CONFIDENCE" unless (0..100).cover?(out["confidence"])
raise "all six sections, in order" unless out["sections"].keys == SECTIONS
out["sections"].each do |k, v|
out["sections"][k] = [] if v.length == 1 && v[0] =~ /\Anone\.?\z/i
end
out
end
<?php
const TYPES = ["Delay", "Visible damage", "Concealed damage", "Temperature damage",
"Shortage", "Overage", "Refusal", "Misdelivery", "Loss", "Contamination",
"Carrier dispute"];
const ACTIONS = ["File a claim", "Absorb the cost", "Dispute the charge",
"Investigate first", "Insufficient information"];
const SECTIONS = ["Immediate actions",
"Evidence and documentation",
"Claim and recovery",
"Carrier and escalation",
"Deadlines",
"Open questions"];
function parse(string $text): array {
$out = ["sections" => []];
$summary = [];
$current = null;
foreach (explode("\n", trim($text)) as $line) {
$t = trim($line);
if (preg_match('/^\#{2,3}\s+(.*)$/', $t, $h)) {
$current = trim($h[1]);
$out["sections"][$current] = [];
continue;
}
if (preg_match('/^(TYPE|SEVERITY|ACTION|CONFIDENCE|SUMMARY)\s*:\s*(.*)$/i', $t, $m)
&& !isset($out[strtolower($m[1])])) {
$out[strtolower($m[1])] = trim($m[2]);
$current = strcasecmp($m[1], "SUMMARY") === 0 ? "__summary__" : null;
continue;
}
if ($current === "__summary__") {
if ($t === "") $current = null; // SUMMARY ends at the first blank line
else $summary[] = $t;
continue;
}
if ($current !== null && isset($out["sections"][$current])
&& preg_match('/^-\s+(.*)$/', $t, $b)) {
$out["sections"][$current][] = trim($b[1]);
}
}
$out["summary"] = trim(implode(" ", array_merge([$out["summary"] ?? ""], $summary)));
$out["confidence"] = (int) $out["confidence"];
if (!in_array($out["type"], TYPES, true)) throw new Exception("bad TYPE");
if (!preg_match('/^Level [1-5]$/', $out["severity"])) throw new Exception("bad SEVERITY");
if (!in_array($out["action"], ACTIONS, true)) throw new Exception("bad ACTION");
if ($out["confidence"] < 0 || $out["confidence"] > 100) throw new Exception("bad CONFIDENCE");
if (array_keys($out["sections"]) !== SECTIONS) throw new Exception("all six sections, in order");
foreach ($out["sections"] as $k => $v) {
if (count($v) === 1 && preg_match('/^none\.?$/i', $v[0])) $out["sections"][$k] = [];
}
return $out;
}
using System.Text.RegularExpressions;
static readonly string[] Types = {
"Delay", "Visible damage", "Concealed damage", "Temperature damage", "Shortage",
"Overage", "Refusal", "Misdelivery", "Loss", "Contamination", "Carrier dispute" };
static readonly string[] Actions = {
"File a claim", "Absorb the cost", "Dispute the charge", "Investigate first",
"Insufficient information" };
static readonly string[] Sections = {
"Immediate actions",
"Evidence and documentation",
"Claim and recovery",
"Carrier and escalation",
"Deadlines",
"Open questions" };
record Report(string Type, string Severity, string Action, int Confidence,
string Summary, Dictionary<string, List<string>> Sections);
static Report Parse(string text) {
var fields = new Dictionary<string, string>();
var sections = new Dictionary<string, List<string>>();
var order = new List<string>();
var summary = new List<string>();
string? current = null;
foreach (var line in text.Trim().Split('\n')) {
var t = line.Trim();
var h = Regex.Match(t, @"^#{2,3}\s+(.*)$");
if (h.Success) {
current = h.Groups[1].Value.Trim();
sections[current] = new List<string>();
order.Add(current);
continue;
}
var m = Regex.Match(t, @"^(TYPE|SEVERITY|ACTION|CONFIDENCE|SUMMARY)\s*:\s*(.*)$",
RegexOptions.IgnoreCase);
if (m.Success && !fields.ContainsKey(m.Groups[1].Value.ToUpper())) {
fields[m.Groups[1].Value.ToUpper()] = m.Groups[2].Value.Trim();
current = m.Groups[1].Value.ToUpper() == "SUMMARY" ? "__summary__" : null;
continue;
}
if (current == "__summary__") {
if (t.Length == 0) current = null; // SUMMARY ends at the first blank line
else summary.Add(t);
continue;
}
var b = Regex.Match(t, @"^-\s+(.*)$");
if (current != null && sections.ContainsKey(current) && b.Success)
sections[current].Add(b.Groups[1].Value.Trim());
}
var confidence = int.Parse(fields["CONFIDENCE"]);
if (!Types.Contains(fields["TYPE"])) throw new InvalidOperationException("bad TYPE");
if (!Regex.IsMatch(fields["SEVERITY"], @"^Level [1-5]$")) throw new InvalidOperationException("bad SEVERITY");
if (!Actions.Contains(fields["ACTION"])) throw new InvalidOperationException("bad ACTION");
if (confidence is < 0 or > 100) throw new InvalidOperationException("bad CONFIDENCE");
if (!order.SequenceEqual(Sections)) throw new InvalidOperationException("all six sections, in order");
foreach (var k in Sections)
if (sections[k].Count == 1 && Regex.IsMatch(sections[k][0], @"^none\.?$", RegexOptions.IgnoreCase))
sections[k].Clear(); // "- None." is an empty section
return new Report(fields["TYPE"], fields["SEVERITY"], fields["ACTION"], confidence,
string.Join(" ", new[] { fields["SUMMARY"] }.Concat(summary)).Trim(),
sections);
}
data.output.output is plain text — no code fence around the response as a
whole — in exactly this shape: five header lines, then six ## sections in
this order. This is what the app's parser (freight.js) decodes; a reply that
breaks any rule below is discarded and retried once.
TYPE: <Delay | Visible damage | Concealed damage | Temperature damage | Shortage
| Overage | Refusal | Misdelivery | Loss | Contamination | Carrier dispute>
SEVERITY: <Level 1 | Level 2 | Level 3 | Level 4 | Level 5>
ACTION: <File a claim | Absorb the cost | Dispute the charge
| Investigate first | Insufficient information>
CONFIDENCE: <integer 0-100>
SUMMARY: <2 to 4 sentences, ending at the first blank line>
## Immediate actions
- <what to do now, and by when>
## Evidence and documentation
- <the document or photo that decides this exception type>
## Claim and recovery
- <the amount, the basis, the settlement floor>
## Carrier and escalation
- <mode-specific carrier behaviour, and the trigger to escalate>
## Deadlines
- <window, its basis, and the date>
## Open questions
- <question>
TYPE: is one of the eleven
values. SEVERITY: is Level 1 through Level 5.
ACTION: is exactly one of the five values — one action, the single next
move, not a menu. CONFIDENCE: is a bare integer 0-100, no percent sign.
SUMMARY: is 2 to 4 sentences, may wrap, and ends at the first blank line. All
six ## headings must appear, spelled exactly, in that order. Every line inside a
section is a - bullet, which may wrap onto indented continuation lines. An
empty section carries the single bullet - None.
TYPE is one of: Delay, Visible damage,
Concealed damage, Temperature damage, Shortage,
Overage, Refusal, Misdelivery, Loss,
Contamination, Carrier dispute.
ACTION is one of: File a claim, Absorb the cost,
Dispute the charge, Investigate first,
Insufficient information.
Absorbing a Level 3 or higher exception contradicts the skill's own bands — absorb is
the under-$500, low-stakes lane. So the app flags an ACTION: Absorb the cost
paired with SEVERITY: Level 3 or worse rather than trusting the tag lines
blind. Worth reproducing in your own client: it is the cheapest check on the whole contract.
retry_note rides along in the same input object and is the only field you would
not send on a first pass. It restates the shape; it never restates the question:
{
"exception": "OS&D EXCEPTION WORKSHEET - internal copy\nCARRIER: Sierra Vantage Freight Lines ...",
"context": "I run claims for the shipper ...",
"facts": "Mode selected: LTL\nException type selected: Concealed damage ...",
"retry_note": "Your previous reply did not parse. Re-emit the SAME assessment, unchanged in substance, in the required shape: the five header lines TYPE, SEVERITY, ACTION, CONFIDENCE and SUMMARY, then the six ## sections in order, each line a '- ' bullet. No code fence around the response."
}
facts that cannot be reconciled against exception is dropped rather
than repeated — including a computed deadline the model does not believe the paperwork
supports. Where the record does not support a call, ACTION is
Insufficient information and the questions that would settle it go in
## Open questions. The deadlines are statutory and industry defaults under the
Carmack Amendment, COGSA and the Montreal Convention, not commitments any carrier has made;
the governing bill of lading, tariff and transportation agreement control.
Every response is {"ok": true, "data": {...}} or
{"ok": false, "error": {"code": "...", "message": "..."}}. Check
ok before reading data, and branch on
error.code rather than on the message text.
| Status | Code | What to do |
|---|---|---|
| 400 | VALIDATION_ERROR | The input shape is wrong. error.details names the field — most often a missing exception, an input object wrapped in {"input": ...} when it should be sent directly, or a slug passed as an X-App-Slug header instead of in the POST /guest body. |
| 401 | UNAUTHORIZED | Missing, malformed or expired token. Mint a new one from the token page or POST /guest. |
| 402 | PAYMENT_REQUIRED | The balance is below the run's hold. Call /estimate first and compare hold_credits against the credits from /me. |
| 404 | NOT_FOUND | Wrong slug or job id. |
| 429 | RATE_LIMITED | Back off and retry with the same idempotency key. |
| 5xx | INTERNAL | Retry with the same idempotency key; a completed job is returned rather than re-billed. |
Idempotency-Key on every /run
and /run-stream. The app derives it from a content hash of the input plus a
per-submit nonce, giving keys shaped fd1-<16 hex>-g<n>, with the
automatic reformat retry taking the same key plus a -reformat suffix. Two
consequences worth copying: a network retry of the same submission reuses the same
key, so it collapses server-side and cannot double-bill; and a deliberate second run of a
byte-identical paste gets a new nonce, so it is a genuinely new run rather than a replay of
the first answer returned with deduped: true. A bare content hash, or a hash
plus an attempt counter that resets to 1 on every submit, fails that second property —
which is the whole reason the nonce exists. The hash covers exception,
context and facts only — retry_note is excluded
on purpose, since it changes the formatting instruction and not the question.
-reformat key is distinct
from the main run’s, which is what makes replaying it safe — but it is a second
model call and it is a second model call and it is billed. The app says so in its
own UI rather than offering a free do-over, and any client you build on this API should too.