← Swing Tags
Swing Tags API
Tokens

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": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task / items is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the listing writer is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. 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

FieldTypeNotes
task, todaystringwrite; YYYY-MM-DD.
itemsstring (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.
sellingstring (JSON){listing_style, fee_percent, shipping_seller_pays}; style is a resale app, an online marketplace or a local listing.
page_bundlesstring (JSON)[{ids, same_size, combined_value, bundle_price}] - may be empty.
page_flagsstring (JSON)[{level, text}] with level warn / info.
notesstringThe seller's words about the whole rail (may be empty; past 1,200 characters cut on whole lines with a marker).
retry_notestringOnly 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"}
}

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')

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}' 

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}' 

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}' 

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