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.
| Code | HTTP | What to do |
|---|---|---|
| UNAUTHORIZED | 401 | Token missing, expired or for another app. Mint a new one. |
| INSUFFICIENT_CREDITS | 402 | Balance below min_credits. Top up, or shorten the brief. |
| VALIDATION_ERROR | 400 | An input field is the wrong type. Every field here is a string. |
| RATE_LIMITED | 429 | Back off and retry once. Never tight-loop. |
| JOB_FAILED | 200 | The 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-side — task 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
task | string | yes | "plan" or "execute". Anything else is treated as plan, and the reply says so. |
brief | string | yes | The user's one-sentence description of what they are making. |
plan_json | string | execute only | execute only: the confirmed step list, JSON-encoded. Empty string for plan. |
facts_json | string | no | What the client read out of the brief locally — duration, aspect, language. JSON-encoded. |
notes | string | no | Free-text corrections, e.g. the open questions the user flagged. Empty string when there are none. |
$refs | array | no | Reserved 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 matching — q 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"
import requests
TOKEN = "YOUR_TOKEN"
r = requests.get("https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": "Bearer " + TOKEN})
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const r = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
headers: { Authorization: `Bearer ${TOKEN}` }
});
const { data } = await r.json();
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.GET().build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $token]);
$data = json_decode(curl_exec($ch), true)["data"];
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var res = await client.GetAsync("https://api.skillsafe.ai/v1/app-api/me");
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}]}'
import requests
TOKEN = "YOUR_TOKEN"
body = {
"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
}
]
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/estimate",
headers={"Authorization": "Bearer " + TOKEN},
json=body)
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const body = {
"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
}
]
};
const r = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const { data } = await r.json();
body := []byte(`{"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}]}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
String body = """
{"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}]}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", BodyPublishers.ofString(body)).build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = "{\"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}]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
$body = [
"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
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
$data = json_decode(curl_exec($ch), true)["data"];
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{""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}]}",
Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", body);
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}]}'
import requests
TOKEN = "YOUR_TOKEN"
body = {
"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
}
]
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run",
headers={"Authorization": "Bearer " + TOKEN},
json=body)
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const body = {
"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
}
]
};
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const { data } = await r.json();
body := []byte(`{"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}]}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
String body = """
{"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}]}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", BodyPublishers.ofString(body)).build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = "{\"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}]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
$body = [
"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
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
$data = json_decode(curl_exec($ch), true)["data"];
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{""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}]}",
Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
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}]}'
import requests
TOKEN = "YOUR_TOKEN"
body = {
"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
}
]
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run",
headers={"Authorization": "Bearer " + TOKEN},
json=body)
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const body = {
"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
}
]
};
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const { data } = await r.json();
body := []byte(`{"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}]}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
String body = """
{"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}]}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", BodyPublishers.ofString(body)).build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = "{\"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}]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
$body = [
"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
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
$data = json_decode(curl_exec($ch), true)["data"];
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{""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}]}",
Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
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}]}'
import requests
TOKEN = "YOUR_TOKEN"
body = {
"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
}
]
}
r = requests.post("https://api.skillsafe.ai/v1/app-api/run-stream",
headers={"Authorization": "Bearer " + TOKEN},
json=body)
print(r.json()["data"])
const TOKEN = "YOUR_TOKEN";
const body = {
"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
}
]
};
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const { data } = await r.json();
body := []byte(`{"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}]}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
String body = """
{"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}]}
""";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.method("POST", BodyPublishers.ofString(body)).build();
HttpResponse<String> res = client.send(req, BodyHandlers.ofString());
uri = URI("https://api.skillsafe.ai/v1/app-api/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = "{\"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}]}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
$body = [
"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
]
]
];
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $token,
"Content-Type: application/json"
]);
$data = json_decode(curl_exec($ch), true)["data"];
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{""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}]}",
Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream", body);
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
- Send an
Idempotency-Keyheader derived from the task plus the input. A retry after a dropped connection then replays instead of billing twice. hold_creditsis a reservation, not a price. Show it as reserved. Settlement refunds the difference, which is usually most of it.