Drive Swing Tags from your own code
The listing writer is available over HTTP: send the pieces on a resale rail - what each one is, its size, condition, measurements, flaws and price anchor, with the page's own reading of it - and get back, for every piece, a list / add-info / bundle / donate verdict, a title, a description, keywords, a price note and the checks to do before posting, plus bundle listings and a short summary. A natural use is an inventory sheet or a closet-cataloguing app: your code sends the rows, and gets back listings it can paste.
The division of labour first: the page reads and prices; the model writes; the page re-checks the reply against the tags. Category detection, measurement parsing, the buyer's checklist, the price from comps (or a rough guess from what was paid), take-home, the lean per piece, bundle groups and flags come from tagkit.js; the run input and the reply check come from tagreply.js. Both run in node as well as in the browser. check holds each reply to the rail: every piece answered once and none invented, no "list" for a piece still missing facts, questions behind every add-info, no condition oversold, every typed flaw disclosed and never denied, no measurement or dollar figure the page does not have, titles that fit, and no internal field names in prose. Prices are estimates from the seller's own comps.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"swing-tags"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task / items is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the listing writer is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field
Every run sends task: "write" - the one task. Verdicts per piece: list (ready), add-info (something a buyer will ask for is missing - the listing is drafted with bracketed placeholders), bundle (worth too little alone; sold with others), donate (a worn piece worth too little to list). The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (TagKit.toWire does this). The schema requires task and items.
Input fields
| Field | Type | Notes |
|---|---|---|
task, today | string | write; YYYY-MM-DD. |
items | string (JSON) | Up to 8 pieces: {id, item, brand, size, category, condition, measurements, flaws, material, color, notes, price, still_missing, worth_adding, page_verdict}. price is {basis, sold_comps?, median?, paid?, list_price?, lowest_to_accept?, take_home_at_list?}; "not given" marks a blank field. |
selling | string (JSON) | {listing_style, fee_percent, shipping_seller_pays}; style is a resale app, an online marketplace or a local listing. |
page_bundles | string (JSON) | [{ids, same_size, combined_value, bundle_price}] - may be empty. |
page_flags | string (JSON) | [{level, text}] with level warn / info. |
notes | string | The seller's words about the whole rail (may be empty; past 1,200 characters cut on whole lines with a marker). |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object: summary, items [{id, verdict, title, description, keywords, price_note, ask}] (one per input piece; a donated piece has an empty title and description), bundles [{ids, title, description}] and tips. Parse it with TagKit.parseReply, then normalize(reply), then check(reply, state), which returns {notes}: the page's notes (info, warn) shown under "Checked by the page".
Worked examples
Replies produced against the app's prompt for the sample rails (made-up pieces, brands and comps; lists shortened; the full replies ship in example-result.js). Together they cover every verdict. The page replays each of them free from its sample buttons.
fall closet clear-out - list, add-info and a size-S bundle
Input:
{
"task": "write",
"today": "2026-10-20",
"items": "[{\"id\":\"t1\",\"item\":\"Long wool-blend coat\",\"brand\":\"Quillmere\",\"size\":\"M\",\"category\":\"Outerwear\",\"condition\":\"Like new\",\"measurements\":{\"pit to pit\":\"21.5 in\",\"length\":\"41 in\",\"sleeve\":\"24.5 in\"},\"flaws\":\"none\",\"material\":\"70% wool, 30% polyamide\",\"color\":\"camel\",\"notes\":\"Worn maybe five times. Two front pockets, fully lined.\",\"price\":{\"basis\":\"the seller's sold comps\",\"sold_comps\":[\"$95\",\"$110\",\"$120\"],\"median\":\"$110\",\"list_price\":\"$121\",\"lowest_to_accept\":\"$94\",\"take_home_at_list\":\"$109\"},\"still_missing\":[],\"worth_adding\":[],\"page_verdict\":\"list\"},\"... 3 more\"]",
"selling": "{\"listing_style\":\"Resale app - short, casual, keyword tags\",\"fee_percent\":10,\"shipping_seller_pays\":\"not given\"}",
"page_bundles": "[{\"ids\":[\"t3\",\"t4\"],\"same_size\":\"S\",\"combined_value\":\"$13\",\"bundle_price\":\"$14\"}]",
"page_flags": "[{\"level\":\"info\",\"text\":\"Piece 3 (Ribbed tank top): no sold comps, so the price is a rough guess from what you paid (30% for good - light wear) - check two or three sold listings.\"},\"... 2 more\"]",
"notes": "Clearing out before winter. I'd rather sell fast than hold out for top dollar."
}
Reply:
{
"task": "write",
"summary": "1 of the 4 pieces (the coat) is ready to list now; the jeans are blocked until the inseam is measured, and the tank top and crewneck tee are set to sell together as a bundle. Measure the jeans' inseam first so every piece is ready to post.",
"items": [
{
"id": "t1",
"verdict": "list",
"title": "Quillmere Long Wool-Blend Coat M - Like New",
"description": "Quillmere long wool-blend coat, size M, camel.\nLike new - worn maybe five times. Two front pockets, fully lined. No flaws noted.\n70% wool, 30% polyamide.\nMeasurements laid flat: pit to pit 21.5 in, length 41 in, sleeve 24.5 in.",
"keywords": [
"Quillmere",
"wool-blend coat",
"long coat",
"camel",
"size M",
"lined",
"pockets"
],
"price_note": "List price is $121, lowest offer to accept $94, and take-home about $109 after fees - based on sold comps of $95, $110 and $120.",
"ask": [
"Photograph the lining and front pockets since the listing calls them out."
]
},
{
"id": "t2",
"verdict": "add-info",
"title": "Brackwold High-Rise Straight Jeans 28 - Good",
"description": "Brackwold high-rise straight jeans, size 28, mid blue.\nGood - light wear, with light fading at the knees.\n100% cotton denim.\nMeasurements laid flat: waist 14 in, rise 11 in, inseam [inseam: measure flat, crotch to hem].",
"keywords": [
"Brackwold",
"high-rise",
"straight jeans",
"mid blue",
"size 28",
"cotton denim"
],
"price_note": "List price is $42, lowest offer to accept $32, and take-home about $38 after fees - based on sold comps of $32, $38 and $40, once the inseam is added.",
"ask": [
"Measure the inseam laid flat."
]
},
{
"id": "t3",
"verdict": "bundle",
"title": "Ribbed Tank Top, Size S - Good",
"description": "Ribbed tank top, size S, black.\nGood - light wear. No flaws noted.\nCotton.\nMeasurements laid flat: pit to pit 15 in, length 22 in.",
"keywords": [
"ribbed tank top",
"black tank",
"size S",
"cotton tank",
"tank top"
],
"price_note": "This one sells as part of the 2-piece bundle with the crewneck tee, priced at $14 for the pair.",
"ask": [
"Check a couple of sold listings for a ribbed tank top like this before pricing on your own."
]
},
"... 1 more"
],
"bundles": [
{
"ids": [
"t3",
"t4"
],
"title": "Ribbed Tank Top & Crewneck Tee Bundle, Size S",
"description": "Ribbed tank top, size S, black, good - light wear, no flaws noted, pit to pit 15 in, length 22 in.\nCrewneck tee, size S, heather grey, good - light wear, no flaws noted, pit to pit 17 in, length 24 in.\nBoth for $14."
}
],
"tips": [
"Since you said you'd rather sell fast than hold out for top dollar, consider taking offers near the lowest price you set on each piece rather than waiting for full list price.",
"Photograph the details you noted - the coat's lining and two front pockets, and the jeans' fading at the knees - so each listing matches what's shown.",
"... 1 more"
]
}
moving-out pile, local pickup - list, add-info and donate
Input:
{
"task": "write",
"today": "2026-10-20",
"items": "[{\"id\":\"t1\",\"item\":\"Leather ankle boots\",\"brand\":\"Tarnvale\",\"size\":\"8\",\"category\":\"Shoes\",\"condition\":\"Good - light wear\",\"measurements\":{\"insole\":\"10 in\"},\"flaws\":\"scuff on the left toe, heel tips worn\",\"material\":\"leather\",\"color\":\"dark brown\",\"notes\":\"\",\"price\":{\"basis\":\"the seller's sold comps\",\"sold_comps\":[\"$45\",\"$55\",\"$60\"],\"median\":\"$55\",\"list_price\":\"$61\",\"lowest_to_accept\":\"$47\"},\"still_missing\":[],\"worth_adding\":[],\"page_verdict\":\"list\"},\"... 3 more\"]",
"selling": "{\"listing_style\":\"Local listing - pickup, short and plain\",\"fee_percent\":\"not given\",\"shipping_seller_pays\":\"not given\"}",
"page_bundles": "[]",
"page_flags": "[{\"level\":\"warn\",\"text\":\"1 piece still missing something a buyer will ask for.\"}]",
"notes": "Moving at the end of the month, so anything left goes to the donation bin."
}
Reply:
{
"task": "write",
"summary": "Two pieces - the boots and the tote - are ready to list as is. The hoodie needs its flaws named and located before it can go up, and the band tee isn't worth listing on its own given its comps. Start by checking the hoodie for any marks or tears so its listing can be finished.",
"items": [
{
"id": "t1",
"verdict": "list",
"title": "Tarnvale Leather Ankle Boots, sz 8, Good",
"description": "Tarnvale leather ankle boots, size 8, dark brown. Condition: good, light wear.\nInsole measures 10 in. Scuff on the left toe and worn heel tips.\nPickup only - message me with a time.",
"keywords": [
"Tarnvale",
"leather ankle boots",
"dark brown",
"size 8"
],
"price_note": "Priced from sold comps: list at $61, take offers down to $47.",
"ask": [
"Photograph the toe scuff and heel tips clearly."
]
},
{
"id": "t2",
"verdict": "add-info",
"title": "Fleece Zip Hoodie, sz L",
"description": "Navy polyester fleece zip hoodie, size L. Condition: fair, visible wear.\nPit to pit 23 in, length 27 in. [flaws: name and locate them]\nPickup only - message me with a time.",
"keywords": [
"fleece zip hoodie",
"navy",
"size L",
"polyester fleece"
],
"price_note": "Priced from sold comps: list at $20, take offers down to $15.",
"ask": [
"Name and locate the flaws before listing."
]
},
{
"id": "t3",
"verdict": "donate",
"title": "",
"description": "",
"keywords": [],
"price_note": "Sold comps run $5-$7 (median $6), too little to be worth listing alone; it also has yellowed underarms and a cracked print.",
"ask": []
},
"... 1 more"
],
"bundles": [],
"tips": [
"You're moving end of month, so list soon - anything unsold goes to the donation bin.",
"Your list prices leave room above the lowest offer you'd take, so there's space to negotiate at pickup.",
"... 1 more"
]
}
one dress, new with tags, with a fee and shipping - list
Input:
{
"task": "write",
"today": "2026-10-20",
"items": "[{\"id\":\"t1\",\"item\":\"Wrap midi dress\",\"brand\":\"Wrenmoor\",\"size\":\"10\",\"category\":\"Dress\",\"condition\":\"New with tags\",\"measurements\":{\"pit to pit\":\"18 in\",\"waist\":\"14 in\",\"length\":\"47 in\"},\"flaws\":\"none\",\"material\":\"100% polyester crepe\",\"color\":\"green floral\",\"notes\":\"Tie waist, flutter sleeves.\",\"price\":{\"basis\":\"the seller's sold comps\",\"sold_comps\":[\"$40\",\"$48\",\"$55\"],\"median\":\"$48\",\"paid\":\"$89\",\"list_price\":\"$53\",\"lowest_to_accept\":\"$41\",\"take_home_at_list\":\"$39\"},\"still_missing\":[],\"worth_adding\":[],\"page_verdict\":\"list\"}]",
"selling": "{\"listing_style\":\"Online marketplace - detailed, keyword-first title\",\"fee_percent\":13,\"shipping_seller_pays\":\"$7\"}",
"page_bundles": "[]",
"page_flags": "[]",
"notes": "Bought for a wedding, then wore something else. Return window closed."
}
Reply:
{
"task": "write",
"summary": "1 of 1 piece on this rail is ready to list - nothing is missing. First, photograph the tags to support the new-with-tags condition, then post at the $53 list price with $41 as your floor.",
"items": [
{
"id": "t1",
"verdict": "list",
"title": "Wrenmoor Green Floral Wrap Midi Dress, Size 10, New with Tags",
"description": "Wrenmoor wrap midi dress in green floral, size 10.\n100% polyester crepe, tie waist, flutter sleeves.\nMeasurements laid flat: pit to pit 18 in, waist 14 in, length 47 in.\nCondition: New with tags, no flaws noted.\nBought for a wedding, then wore something else - the return window had already closed.",
"keywords": [
"Wrenmoor",
"wrap midi dress",
"green floral",
"size 10",
"polyester crepe",
"tie waist",
"flutter sleeves",
"dress"
],
"price_note": "List price is $53, with $41 as the lowest offer to accept, leaving a take-home of about $39 based on sold comps of $40, $48 and $55.",
"ask": [
"Photograph the tags to back up the new-with-tags condition."
]
}
],
"bundles": [],
"tips": [
"Photograph the tags clearly since the dress is new with tags.",
"You have room to negotiate between the $53 list price and the $41 lowest offer.",
"... 1 more"
]
}
Build the input yourself
Load both scripts in node (global.window = {}; require("./tagkit.js"); require("./tagreply.js")), then:
const K = TagKit;
const st = K.state({ sell: { style: "local", fee: "", ship: "" }, notes: "", items: [
{ what: "Leather ankle boots", size: "8", condition: "good", measurements: "insole 10 in", flaws: "scuff on the left toe", comps: "45, 55, 60" },
{ what: "Fleece zip hoodie", size: "L", condition: "fair", measurements: "pit to pit 23 in, length 27 in", comps: "15, 18, 20" } ] });
console.log(st.items.map(it => [it.id, it.category, it.verdict, it.missing.map(m => m.text), it.price.list]), st.bundles, st.flags);
const wire = K.toWire(K.buildInput(st, { today: "2026-10-20" }).input); // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, st);
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://swing-tags.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://swing-tags.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// swing-tags.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://swing-tags.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://swing-tags.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
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 { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// SwingTags.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class SwingTags {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://swing-tags.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://swing-tags.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://swing-tags.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://swing-tags.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"swing-tags"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "swing-tags"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "swing-tags" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "swing-tags"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "swing-tags"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "swing-tags" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "swing-tags"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "swing-tags" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "write", "today": "2026-10-20", "items": "[...]", "selling": "{...}", "page_bundles": "[...]", "page_flags": "[...]", "notes": "Moving at the end of the month ..."}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "write", "today": "2026-10-20", "items": "[...]", "selling": "{...}", "page_bundles": "[...]", "page_flags": "[...]", "notes": "Moving at the end of the month ..."}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "write", today, items, selling, page_bundles, page_flags, notes (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses swing-tags:write:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="swing-tags:write:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "swing-tags:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `swing-tags:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "swing-tags:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "swing-tags:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "swing-tags:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "swing-tags:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"swing-tags:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: swing-tags:write:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
payload = json.load(open("input.json")) # the same run input as input.json: {"task": "write", "today": "2026-10-20", "items": "[...]", "selling": "{...}", "page_bundles": "[...]", "page_flags": "[...]", "notes": "Moving at the end of the month ..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "swing-tags:write:" + hashlib.sha256(json.dumps(payload).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const payload = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(payload), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `swing-tags:write:${createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "swing-tags:write:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String payload = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(payload.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "swing-tags:write:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(payload)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
payload = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "swing-tags:write:#{Digest::SHA256.hexdigest(payload)[0, 16]}:a1")
req.body = payload
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$payload = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: swing-tags:write:" . substr(hash("sha256", $payload), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var payload = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(payload));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"swing-tags:write:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);