← Freight Desk Token page

Freight Desk API

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.

Input fields

The object you send — and it is the input object directly, not wrapped in {"input": ...}. Only exception is required.

FieldTypeMeaning
exceptionstringRequired. 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.
contextstringOptional, 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.
factsstringOptional. 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_notestringOptional, 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.
No slug in the path. Every route below is 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.

1. Get a token

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.

POST/guest

2. Check the session and balance

Confirms who the token belongs to and how many credits are available. Do this before a run: a 402 after submitting is avoidable.

GET/me

3. Estimate — free, no job created

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.

POST/estimate
What this app resolves to. Freight Desk requests the 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.

4. Run and poll

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.

POST/run
GET/jobs/{job_id}

5. Run with streaming (SSE)

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.

POST/run-stream
When a stream dies early. The app keeps whatever arrived and labels it a fragment: a section that never showed up is marked not received rather than 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.

6. Parsing the TYPE / SEVERITY / ACTION contract

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.

Output contract

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>
Parsing rules, as implemented. 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.

The eleven types and the five actions

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.

The consistency check the renderer applies

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.

Reformat retries

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."
}
Grounding. The assessment only argues from what is on the page. A figure in 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.

The envelope and error codes

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.

StatusCodeWhat to do
400VALIDATION_ERRORThe 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.
401UNAUTHORIZEDMissing, malformed or expired token. Mint a new one from the token page or POST /guest.
402PAYMENT_REQUIREDThe balance is below the run's hold. Call /estimate first and compare hold_credits against the credits from /me.
404NOT_FOUNDWrong slug or job id.
429RATE_LIMITEDBack off and retry with the same idempotency key.
5xxINTERNALRetry with the same idempotency key; a completed job is returned rather than re-billed.
Idempotency. Send 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.

The reformat retry is not free. Its -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.