← URDF Desk / API
Tokens

Drive URDF Desk from your own code

Base URL https://api.skillsafe.ai/v1/app-api. One robot description goes in, one structured review comes back. Everything on this page uses the same token the web app uses, which you can read off the tokens page.

The task field comes first

This app has three lanes over one work object, and task selects which one you get. It is the field to decide before any other: it changes the body key you get back, the posture vocabulary, the artifacts, and the price. Omit it and the model picks the lane your input best fits and names its choice in exec_summary — convenient interactively, not something to rely on in a script.

taskLaneWhat it readsBody keypostureArtifacts
structureStructure & physicsThe kinematic tree, the inertials, the joint limits and axes, the geometry and mesh references.links[]spawnable / spawnable-with-caveats / not-spawnablestructure-review.md, links.csv
planningMoveIt 2 semanticsThe planning groups, chains, group states, end effectors, virtual joint and self-collision matrix, each resolved against the URDF. Paste no SRDF and this lane authors one.groups[]plannable / plannable-with-caveats / not-plannableplanning-review.md, robot.srdf
simGazebo handoffThe SDFormat version, per-link inertials and collisions, mesh URI schemes, sensors, plugins and physics — plus which URDF links did not survive the conversion.entities[]sim-ready / sim-ready-with-caveats / not-sim-readysim-review.md, entities.csv

The envelope

Every response has the same shape. Check ok before touching data.

{"ok": true,  "data":  { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "...", "details": { ... }}}
HTTPerror.codeWhat to do
401UNAUTHORIZEDThe token is missing, malformed or expired. Get a fresh one from the tokens page.
402INSUFFICIENT_CREDITSThe balance is below min_credits. /estimate is free, so check it first.
403FORBIDDENA guest token tried a metered call. /run and /run-stream need a personal token.
404NOT_FOUNDUsually a job id that does not exist, or a mistyped path.
409IDEMPOTENCY_CONFLICTThe same Idempotency-Key was reused with a different body. Change the key or send the original body.
422VALIDATION_ERRORThe input object is the wrong shape. Note that the body is the input object — do not wrap it in an input key.
429RATE_LIMITEDBack off and retry; do not tight-loop.
503UPSTREAM_UNAVAILABLEThe model provider is briefly unavailable. Retry with the same idempotency key.

Input fields

The request body is the input object itself. Do not wrap it in an input key: a wrapped body returns 200 while hiding task from the model, so you silently get whichever lane it guessed.

FieldTypeRequiredMeaning
taskstringrecommendedOne of structure, planning, sim.
descriptionstringyesThe robot description text. Several files in one string are fine — separate them with a line reading # file: name.urdf. URDF, SRDF and SDF are told apart by their content, not by the name.
notesstringnoWhat you are about to do with the robot. Short and concrete sharpens the review a lot.
prescanobjectnoThe deterministic facts and flags the browser computes. The web app always sends it, and the prompt requires one coverage_check entry per flag. Omit it and you get a review with an empty coverage_check — still useful, but nothing holds the model to arithmetic.
clip_notestringnoSet this when you have truncated a long description yourself, so the model knows what it is missing.

Output contract

job.output is a JSON string; parse it. Inside is one object: a common envelope shared by all three lanes, plus that lane’s own body key from the table above. Taken from the parser the web app itself uses.

{
  "task":           "structure" | "planning" | "sim",
  "title":          string,
  "posture":        one of the lane's three values,
  "confidence":     "high" | "medium" | "low",
  "verdict":        string   // one actionable sentence
  "exec_summary":   string,
  "findings": [{
     "id":       "S-001" | "P-001" | "M-001",
     "severity": "critical" | "high" | "medium" | "low",
     "area":     lane-specific,
     "target":   string,   // the link, joint, group or element
     "title":    string,
     "evidence": string,   // the exact values from your input
     "impact":   string,
     "remedy":   string,
     "blocks":   boolean,
     "elements_cited": [string]
  }],
  "coverage_check": [{"id": prescan flag id,
                      "status": "confirmed" | "set-aside" | "contradicted",
                      "note": string}],
  "assumptions":    [string],
  "open_questions": [string],
  "artifacts":      [{"name": string,
                      "language": "markdown" | "csv" | "xml" | "text",
                      "content": string}],
  "next_steps":     [string],
  "summary":        string,

  // exactly one of these, matching "task":
  "links":    [{"name", "role", "parent_joint", "mass_kg", "grade", "issue", "note"}],
  "groups":   [{"name", "origin", "kind", "dof", "members", "grade", "issue", "note"}],
  "entities": [{"name", "kind", "grade", "issue", "note"}]
}

grade is always one of ok, watch, broken. An empty array is a legitimate answer and does not mean the run failed — a description with nothing wrong in a lane returns no findings for it.

1. A tiny client helper

Every endpoint below returns the same {ok, data, error} envelope and takes the same two headers, so one small helper covers the whole API. Get your token from the tokens page — no developer console required.

# Every call needs the same two headers. Keep the token out of your shell history:
# read it from a file you control, or paste it into a variable in a subshell.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"

call() {  # call <method> <path> [json]
  curl -sS -X "$1" "$BASE/$2" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    ${3:+--data "$3"}
}

2. Check the session and the balance

GET /me tells you whether the token is a guest or a personal session, and what the credit balance is. Do this before a metered call so a shortfall is something you handle rather than a 402 you are surprised by.

call GET me

3. Price a lane for free

POST /estimate costs nothing and starts no job. It returns hold_credits (what will be reserved, priced against the full output cap), min_credits, and the model binding. The hold differs per lane, because the lanes have different prompts and output caps — estimate the lane you are actually about to run.

# /estimate is FREE and runs no job. It is the honest way to price a lane.
DESC=$(python3 -c 'import json,sys;print(json.dumps(open("my_robot.urdf").read()))')
call POST estimate "{\"task\":\"structure\",\"description\":$DESC}"

4. Run a review and poll for it

POST /run is metered and returns a job_id; poll GET /jobs/{id} until status is succeeded or failed. Always send an Idempotency-Key: it makes a retry after a network blip free instead of double-billing. Include the lane in the key — two lanes over one description are two distinct runs and must not collide.

# Metered. The Idempotency-Key makes a retry safe: the same key returns the
# same job instead of billing twice.
KEY="urdf-desk:structure:$(shasum -a 256 my_robot.urdf | cut -c1-16):a0"
DESC=$(python3 -c 'import json,sys;print(json.dumps(open("my_robot.urdf").read()))')
JOB=$(curl -sS -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  --data "{\"task\":\"structure\",\"description\":$DESC}" \
  | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll until terminal.
while :; do
  OUT=$(call GET "jobs/$JOB")
  echo "$OUT" | grep -q '"status":"succeeded"' && break
  echo "$OUT" | grep -q '"status":"failed"' && { echo "$OUT"; exit 1; }
  sleep 2
done
echo "$OUT"

5. Stream it instead

POST /run-stream returns server-sent events. A full review takes tens of seconds, so streaming lets you show progress. Concatenate every delta, then parse the accumulated text as one JSON object. The same Idempotency-Key rules apply.

# Server-sent events. Each data: line carries a delta; the terminal event
# carries the whole output. Useful because a full review takes tens of seconds.
curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  --data "{\"task\":\"planning\",\"description\":$DESC}"

One worked example per lane

The same description, three requests. Only task changes.

task: "structure" — Structure & physics

# request
{"task": "structure", "description": "<robot name=\"acme_arm\"> ... </robot>",
 "notes": "Going into a MoveIt config next week."}

# response: job.output parsed
{"task": "structure", "posture": ..., "verdict": ...,
 "findings": [...], "links": [...], "artifacts": ["structure-review.md", "links.csv"]}

task: "planning" — MoveIt 2 semantics

# request
{"task": "planning", "description": "<robot name=\"acme_arm\"> ... </robot>",
 "notes": "Going into a MoveIt config next week."}

# response: job.output parsed
{"task": "planning", "posture": ..., "verdict": ...,
 "findings": [...], "groups": [...], "artifacts": ["planning-review.md", "robot.srdf"]}

task: "sim" — Gazebo handoff

# request
{"task": "sim", "description": "<robot name=\"acme_arm\"> ... </robot>",
 "notes": "Going into a MoveIt config next week."}

# response: job.output parsed
{"task": "sim", "posture": ..., "verdict": ...,
 "findings": [...], "entities": [...], "artifacts": ["sim-review.md", "entities.csv"]}

Costs, and what not to do on a schedule

What this app does not do

It reads the description text you send and returns judgement over it. It never loads your robot, never resolves a mesh file, never spawns anything, and never runs a command. Findings about a mesh are about its path; findings about physics are computed from the numbers you sent. That boundary is deliberate and the prompt enforces it.

Credits

URDF Desk is a derived work built on three agent skills by @earthtojake/urdf, @earthtojake/srdf and @earthtojake/sdf. It is not a republication of those skills.