Make Route

API

Make Route is two tasks over one system prompt. You send a plain-language brief and get a proposed route; you send the confirmed route back and get the production package. Everything below is the same path the web app uses.

Base URL and envelope

All calls go to https://api.skillsafe.ai/v1/app-api. Every response is wrapped: success is {"ok":true,"data":{...}}, failure is {"ok":false,"error":{"code":"...","message":"..."}}. Read data, never the top level.

CodeHTTPWhat to do
UNAUTHORIZED401Token missing, expired or for another app. Mint a new one.
INSUFFICIENT_CREDITS402Balance below min_credits. Top up, or shorten the brief.
VALIDATION_ERROR400An input field is the wrong type. Every field here is a string.
RATE_LIMITED429Back off and retry once. Never tight-loop.
JOB_FAILED200The job reached a terminal failed status; read error on the job.

The input contract

The run body is the input object. Do not wrap your fields in an outer input key — that returns 200 while hiding every field from the model, which reads as a bad answer rather than a bad request.

Every field is a string. Nested structures are JSON-encoded into a string field at the wire boundary and nowhere else.

Two fields are declared required server-sidetask and brief. Omit either and /estimate and /run return a warnings entry naming it. This is the only shape check that applies to your call: the browser app also guards client-side, and a direct API caller does not get that guard. plan_json is required by the execute task but cannot be declared conditionally, so its absence is not warned about — send it.

FieldTypeRequiredMeaning
taskstringyes"plan" or "execute". Anything else is treated as plan, and the reply says so.
briefstringyesThe user's one-sentence description of what they are making.
plan_jsonstringexecute onlyexecute only: the confirmed step list, JSON-encoded. Empty string for plan.
facts_jsonstringnoWhat the client read out of the brief locally — duration, aspect, language. JSON-encoded.
notesstringnoFree-text corrections, e.g. the open questions the user flagged. Empty string when there are none.
$refsarraynoReserved platform key. Retrieves the private step catalogue server-side; it is stripped before the model sees it and never reaches the caller. The reference is a search reference and search is case-insensitive SUBSTRING matchingq is a needle that must occur inside a record, so a natural-language sentence matches nothing and silently retrieves an empty catalogue. Send the anchor token pipeline-step (carried by every record) to retrieve the whole catalogue, or an exact step id to retrieve one record.

1. Get a token

Open the token page, sign in, and copy the token. It is scoped to this app. A guest token can call /me and /estimate but cannot run a metered task.

export SKILLSAFE_TOKEN="YOUR_TOKEN"

2. Confirm the session

Every call needs a token. /me tells you whose it is and what it can spend. A personal session reads subject_type: "user"; a guest reads "guest" and cannot run a metered task.

curl -s "https://api.skillsafe.ai/v1/app-api/me" \
  -H "Authorization: Bearer YOUR_TOKEN"

3. Price the run before you make it

/estimate applies the identical gate as a real run and costs nothing. hold_credits is what gets reserved, not what you pay — settlement refunds the difference. A $refs lookup is surcharged by the injection cap, so estimate the body you actually intend to send.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task": "plan", "brief": "a 20-second vertical ad for our cold-brew can, Spanish voiceover, I have one product photo", "notes": "", "plan_json": "", "facts_json": "{\"seconds\":20,\"aspect\":\"9:16\",\"language\":\"es-ES\"}", "$refs": [{"path": "private/skills.jsonl", "q": "pipeline-step", "limit": 40}]}'

4. Run the plan task

The response carries a job_id. Poll /jobs/{job_id} until status is succeeded or failed; the model's text is at output.output. Send an Idempotency-Key header so a retry after a dropped connection does not bill twice.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task": "plan", "brief": "a 20-second vertical ad for our cold-brew can, Spanish voiceover, I have one product photo", "notes": "", "plan_json": "", "facts_json": "{\"seconds\":20,\"aspect\":\"9:16\",\"language\":\"es-ES\"}", "$refs": [{"path": "private/skills.jsonl", "q": "pipeline-step", "limit": 40}]}'

5. Run the execute task

Same endpoint, different task. plan_json carries the confirmed step list as a JSON string — the run body is the input object itself, so never wrap your fields in an outer input key.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task": "execute", "brief": "a 20-second vertical ad for our cold-brew can, Spanish voiceover, I have one product photo", "notes": "", "plan_json": "{\"steps\":[{\"n\":1,\"skill_id\":\"brief-lock\"},{\"n\":2,\"skill_id\":\"product-hero\"}]}", "facts_json": "{\"seconds\":20}", "$refs": [{"path": "private/skills.jsonl", "q": "brief-lock", "limit": 2}, {"path": "private/skills.jsonl", "q": "product-hero", "limit": 2}]}'

6. Stream instead of polling

/run-stream returns text/event-stream. Read the SSE frames yourself: named events job, delta, done and error, each with a JSON data: line. Browsers receive job ticks rather than token deltas, so do not build a typing effect on this.

curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task": "plan", "brief": "a 20-second vertical ad for our cold-brew can, Spanish voiceover, I have one product photo", "notes": "", "plan_json": "", "facts_json": "{\"seconds\":20,\"aspect\":\"9:16\",\"language\":\"es-ES\"}", "$refs": [{"path": "private/skills.jsonl", "q": "pipeline-step", "limit": 40}]}'

7. What comes back

The model's text sits at output.output on the finished job. It is a single fenced JSON block. Parse the fence, then the JSON.

task: plan

{
  "task": "plan",
  "read": "A 20-second vertical ad for a cold-brew can with a Spanish voiceover.",
  "target": { "format": "vertical video", "aspect": "9:16", "duration": "20s",
              "language": "es-ES", "platform": "unstated" },
  "unknowns": ["Which platform is this for? It decides the safe area and the cut length."],
  "steps": [
    { "n": 1, "skill_id": "brief-lock", "title": "Lock the 9:16 20-second target",
      "why": "Every later step inherits one target instead of guessing.",
      "needs": "your brief", "gives": "a target spec", "cost": "free",
      "optional": false, "after": [] }
  ],
  "watch": ["Label text on the can warps unless the real photo conditions the generation."]
}

after lists the step numbers a step genuinely cannot run before. The web app warns the user with exactly this field when they drop or reorder a step.

task: execute

{
  "task": "execute",
  "title": "Spanish Cold-Brew Can Ad",
  "target": { "format": "vertical video", "aspect": "9:16", "duration": "20s",
              "language": "es-ES", "platform": "unstated" },
  "steps": [
    { "n": 2, "skill_id": "product-hero", "title": "Studio hero from the supplied photo",
      "do": "Condition the generation on the photo you already have ...",
      "prompt": "Studio packshot of the cold-brew can on a seamless ...",
      "settings": ["aspect: 9:16", "seed: locked"],
      "input": "your product photo", "output": "hero-can.png",
      "check": "The label text is legible at 100%.",
      "fallback": "Re-shoot the reference straight-on and re-run." }
  ],
  "sequence_notes": ["..."],
  "budget": { "cheap_first": "...", "biggest_cost": "..." },
  "not_covered": ["Music licensing.", "Platform upload."]
}

Steps come back in exactly the order you confirmed. If your order breaks a dependency it is written as confirmed and the conflict is raised in sequence_notes — the router will not silently reorder your plan. A step that cannot run where you put it has output: "none — blocked at this position".

8. Two things that will cost you a run