Costume Squad over HTTP
Base URL https://api.skillsafe.ai/v1/app-api. Every response is the envelope { "ok": true, "data": ... } or { "ok": false, "error": { "code", "message" } }. /guest, /me and /estimate are free; a run is metered against the caller's SkillSafe credits.
The request body
One flat JSON object, posted as the whole body, with scalar fields only. Do not wrap it in {"input": ...}: a wrapper is accepted with a 200 and hides every field from the model. Costume Squad is a two-lane app, and the lane is chosen by task:
| Field | Type | Meaning |
|---|---|---|
task | string, required | pitch: three group costume themes, each with casting. build: a full per-person plan for one chosen theme. Send this first; every other field is read in its light. |
members | string, required | a JSON-encoded array (a string, not an array) of {name, budget, owns, limits, notes}. budget is a number in USD or null; owns and limits are arrays of strings. At most 12 people, and names must be unique: casting refers to people by name. |
occasion | string | e.g. house party, bar or club, costume contest, office, trick-or-treating with kids, school or family event, outdoor event |
vibe | string | trending | spooky | funny | cute | clever | mythic |
seeds | string | optional comma-separated theme ideas the group already likes |
theme | string | build only: the chosen group theme, usually a pitches[].theme from an earlier pitch run |
casting | string | build only, optional: JSON-encoded [{member, character}]. Omit it and the model casts |
days_left | number | whole days until October 31; drives the shopping timeline |
prescan | string | JSON-encoded facts the web app computes before sending: member_count, budget_total, missing_budget, empty_closets, dark_closets, seeds_that_fit, clipped. Optional for API callers |
Encode members twice. Build the array in your language, serialize it to a string, put that string in the body, then serialize the body. On the wire it looks like "members": "[{\"name\":\"Maya\",...}]". Every sample below does this with the language's own JSON encoder; never hand-write the escapes. Check the 12-person cap and name uniqueness on your side before sending.
Example: pitch, three friends
Maya ($30, owns an orange cardigan and chunky glasses), Theo ($20, brown cargo pants and a black hoodie, no face paint) and Sam ($15, a brown fleece onesie, hates masks), going to a house party, 13 days out. This is the exact body on the wire:
{
"task": "pitch",
"members": "[{\"name\":\"Maya\",\"budget\":30,\"owns\":[\"orange cardigan\",\"chunky glasses\"],\"limits\":[],\"notes\":\"\"},{\"name\":\"Theo\",\"budget\":20,\"owns\":[\"brown cargo pants\",\"black hoodie\"],\"limits\":[\"no face paint\"],\"notes\":\"\"},{\"name\":\"Sam\",\"budget\":15,\"owns\":[\"brown fleece onesie\"],\"limits\":[\"hates masks\"],\"notes\":\"\"}]",
"occasion": "house party",
"vibe": "trending",
"seeds": "Scooby-Doo gang",
"days_left": 13
}
The members string, decoded:
[
{"name": "Maya", "budget": 30, "owns": ["orange cardigan", "chunky glasses"], "limits": [], "notes": ""},
{"name": "Theo", "budget": 20, "owns": ["brown cargo pants", "black hoodie"], "limits": ["no face paint"], "notes": ""},
{"name": "Sam", "budget": 15, "owns": ["brown fleece onesie"], "limits": ["hates masks"], "notes": ""}
]
Example: build, a couple
Lena ($35, white slip dress, black trench coat, combat boots, no wigs) and Marcus ($35, black suit, yellow dish gloves, wears glasses) have picked a theme and a casting, heading to a bar:
{
"task": "build",
"members": "[{\"name\":\"Lena\",\"budget\":35,\"owns\":[\"white slip dress\",\"black trench coat\",\"combat boots\"],\"limits\":[\"no wigs\"],\"notes\":\"\"},{\"name\":\"Marcus\",\"budget\":35,\"owns\":[\"black suit\",\"yellow dish gloves\"],\"limits\":[\"wears glasses\"],\"notes\":\"\"}]",
"theme": "Cyclops & Jean Grey (X-Men)",
"casting": "[{\"member\":\"Lena\",\"character\":\"Jean Grey\"},{\"member\":\"Marcus\",\"character\":\"Cyclops\"}]",
"occasion": "bar or club",
"vibe": "spooky",
"days_left": 13
}
The output contract
The finished job carries output; output.output is the model's reply text, and that text is one JSON object. Parse it a second time. Its lane field tells you which shape you have. Samples are truncated (...).
pitch – exactly three pitches
{
"lane": "pitch",
"pitches": [
{
"id": "p1",
"theme": "Mystery Inc. (Scooby-Doo)",
"hook": "Two of the three costumes are already hanging in a closet.",
"trend": null,
"family_read": "Every age in the room names it on sight.",
"casting": [
{ "member": "Maya", "character": "Velma", "why": "Orange cardigan and chunky glasses are the costume." },
{ "member": "Theo", "character": "Shaggy", "why": "Brown cargo pants plus a green tee; no face paint." },
{ "member": "Sam", "character": "Scooby-Doo", "why": "Brown fleece onesie, ears and a collar; no mask." }
],
"est_cost": [ { "member": "Maya", "usd": 6 }, { "member": "Theo", "usd": 12 }, { "member": "Sam", "usd": 10 } ],
"difficulty": "easy",
"risks": [ "Shaggy needs a green tee; Theo owns a black hoodie, not green." ]
},
{ "id": "p2", ... },
{ "id": "p3", ... }
],
"notes": [ "All three pitches stay inside every budget." ]
}
trend is a short note on why the theme is current, or null. difficulty is easy, medium or hard. Each casting[].member and est_cost[].member is a name from your members. To go deeper on one pitch, send its theme and casting back as a build request.
build – one plan per person
{
"lane": "build",
"theme": "Cyclops & Jean Grey (X-Men)",
"group_read": "Red visor plus a green-and-gold silhouette reads as a pair across a dark bar.",
"members": [
{
"member": "Marcus",
"character": "Cyclops",
"silhouette": "Dark suit, ruby visor line across the eyes, yellow hands.",
"shape": { "head": "flat", "body": "tall", "accent": "mask" },
"colors": [
{ "name": "suit black", "hex": "#1B1B1F", "role": "primary" },
{ "name": "visor ruby", "hex": "#C8102E", "role": "accent" }
],
"signature": "The visor: a red strip worn over his own glasses.",
"closet": [ "black suit", "yellow dish gloves" ],
"buy": [ { "item": "red acrylic sheet for the visor", "usd": 8, "where": "craft store" } ],
"diy": [ "Cut the visor to clip onto his glasses frame." ],
"face_hair": "None needed; keep his own glasses on.",
"reads": { "silhouette": "...", "color": "...", "detail": "..." },
"dim_light": "Add red glow tape inside the visor edge.",
"comfort": "Visor clips off for drinks."
},
{ "member": "Lena", "character": "Jean Grey", ... }
],
"lineup": [ "Lena", "Marcus" ],
"timeline": [ { "days_before": 10, "task": "Order the acrylic sheet", "who": "Marcus" }, ... ],
"warnings": [ "No wigs: Jean Grey's red hair is done with a one-night spray tint or skipped." ]
}
shape is a closed vocabulary the app draws figures from: head is round|square|point|flat, body is round|square|triangle|tall, accent is hat|cape|ears|horns|wings|staff|mask|wig|tail|none. colors[].role is primary|secondary|accent. closet lists items the person already owns; buy is the shopping list with usd and where. timeline[].days_before counts back from October 31.
declined
Returned instead of a pitch or build when a request uses a real ethnicity, nationality, religion or culture as a costume:
{
"lane": "declined",
"reason": "A real culture is not a costume.",
"alternative": "A group of fictional sky pirates keeps the bright colors and the props."
}
Handle all three shapes. A declined run is a successful job (status succeeded) and is billed like any other.
Credits
Costs are in credits: 10,000 credits = $1. The model is bound to the gpt-terra tier alias, which /estimate reports as model_alias alongside the concrete model it resolves to today. hold_credits is a reservation taken when a run starts, not the price. The real cost is charged_credits on the finished job, after settlement; the unused part of the hold is released. If the balance ran out mid-reply the job carries truncated: true and the JSON text may be cut short – check it before parsing.
1. Get a token
Every call except /guest needs Authorization: Bearer <token>. For runs, get your token from the token page: sign in there and copy the signed-in token. For the free calls only, POST /guest with {"slug":"costume-squad"} mints a guest token scoped to this app; the slug travels in that body, and no later path names the app. Guest tokens can call /me and /estimate; a run needs a signed-in token with credits. Each tab below defines the small call helper the later steps reuse.
BASE=https://api.skillsafe.ai/v1/app-api
# Free calls only (/me, /estimate): mint a guest token for this app.
# No Authorization header; the slug goes in the BODY.
curl -sS -X POST "$BASE/guest" \
-H 'Content-Type: application/json' \
-d '{"slug":"costume-squad"}'
# -> {"ok":true,"data":{"token":"...","guest_id":"..."}}
# Runs need a signed-in token with credits. Copy it from
# https://costume-squad.skillsafe.ai/tokens.html and paste it here:
TOKEN="YOUR_TOKEN"
import json, time, uuid, urllib.request, urllib.error
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # signed-in token from /tokens.html (needed for runs)
def call(method, path, payload=None, headers=None, auth=True):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Content-Type", "application/json")
if auth:
req.add_header("Authorization", "Bearer " + TOKEN)
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req) as r:
env = json.load(r)
except urllib.error.HTTPError as e: # 4xx/5xx still carry the envelope
env = json.load(e)
if not env.get("ok"):
err = env["error"]
raise RuntimeError(err["code"] + ": " + err["message"])
return env["data"]
# No signed-in token? A guest token covers the free calls (/me, /estimate).
if TOKEN == "YOUR_TOKEN":
TOKEN = call("POST", "/guest", {"slug": "costume-squad"}, auth=False)["token"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // signed-in token from /tokens.html (needed for runs)
async function call(method, path, payload, headers = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
...headers,
},
body: payload === undefined ? undefined : JSON.stringify(payload),
});
const env = await res.json();
if (!env.ok) throw new Error(`${res.status} ${env.error.code}: ${env.error.message}`);
return env.data;
}
// No signed-in token? A guest token covers the free calls (/me, /estimate).
if (TOKEN === "YOUR_TOKEN") {
TOKEN = "";
TOKEN = (await call("POST", "/guest", { slug: "costume-squad" })).token;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
// Signed-in token from /tokens.html (needed for runs), read from your shell.
var token = os.Getenv("SKILLSAFE_TOKEN")
func call(method, path string, payload any, headers map[string]string) (map[string]any, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
for k, v := range headers {
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("%d %s: %s", res.StatusCode, env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
func main() {
// No signed-in token? A guest token covers the free calls (/me, /estimate).
if token == "" {
g, err := call("POST", "/guest", map[string]any{"slug": "costume-squad"}, nil)
if err != nil {
panic(err)
}
token = g["token"].(string)
}
fmt.Println("token ready")
}
import java.net.URI;
import java.net.http.*;
import java.util.UUID;
import java.util.regex.*;
class CostumeSquad {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String token = "YOUR_TOKEN"; // signed-in token from /tokens.html (needed for runs)
// Returns the raw envelope: {"ok":true,"data":{...}} or {"ok":false,"error":{...}}.
// Extra headers go in as name/value pairs, e.g. "Idempotency-Key", key.
static String call(String method, String path, String payload, String... headers)
throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json")
.method(method, payload == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(payload));
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers.length > 0) b.headers(headers);
return HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
// Two tiny helpers so the sample needs no JSON library; use Jackson or Gson in real code.
static String field(String json, String key) {
Matcher m = Pattern.compile("\"" + key + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
return m.find() ? m.group(1) : null;
}
static String q(String raw) { // JSON-encode a string value
return "\"" + raw.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", " ") + "\"";
}
public static void main(String[] a) throws Exception {
// No signed-in token? A guest token covers the free calls (/me, /estimate).
if (token.equals("YOUR_TOKEN")) {
token = "";
token = field(call("POST", "/guest", "{\"slug\":\"costume-squad\"}"), "token");
}
System.out.println("token ready");
}
}
require "json"
require "net/http"
require "securerandom"
BASE = "https://api.skillsafe.ai/v1/app-api"
$token = "YOUR_TOKEN" # signed-in token from /tokens.html (needed for runs)
def call(method, path, payload = nil, headers = {})
uri = URI(BASE + path)
req = (method == "GET" ? Net::HTTP::Get : Net::HTTP::Post).new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}" unless $token.empty?
headers.each { |k, v| req[k] = v }
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{res.code} #{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
# No signed-in token? A guest token covers the free calls (/me, /estimate).
if $token == "YOUR_TOKEN"
$token = ""
$token = call("POST", "/guest", { slug: "costume-squad" })["token"]
end
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = "YOUR_TOKEN"; // signed-in token from /tokens.html (needed for runs)
function call($method, $path, $payload = null, $extra = []) {
global $base, $token;
$headers = array_merge(["Content-Type: application/json"], $extra);
if ($token !== "") $headers[] = "Authorization: Bearer " . $token;
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
// No signed-in token? A guest token covers the free calls (/me, /estimate).
if ($token === "YOUR_TOKEN") {
$token = "";
$token = call("POST", "/guest", ["slug" => "costume-squad"])["token"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
var token = "YOUR_TOKEN"; // signed-in token from /tokens.html (needed for runs)
var http = new HttpClient();
async Task<JsonElement> Call(HttpMethod method, string path, object? payload = null,
string? idempotencyKey = null)
{
var req = new HttpRequestMessage(method, Base + path);
if (token != "") req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (idempotencyKey != null) req.Headers.Add("Idempotency-Key", idempotencyKey);
if (payload != null)
req.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8,
"application/json");
var res = await http.SendAsync(req);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean())
{
var e = env.GetProperty("error");
throw new Exception($"{(int)res.StatusCode} {e.GetProperty("code")}: {e.GetProperty("message")}");
}
return env.GetProperty("data");
}
// No signed-in token? A guest token covers the free calls (/me, /estimate).
if (token == "YOUR_TOKEN")
{
token = "";
token = (await Call(HttpMethod.Post, "/guest", new { slug = "costume-squad" }))
.GetProperty("token").GetString()!;
}
2. Check who you are
GET /me returns subject_type (user or guest), subject_id and credits. There is no name or email. A 401 here before any token exists is the expected answer.
curl -sS "$BASE/me" -H "Authorization: Bearer $TOKEN"
# -> {"ok":true,"data":{"subject_type":"user","subject_id":"...","credits":48210}}
# call() and TOKEN come from step 1
me = call("GET", "/me")
print(me["subject_type"], me["credits"]) # "user" or "guest"; balance in credits
// call() and TOKEN come from step 1
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits); // "user" or "guest"; balance in credits
// replaces main() from step 1 (keep the guest fallback if you want it)
func main() {
me, err := call("GET", "/me", nil, nil)
if err != nil {
panic(err)
}
fmt.Println(me["subject_type"], me["credits"]) // "user" or "guest"; balance in credits
}
// inside class CostumeSquad from step 1
public static void main(String[] a) throws Exception {
// {"ok":true,"data":{"subject_type":"user","subject_id":"...","credits":48210}}
System.out.println(call("GET", "/me", null));
}
# call() and $token come from step 1
me = call("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}" # "user" or "guest"; balance in credits
// call() and $token come from step 1
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"], "\n"; // "user" or "guest"; balance in credits
// Call() and token come from step 1
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
3. Price it, free
POST /estimate takes the same body as a run and returns hold_credits, min_credits, model, model_alias (gpt-terra) and markup_bps. No charge and no job. It does not validate the body: a 200 proves your token and the model binding, not that members is encoded correctly. These samples build the pitch body that step 4 reuses.
PITCH='{"task":"pitch","members":"[{\"name\":\"Maya\",\"budget\":30,\"owns\":[\"orange cardigan\",\"chunky glasses\"],\"limits\":[],\"notes\":\"\"},{\"name\":\"Theo\",\"budget\":20,\"owns\":[\"brown cargo pants\",\"black hoodie\"],\"limits\":[\"no face paint\"],\"notes\":\"\"},{\"name\":\"Sam\",\"budget\":15,\"owns\":[\"brown fleece onesie\"],\"limits\":[\"hates masks\"],\"notes\":\"\"}]","occasion":"house party","vibe":"trending","seeds":"Scooby-Doo gang","days_left":13}'
curl -sS -X POST "$BASE/estimate" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "$PITCH"
# -> {"ok":true,"data":{"hold_credits":...,"min_credits":...,"model":"...",
# "model_alias":"gpt-terra","markup_bps":...}}
# call() comes from step 1
members = [
{"name": "Maya", "budget": 30, "owns": ["orange cardigan", "chunky glasses"], "limits": [], "notes": ""},
{"name": "Theo", "budget": 20, "owns": ["brown cargo pants", "black hoodie"], "limits": ["no face paint"], "notes": ""},
{"name": "Sam", "budget": 15, "owns": ["brown fleece onesie"], "limits": ["hates masks"], "notes": ""},
]
body = {
"task": "pitch",
"members": json.dumps(members), # a JSON-encoded STRING, not an array
"occasion": "house party",
"vibe": "trending",
"seeds": "Scooby-Doo gang",
"days_left": 13,
}
est = call("POST", "/estimate", body)
print(est["hold_credits"], est["min_credits"], est["model_alias"], est["model"])
// call() comes from step 1
const pitchMembers = [
{"name": "Maya", "budget": 30, "owns": ["orange cardigan", "chunky glasses"], "limits": [], "notes": ""},
{"name": "Theo", "budget": 20, "owns": ["brown cargo pants", "black hoodie"], "limits": ["no face paint"], "notes": ""},
{"name": "Sam", "budget": 15, "owns": ["brown fleece onesie"], "limits": ["hates masks"], "notes": ""},
];
const pitch = {
task: "pitch",
members: JSON.stringify(pitchMembers), // a JSON-encoded STRING, not an array
occasion: "house party",
vibe: "trending",
seeds: "Scooby-Doo gang",
days_left: 13,
};
const est = await call("POST", "/estimate", pitch);
console.log(est.hold_credits, est.min_credits, est.model_alias, est.model);
func pitchBody() map[string]any {
members, _ := json.Marshal([]map[string]any{
{"name": "Maya", "budget": 30, "owns": []string{"orange cardigan", "chunky glasses"}, "limits": []string{}, "notes": ""},
{"name": "Theo", "budget": 20, "owns": []string{"brown cargo pants", "black hoodie"}, "limits": []string{"no face paint"}, "notes": ""},
{"name": "Sam", "budget": 15, "owns": []string{"brown fleece onesie"}, "limits": []string{"hates masks"}, "notes": ""},
})
return map[string]any{
"task": "pitch",
"members": string(members), // a JSON-encoded STRING, not an array
"occasion": "house party",
"vibe": "trending",
"seeds": "Scooby-Doo gang",
"days_left": 13,
}
}
// replaces main(); call() comes from step 1
func main() {
est, err := call("POST", "/estimate", pitchBody(), nil)
if err != nil {
panic(err)
}
fmt.Println(est["hold_credits"], est["min_credits"], est["model_alias"], est["model"])
}
// inside class CostumeSquad from step 1
static String pitchBody() {
String members = """
[{"name":"Maya","budget":30,"owns":["orange cardigan","chunky glasses"],"limits":[],"notes":""},
{"name":"Theo","budget":20,"owns":["brown cargo pants","black hoodie"],"limits":["no face paint"],"notes":""},
{"name":"Sam","budget":15,"owns":["brown fleece onesie"],"limits":["hates masks"],"notes":""}]""";
// members (and casting) travel as JSON-encoded STRINGS: q() quotes and escapes them.
return """
{"task":"pitch","members":%s,"occasion":"house party","vibe":"trending",
"seeds":"Scooby-Doo gang","days_left":13}""".formatted(q(members));
}
public static void main(String[] a) throws Exception {
// {"ok":true,"data":{"hold_credits":...,"min_credits":...,"model_alias":"gpt-terra",...}}
System.out.println(call("POST", "/estimate", pitchBody()));
}
# call() comes from step 1
def pitch_body
members = [
{ name: "Maya", budget: 30, owns: ["orange cardigan", "chunky glasses"], limits: [], notes: "" },
{ name: "Theo", budget: 20, owns: ["brown cargo pants", "black hoodie"], limits: ["no face paint"], notes: "" },
{ name: "Sam", budget: 15, owns: ["brown fleece onesie"], limits: ["hates masks"], notes: "" },
]
{
task: "pitch",
members: members.to_json, # a JSON-encoded STRING, not an array
occasion: "house party",
vibe: "trending",
seeds: "Scooby-Doo gang",
days_left: 13,
}
end
est = call("POST", "/estimate", pitch_body)
puts est.values_at("hold_credits", "min_credits", "model_alias", "model").join(" ")
// call() comes from step 1
function pitch_body() {
$members = [
["name" => "Maya", "budget" => 30, "owns" => ["orange cardigan", "chunky glasses"], "limits" => [], "notes" => ""],
["name" => "Theo", "budget" => 20, "owns" => ["brown cargo pants", "black hoodie"], "limits" => ["no face paint"], "notes" => ""],
["name" => "Sam", "budget" => 15, "owns" => ["brown fleece onesie"], "limits" => ["hates masks"], "notes" => ""],
];
return [
"task" => "pitch",
"members" => json_encode($members), // a JSON-encoded STRING, not an array
"occasion" => "house party",
"vibe" => "trending",
"seeds" => "Scooby-Doo gang",
"days_left" => 13,
];
}
$est = call("POST", "/estimate", pitch_body());
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model_alias"], "\n";
// Call() comes from step 1
object PitchBody()
{
var members = new[]
{
new { name = "Maya", budget = 30, owns = new string[] { "orange cardigan", "chunky glasses" }, limits = new string[] { }, notes = "" },
new { name = "Theo", budget = 20, owns = new string[] { "brown cargo pants", "black hoodie" }, limits = new string[] { "no face paint" }, notes = "" },
new { name = "Sam", budget = 15, owns = new string[] { "brown fleece onesie" }, limits = new string[] { "hates masks" }, notes = "" },
};
return new
{
task = "pitch",
members = JsonSerializer.Serialize(members), // a JSON-encoded STRING, not an array
occasion = "house party",
vibe = "trending",
seeds = "Scooby-Doo gang",
days_left = 13,
};
}
var est = await Call(HttpMethod.Post, "/estimate", PitchBody());
Console.WriteLine($"hold {est.GetProperty("hold_credits")}, min {est.GetProperty("min_credits")}, " +
$"{est.GetProperty("model_alias")} -> {est.GetProperty("model")}");
4. Run it, and poll
POST /run returns {"job_id": "job_..."}. Poll GET /jobs/{job_id} until status is succeeded or failed. The finished job carries output (with the reply text in output.output), charged_credits and, if the balance cut the reply short, truncated: true.
Send an Idempotency-Key header on every run. A retry after a timeout must reuse the first attempt's key, or it starts and bills a second job.
# $PITCH comes from step 3. Make the key once; reuse it if you retry the POST.
KEY=$(uuidgen)
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $KEY" \
-d "$PITCH" | jq -r .data.job_id)
while :; do
J=$(curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN")
S=$(echo "$J" | jq -r .data.status)
[ "$S" = succeeded ] || [ "$S" = failed ] && break
sleep 2
done
echo "$J" | jq '.data | {status, charged_credits, truncated}'
echo "$J" | jq -r '.data.output.output' | jq . # the model reply is a JSON string
# call() from step 1, body from step 3
key = str(uuid.uuid4()) # reuse this exact key if you retry the POST
job = call("POST", "/run", body, headers={"Idempotency-Key": key})
while True:
j = call("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if j["status"] == "failed":
raise RuntimeError(j.get("error"))
out = j["output"]
result = json.loads(out["output"] if isinstance(out, dict) else out)
print(result["lane"], [p["theme"] for p in result.get("pitches", [])])
print("charged", j["charged_credits"], "credits; truncated:", j.get("truncated", False))
// call() from step 1, pitch from step 3
const key = crypto.randomUUID(); // reuse this exact key if you retry the POST
const { job_id } = await call("POST", "/run", pitch, { "Idempotency-Key": key });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await call("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const result = JSON.parse(typeof job.output === "string" ? job.output : job.output.output);
console.log(result.lane, (result.pitches || []).map((p) => p.theme));
console.log("charged", job.charged_credits, "truncated", Boolean(job.truncated));
// add "crypto/rand" and "time" to the imports; call() from step 1, pitchBody() from step 3
func newKey() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
func main() {
key := newKey() // reuse this exact key if you retry the POST
job, err := call("POST", "/run", pitchBody(), map[string]string{"Idempotency-Key": key})
if err != nil {
panic(err)
}
var j map[string]any
for {
time.Sleep(1500 * time.Millisecond)
if j, err = call("GET", "/jobs/"+job["job_id"].(string), nil, nil); err != nil {
panic(err)
}
if s := j["status"]; s == "succeeded" || s == "failed" {
break
}
}
out, _ := j["output"].(map[string]any)
text, _ := out["output"].(string)
var result map[string]any
json.Unmarshal([]byte(text), &result)
fmt.Println(j["status"], result["lane"], "charged:", j["charged_credits"], "truncated:", j["truncated"])
}
// inside class CostumeSquad: call(), field() from step 1, pitchBody() from step 3
public static void main(String[] a) throws Exception {
String key = UUID.randomUUID().toString(); // reuse this exact key if you retry the POST
String jobId = field(call("POST", "/run", pitchBody(), "Idempotency-Key", key), "job_id");
String job, status;
do {
Thread.sleep(1500);
job = call("GET", "/jobs/" + jobId, null);
status = field(job, "status");
} while (!"succeeded".equals(status) && !"failed".equals(status));
// data.output.output is the model reply: a JSON string holding {"lane":"pitch",...}.
// charged_credits and truncated sit beside it. Parse with Jackson or Gson.
System.out.println(job);
}
# call() from step 1, pitch_body from step 3
key = SecureRandom.uuid # reuse this exact key if you retry the POST
job = call("POST", "/run", pitch_body, { "Idempotency-Key" => key })
j = nil
loop do
sleep 1.5
j = call("GET", "/jobs/#{job['job_id']}")
break if %w[succeeded failed].include?(j["status"])
end
raise j["error"].inspect if j["status"] == "failed"
out = j["output"]
result = JSON.parse(out.is_a?(Hash) ? out["output"] : out)
puts result.fetch("pitches", []).map { |p| p["theme"] }.join(" | ")
puts "charged #{j['charged_credits']} credits, truncated: #{j['truncated'] || false}"
// call() from step 1, pitch_body() from step 3
$key = bin2hex(random_bytes(16)); // reuse this exact key if you retry the POST
$job = call("POST", "/run", pitch_body(), ["Idempotency-Key: " . $key]);
do {
usleep(1500000);
$j = call("GET", "/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed"], true));
if ($j["status"] === "failed") throw new Exception(json_encode($j["error"] ?? null));
$out = is_array($j["output"]) ? $j["output"]["output"] : $j["output"];
$result = json_decode($out, true);
foreach ($result["pitches"] ?? [] as $p) echo $p["theme"], "\n";
echo "charged ", $j["charged_credits"], " credits, truncated: ", json_encode($j["truncated"] ?? false), "\n";
// Call() from step 1, PitchBody() from step 3
var key = Guid.NewGuid().ToString(); // reuse this exact key if you retry the POST
var run = await Call(HttpMethod.Post, "/run", PitchBody(), key);
var jobId = run.GetProperty("job_id").GetString();
JsonElement job;
string status;
do
{
await Task.Delay(1500);
job = await Call(HttpMethod.Get, $"/jobs/{jobId}");
status = job.GetProperty("status").GetString()!;
} while (status != "succeeded" && status != "failed");
if (status == "failed") throw new Exception(job.GetProperty("error").ToString());
var text = job.GetProperty("output").GetProperty("output").GetString()!;
var result = JsonDocument.Parse(text).RootElement;
foreach (var p in result.GetProperty("pitches").EnumerateArray())
Console.WriteLine(p.GetProperty("theme"));
Console.WriteLine($"charged {job.GetProperty("charged_credits")} credits");
5. Or stream it
POST /run-stream takes the same body and answers with text/event-stream. Frames are separated by a blank line; the event name is on its own event: line and the JSON payload on a data: line – there is no type field inside the payload:
event: job
data: {"job_id": "job_..."}
event: delta
data: {"text": "{\"lane\":\"build\",\"theme\":..."}
event: tick
data: {}
event: done
data: {"job_id": "job_...", "status": "succeeded", "charged_credits": ..., "output": {"output": "{\"lane\":\"build\",...}"}}
Events are job (the job id), delta (a piece of reply text), tick (a keep-alive with no text; some clients receive only ticks and no deltas), done and error. Treat done as the source of truth: its output.output is the complete JSON text, so parse that rather than the concatenated deltas. These samples stream the build example.
BUILD='{"task":"build","members":"[{\"name\":\"Lena\",\"budget\":35,\"owns\":[\"white slip dress\",\"black trench coat\",\"combat boots\"],\"limits\":[\"no wigs\"],\"notes\":\"\"},{\"name\":\"Marcus\",\"budget\":35,\"owns\":[\"black suit\",\"yellow dish gloves\"],\"limits\":[\"wears glasses\"],\"notes\":\"\"}]","theme":"Cyclops & Jean Grey (X-Men)","casting":"[{\"member\":\"Lena\",\"character\":\"Jean Grey\"},{\"member\":\"Marcus\",\"character\":\"Cyclops\"}]","occasion":"bar or club","vibe":"spooky","days_left":13}'
# -N turns off buffering so frames print as they arrive
curl -N -sS -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d "$BUILD"
# TOKEN from step 1
import codecs, http.client
members = [
{"name": "Lena", "budget": 35, "owns": ["white slip dress", "black trench coat", "combat boots"], "limits": ["no wigs"], "notes": ""},
{"name": "Marcus", "budget": 35, "owns": ["black suit", "yellow dish gloves"], "limits": ["wears glasses"], "notes": ""},
]
casting = [
{"member": "Lena", "character": "Jean Grey"},
{"member": "Marcus", "character": "Cyclops"},
]
build = {
"task": "build",
"members": json.dumps(members), # a JSON-encoded STRING, not an array
"theme": "Cyclops & Jean Grey (X-Men)",
"casting": json.dumps(casting),
"occasion": "bar or club",
"vibe": "spooky",
"days_left": 13,
}
conn = http.client.HTTPSConnection("api.skillsafe.ai")
conn.request("POST", "/v1/app-api/run-stream", json.dumps(build), {
"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Accept": "text/event-stream",
})
resp = conn.getresponse()
dec = codecs.getincrementaldecoder("utf-8")()
buf, text = "", ""
while chunk := resp.read1(4096):
buf += dec.decode(chunk).replace("\r\n", "\n")
while "\n\n" in buf: # frames end with a blank line
frame, buf = buf.split("\n\n", 1)
name, data = "message", ""
for line in frame.split("\n"):
if line.startswith("event:"):
name = line[6:].strip()
elif line.startswith("data:"):
data += line[5:].strip()
if not data:
continue
d = json.loads(data)
if name == "delta":
text += d.get("text", "") # partial reply text
elif name == "error":
raise RuntimeError(d)
elif name == "done":
out = d["output"]
plan = json.loads(out["output"] if isinstance(out, dict) else out)
for m in plan.get("members", []):
print(m["member"], "->", m["character"], [c["hex"] for c in m["colors"]])
print("charged", d.get("charged_credits"), "truncated", d.get("truncated", False))
// BASE and TOKEN from step 1
const buildMembers = [
{"name": "Lena", "budget": 35, "owns": ["white slip dress", "black trench coat", "combat boots"], "limits": ["no wigs"], "notes": ""},
{"name": "Marcus", "budget": 35, "owns": ["black suit", "yellow dish gloves"], "limits": ["wears glasses"], "notes": ""},
];
const buildCasting = [
{"member": "Lena", "character": "Jean Grey"},
{"member": "Marcus", "character": "Cyclops"},
];
const build = {
task: "build",
members: JSON.stringify(buildMembers), // a JSON-encoded STRING, not an array
theme: "Cyclops & Jean Grey (X-Men)",
casting: JSON.stringify(buildCasting),
occasion: "bar or club",
vibe: "spooky",
days_left: 13,
};
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify(build),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true }).replace(/\r\n/g, "\n");
let i;
while ((i = buf.indexOf("\n\n")) >= 0) { // frames end with a blank line
const frame = buf.slice(0, i);
buf = buf.slice(i + 2);
let name = "message", data = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) name = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (!data) continue;
const d = JSON.parse(data);
if (name === "delta") text += d.text || "";
else if (name === "error") throw new Error(JSON.stringify(d));
else if (name === "done") {
const plan = JSON.parse(typeof d.output === "string" ? d.output : d.output.output);
for (const m of plan.members || []) console.log(m.member, "->", m.character, m.colors.map((c) => c.hex));
console.log("charged", d.charged_credits, "truncated", Boolean(d.truncated));
}
}
}
// add "bufio" and "strings" to the imports; base and token from step 1
func buildBody() map[string]any {
members, _ := json.Marshal([]map[string]any{
{"name": "Lena", "budget": 35, "owns": []string{"white slip dress", "black trench coat", "combat boots"}, "limits": []string{"no wigs"}, "notes": ""},
{"name": "Marcus", "budget": 35, "owns": []string{"black suit", "yellow dish gloves"}, "limits": []string{"wears glasses"}, "notes": ""},
})
casting, _ := json.Marshal([]map[string]any{
{"member": "Lena", "character": "Jean Grey"},
{"member": "Marcus", "character": "Cyclops"},
})
return map[string]any{
"task": "build",
"members": string(members), // a JSON-encoded STRING, not an array
"theme": "Cyclops & Jean Grey (X-Men)",
"casting": string(casting),
"occasion": "bar or club",
"vibe": "spooky",
"days_left": 13,
}
}
func main() {
b, _ := json.Marshal(buildBody())
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 64*1024), 4*1024*1024) // a done frame can be large
name, data := "message", ""
for sc.Scan() {
line := strings.TrimRight(sc.Text(), "\r")
switch {
case strings.HasPrefix(line, "event:"):
name = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
data += strings.TrimSpace(line[5:])
case line == "" && data != "": // blank line ends the frame
var d map[string]any
json.Unmarshal([]byte(data), &d)
switch name {
case "delta":
fmt.Print(d["text"])
case "error":
panic(fmt.Sprint(d))
case "done":
out, _ := d["output"].(map[string]any)
text, _ := out["output"].(string)
var plan map[string]any
json.Unmarshal([]byte(text), &plan)
fmt.Println("\nlane:", plan["lane"], "charged:", d["charged_credits"])
}
name, data = "message", ""
}
}
}
// inside class CostumeSquad: HTTP, token, q() from step 1
static String buildBody() {
String members = """
[{"name":"Lena","budget":35,"owns":["white slip dress","black trench coat","combat boots"],"limits":["no wigs"],"notes":""},
{"name":"Marcus","budget":35,"owns":["black suit","yellow dish gloves"],"limits":["wears glasses"],"notes":""}]""";
String casting = """
[{"member":"Lena","character":"Jean Grey"},
{"member":"Marcus","character":"Cyclops"}]""";
// members (and casting) travel as JSON-encoded STRINGS: q() quotes and escapes them.
return """
{"task":"build","members":%s,"theme":"Cyclops & Jean Grey (X-Men)",
"casting":%s,"occasion":"bar or club","vibe":"spooky","days_left":13}""".formatted(q(members), q(casting));
}
public static void main(String[] a) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(buildBody()))
.build();
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();
String[] name = {"message"};
var data = new StringBuilder();
lines.forEach(line -> {
if (line.startsWith("event:")) name[0] = line.substring(6).trim();
else if (line.startsWith("data:")) data.append(line.substring(5).trim());
else if (line.isEmpty() && data.length() > 0) { // blank line ends the frame
switch (name[0]) {
case "done" -> System.out.println("\n" + data); // data.output.output = plan JSON
case "error" -> System.err.println(data);
default -> System.out.print("."); // job, delta, tick
}
name[0] = "message";
data.setLength(0);
}
});
}
# BASE and $token from step 1
def build_body
members = [
{ name: "Lena", budget: 35, owns: ["white slip dress", "black trench coat", "combat boots"], limits: ["no wigs"], notes: "" },
{ name: "Marcus", budget: 35, owns: ["black suit", "yellow dish gloves"], limits: ["wears glasses"], notes: "" },
]
casting = [
{ member: "Lena", character: "Jean Grey" },
{ member: "Marcus", character: "Cyclops" },
]
{
task: "build",
members: members.to_json, # a JSON-encoded STRING, not an array
theme: "Cyclops & Jean Grey (X-Men)",
casting: casting.to_json,
occasion: "bar or club",
vibe: "spooky",
days_left: 13,
}
end
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{$token}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req.body = JSON.dump(build_body)
buf = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
buf << chunk.dup.force_encoding("UTF-8").delete("\r")
while (i = buf.index("\n\n")) # frames end with a blank line
frame = buf.slice!(0, i + 2)
name = frame[/^event:\s*(.+)$/, 1] || "message"
data = frame.scan(/^data:\s?(.*)$/).flatten.join
next if data.empty?
d = JSON.parse(data)
case name
when "delta" then print d["text"]
when "error" then raise d.inspect
when "done"
out = d["output"]
plan = JSON.parse(out.is_a?(Hash) ? out["output"] : out)
plan.fetch("members", []).each { |m| puts "\n#{m['member']} -> #{m['character']}" }
puts "charged #{d['charged_credits']}"
end
end
end
end
end
// $base and $token from step 1
function build_body() {
$members = [
["name" => "Lena", "budget" => 35, "owns" => ["white slip dress", "black trench coat", "combat boots"], "limits" => ["no wigs"], "notes" => ""],
["name" => "Marcus", "budget" => 35, "owns" => ["black suit", "yellow dish gloves"], "limits" => ["wears glasses"], "notes" => ""],
];
$casting = [
["member" => "Lena", "character" => "Jean Grey"],
["member" => "Marcus", "character" => "Cyclops"],
];
return [
"task" => "build",
"members" => json_encode($members), // a JSON-encoded STRING, not an array
"theme" => "Cyclops & Jean Grey (X-Men)",
"casting" => json_encode($casting),
"occasion" => "bar or club",
"vibe" => "spooky",
"days_left" => 13,
];
}
$buf = "";
$ch = curl_init($base . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(build_body()),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $token,
"Content-Type: application/json",
"Accept: text/event-stream",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buf) {
$buf .= str_replace("\r", "", $chunk);
while (($i = strpos($buf, "\n\n")) !== false) { // frames end with a blank line
$frame = substr($buf, 0, $i);
$buf = substr($buf, $i + 2);
$name = "message";
$data = "";
foreach (explode("\n", $frame) as $line) {
if (str_starts_with($line, "event:")) $name = trim(substr($line, 6));
elseif (str_starts_with($line, "data:")) $data .= trim(substr($line, 5));
}
if ($data === "") continue;
$d = json_decode($data, true);
if ($name === "delta") echo $d["text"] ?? "";
elseif ($name === "error") throw new Exception($data);
elseif ($name === "done") {
$out = is_array($d["output"]) ? $d["output"]["output"] : $d["output"];
$plan = json_decode($out, true);
foreach ($plan["members"] ?? [] as $m) echo "\n", $m["member"], " -> ", $m["character"];
echo "\ncharged ", $d["charged_credits"] ?? "?", "\n";
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
// Base, token and http from step 1
object BuildBody()
{
var members = new[]
{
new { name = "Lena", budget = 35, owns = new string[] { "white slip dress", "black trench coat", "combat boots" }, limits = new string[] { "no wigs" }, notes = "" },
new { name = "Marcus", budget = 35, owns = new string[] { "black suit", "yellow dish gloves" }, limits = new string[] { "wears glasses" }, notes = "" },
};
var casting = new[]
{
new { member = "Lena", character = "Jean Grey" },
new { member = "Marcus", character = "Cyclops" },
};
return new
{
task = "build",
members = JsonSerializer.Serialize(members), // a JSON-encoded STRING, not an array
theme = "Cyclops & Jean Grey (X-Men)",
casting = JsonSerializer.Serialize(casting),
occasion = "bar or club",
vibe = "spooky",
days_left = 13,
};
}
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream")
{
Content = new StringContent(JsonSerializer.Serialize(BuildBody()), Encoding.UTF8,
"application/json"),
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string name = "message", data = "";
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) name = line[6..].Trim();
else if (line.StartsWith("data:")) data += line[5..].Trim();
else if (line == "" && data != "") // blank line ends the frame
{
var d = JsonDocument.Parse(data).RootElement;
if (name == "delta" && d.TryGetProperty("text", out var t)) Console.Write(t.GetString());
else if (name == "error") throw new Exception(data);
else if (name == "done")
{
var text = d.GetProperty("output").GetProperty("output").GetString()!;
var plan = JsonDocument.Parse(text).RootElement;
foreach (var m in plan.GetProperty("members").EnumerateArray())
Console.WriteLine($"{m.GetProperty("member")} -> {m.GetProperty("character")}");
}
name = "message";
data = "";
}
}
Errors
A failure is { "ok": false, "error": { "code", "message" } } with a matching HTTP status. Branch on the status first and error.code second.
| HTTP | Code | What to do |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is malformed. Check that members and casting are strings holding JSON, not raw arrays. |
| 401 | UNAUTHORIZED | No token, or the app session expired. Mint a new one (guest) or copy a fresh one from the token page. |
| 402 | INSUFFICIENT_CREDITS | Balance below min_credits. Compare /estimate with /me, or top up. Guest tokens hit this on any run. |
| 404 | – | Wrong path or an unknown job_id. Paths sit directly under /v1/app-api; there is no /apps/{slug}/ segment. |
| 409 | – | Idempotency conflict: the same Idempotency-Key was sent with a different body, or while the first request is still in flight. Reuse a key only for a byte-identical retry. |
| 429 | RATE_LIMITED | Back off and retry with the same idempotency key. Do not tight-loop. |
| 5xx | – | Platform-side failure. Retry a run with the same key so it cannot bill twice. |
A job can also end with status: "failed" inside an ok: true envelope; read error on the job.