← Woven to Order
API

Drive Woven to Order from your own code

The one metered task, weave-check, is available over HTTP. It takes an order slip for a companion woven to order from willow - 3 to 10 strands, one trait each, every strand marked must, want or nice - plus the page's own counts. It returns up to 4 clashes between pairs of strands (each direct or tension, with a reason and one change that could give), a short maker's note and a verdict (weave-it, reweave-first or cannot-weave).

The division of labour first: the model names the clashes; a fixed rule turns those clashes into the verdict; the page then re-checks that the verdict follows from the clashes listed, that every clash points at a real pair of strands, the order, the lengths, and that no internal id leaks into the prose. Build strands and page_read the same way (see "Build the input yourself") so the lines match.

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":"woven-to-order"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, count, strands, page_read) 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: a check 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, the strands and the verdicts

Every run sends task: weave-check (the one task is weave-check); a missing or different one is still answered as weave-check. A direct clash (shown as "snaps") means both strands cannot be true as written; a tension ("pulls") means the words of both claim the same time, money or attention. The verdict, first match wins: cannot-weave when a direct clash joins 2 must strands; reweave-first for any other direct clash or a tension touching a must; otherwise weave-it.

Input fields (all strings)

FieldRequiredWhat it holds
taskyesAlways weave-check.
countyesHow many strands (3 to 10), as digits.
strandsyesOne line per strand, numbered in slip order: 1: always up for a last-minute trip | must. Each trait at most 70 characters.
page_readyesThe page's counts on one line: strands, must-have, would-like, nice-bonus, and any strands that repeat each other (repeated: 2 and 5 or none).
retry_notenoSent only by the page's single reformat retry.

The reply

The job's output.output is one JSON object:

{"task": "weave-check", "verdict": "cannot-weave", "headline": "...",
 "clashes": [{"a": "1", "b": "4", "kind": "direct", "why": "...", "give": "..."}],
 "maker_note": "...", "closing": "..."}
FieldWhat it holds
verdictweave-it, reweave-first or cannot-weave, following from the clashes by the rule above.
headlineThe verdict in plain words and the strands it turns on, at most 16 words.
clashes0 to 4 pairs; a is the lower strand number and b the higher, as digits. Direct clashes first, then tensions, each by strand order. why at most 20 words; give at most 25 words, changing exactly 1 of the 2 strands (move it down a weight, or drop an absolute word such as "always" or "within minutes").
maker_noteThe basket-maker's note to the person who placed the order, at most 40 words.
closingOne sentence restating the verdict, at most 20 words.

The page then checks the reply (WeaveKit.check) and lists any disagreement under "The page's check".

Worked examples

These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every verdict - the last-minute traveller (cannot weave as ordered: a trip on a whim against the same routine every evening, both must-haves), the weekend hiker (reweave first: pulls that touch must-haves) and soup and kindness (weave it: the only pull is between lighter strands, or none).

Last-minute traveller - cannot weave as ordered

Input:

{
 "task": "weave-check",
 "count": "5",
 "strands": "1: always up for a last-minute trip | must\n2: tells me the truth even when it stings | must\n3: laughs at my terrible jokes | want\n4: keeps the same quiet routine every evening | must\n5: good with houseplants | nice",
 "page_read": "strands: 5 | must-have: 3 | would-like: 1 | nice-bonus: 1 | repeated: none"
}

Reply:

{
 "task": "weave-check",
 "verdict": "cannot-weave",
 "headline": "Can't weave: \"always up for a last-minute trip\" vs \"keeps the same quiet routine every evening\".",
 "clashes": [
  {
   "a": "1",
   "b": "4",
   "kind": "direct",
   "why": "Always up for a last-minute trip can't also keep the same quiet routine every evening.",
   "give": "Move \"keeps the same quiet routine every evening\" from must to want."
  }
 ],
 "maker_note": "You want always up for a last-minute trip and the same quiet routine every evening, but willow won't bend to hold these without something giving.",
 "closing": "As written, this order can't be woven until something here gives."
}

Weekend hiker - reweave first

Input:

{
 "task": "weave-check",
 "count": "5",
 "strands": "1: loves long hikes on weekends | must\n2: happy to stay in and watch films on weekends | want\n3: replies to messages within minutes | want\n4: never on their phone at dinner | must\n5: kind to waiters | must",
 "page_read": "strands: 5 | must-have: 3 | would-like: 2 | nice-bonus: 0 | repeated: none"
}

Reply:

{
 "task": "weave-check",
 "verdict": "reweave-first",
 "headline": "This weave needs reworking before replies within minutes can match never on the phone at dinner.",
 "clashes": [
  {
   "a": "3",
   "b": "4",
   "kind": "direct",
   "why": "Replying to messages within minutes cannot happen at dinner if they are never on their phone then.",
   "give": "Drop \"within minutes\" from \"replies to messages within minutes,\" leaving \"replies to messages.\""
  },
  {
   "a": "1",
   "b": "2",
   "kind": "tension",
   "why": "Long hikes and staying in with films each want the weekend, leaving less day for the other to use.",
   "give": "Move \"happy to stay in and watch films on weekends\" down from want to nice."
  }
 ],
 "maker_note": "Friend, before I touch the willow: you ask for replies to messages within minutes, yet also want never on their phone at dinner. Something there needs to loosen.",
 "closing": "So this weave needs some reworking before the order can go ahead."
}

Soup and kindness - weave it

Input:

{
 "task": "weave-check",
 "count": "6",
 "strands": "1: kind to strangers | must\n2: makes a good pot of soup | want\n3: saves up for big trips | want\n4: treats friends to dinner often | nice\n5: laughs easily | want\n6: reads before bed | nice",
 "page_read": "strands: 6 | must-have: 1 | would-like: 3 | nice-bonus: 2 | repeated: none"
}

Reply:

{
 "task": "weave-check",
 "verdict": "weave-it",
 "headline": "You can weave as asked: saves up for big trips, treats friends to dinner often.",
 "clashes": [
  {
   "a": "3",
   "b": "4",
   "kind": "tension",
   "why": "Saves up for big trips and treats friends to dinner often pull on the same money.",
   "give": "Drop \"often\" so \"treats friends to dinner often\" reads \"treats friends to dinner.\""
  }
 ],
 "maker_note": "Kind to strangers is the willow this whole frame rests on: everything else bends around it. Saving for big trips and treating friends to dinner will just have to share the money.",
 "closing": "This order can be woven just as you wrote it."
}

Build the input yourself

Load the engine in node (global.window = global; require("./weavekit.js")), then:

const K = WeaveKit;
const built = K.buildInput({ strands: [
  { text: "early riser", w: "must" },
  { text: "night owl", w: "must" },
  { text: "loves board games", w: "nice" }] });
built.input.strands;                     // "1: early riser | must
2: night owl | must
3: loves board games | nice"
const wire = K.toWire(built.input);      // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.verdictFrom(built.parsed, reply.clashes)  // the verdict the page's rule gives for those clashes
// K.check(reply, built).notes                 // the page's re-check of the reply
// K.md(built, reply)                          // the order as Markdown

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://woven-to-order.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":"woven-to-order"}' | 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": "weave-check", "count": "5", "strands": "1: always up for a last-minute trip | must\n2: ...", "page_read": "strands: 5 | must-have: 3 | ...
2. ..."} - see the field table
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 lane and the input: a retry with the same key never bills twice. The page uses woven-to-order:<task>:<inputHash>:a<attempt> (the one task is weave-check), 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="woven-to-order:weave-check:$(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: woven-to-order:weave-check:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json