← Send It Back
API

Drive Send It Back from your own code

The one metered task, sort, is available over HTTP. It takes 1 to 6 parcels as the page's pile lines - what each thing is, the job it was bought to do, how it went, what is off, a job score from 1 to 5, the days left to send it back and the page's call - plus the totals, the verdict and the parcel to send first. It returns, for every parcel, the job it was hired for, how it is doing that job and a next step, then a headline, the send-first step and a closing.

The division of labour first: a fixed rule on the page makes every call (keep, return, exchange, too-late), picks the parcel to send first and decides the verdict; the model only writes the words. After a run the page checks that the reply copied every one of those and kept to the house rules.

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

The token is minted for this app (the guest call takes {"slug":"send-it-back"}), 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, pile) 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 written sort is metered and needs a personal sign-in.
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 input and the verdicts

Every run sends task: sort (the one task is sort); a missing or different one is still answered as sort. Each parcel's call, first match wins: days left below 0 - too-late; wrong size, colour or model - exchange; faulty or damaged, not really needed, or a job score of 1 or 2 - return; otherwise - keep. The parcel to send first is the return or exchange parcel with the fewest days left, the earliest on a tie. The verdict: no parcel going back - nothing-to-send; the send-first parcel has 3 days left or fewer - post-today; otherwise - post-soon.

FieldRequiredWhat it holds
taskyesAlways sort.
pileyesThe page's lines: a house rules: line, a totals: line (parcels, counts, verdict, send first), then 1 line per parcel such as item 2 | running shoes | job: start running in the park | how it went: comfy but a size too small | off: wrong size, colour or model | does the job: 3 of 5 | days left: 10 | status: exchange.
retry_notenoOnly on the page's single reformat retry.

The reply is one JSON object:

{"task": "sort", "verdict": "post-today", "headline": "...", "items": [{"item": "1", "status": "return", "hired_for": "...", "report": "...", "next_step": "..."}], "send_first": {"item": "1", "step": "..."}, "closing": "..."}
FieldWhat it holds
verdictCopied from the totals line.
headlineThe verdict in plain words with how many parcels go back and how many stay, at most 16 words.
items1 per parcel in pile order: item (its number), status (copied), hired_for (the job, at most 14 words), report (how it is doing that job with its score, at most 20 words) and next_step (at most 18 words; for a parcel going back it gives the days left).
send_firstitem (copied from the totals line, or empty) and step (agreeing with that parcel's next_step).
closingOne light sentence that agrees with the verdict, at most 20 words.

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

Worked examples - every verdict

These are the four free samples on the page: the exact input the page sends and the reply that replays for free.

All keepers and a lamp past its window - nothing to send back

Input:

{
 "task": "sort",
 "pile": "house rules: checked in order - days left below 0: too-late | off is wrong size, colour or model: exchange | off is faulty or damaged, or do not really need it, or does the job 1 or 2 of 5: return | otherwise: keep | send first: the return or exchange parcel with the fewest days left, the earliest on a tie | verdict: nothing-to-send when no parcel is going back, post-today when the send-first parcel has 3 days left or fewer, otherwise post-soon\ntotals: 3 parcels | keep: 2 | return: 0 | exchange: 0 | too-late: 1 | verdict: nothing-to-send | send first: none\nitem 1 | rain jacket | job: stay dry cycling to the station | how it went: kept me dry in a downpour and the hood stays up | off: nothing | does the job: 5 of 5 | days left: 20 | status: keep\nitem 2 | slow cooker | job: have dinner ready after late shifts | how it went: used it 3 times and the stews come out fine | off: nothing | does the job: 4 of 5 | days left: 12 | status: keep\nitem 3 | desk lamp | job: light my desk for evening reading | how it went: a bit dim but it does the job | off: nothing | does the job: 3 of 5 | days left: window passed | status: too-late"
}

Reply:

{
 "task": "sort",
 "verdict": "nothing-to-send",
 "headline": "Nothing needs to go back; 3 parcels stay.",
 "items": [
  {
   "item": "1",
   "status": "keep",
   "hired_for": "to stay dry cycling to the station",
   "report": "It kept you dry in a downpour and the hood stays up, 5 of 5 on the job.",
   "next_step": "Wear it cycling to the station to stay dry."
  },
  {
   "item": "2",
   "status": "keep",
   "hired_for": "to have dinner ready after late shifts",
   "report": "You used it 3 times and the stews come out fine, 4 of 5 on the job.",
   "next_step": "Use it to have dinner ready after late shifts."
  },
  {
   "item": "3",
   "status": "too-late",
   "hired_for": "to light your desk for evening reading",
   "report": "A bit dim but it does the job, 3 of 5, and the window has passed.",
   "next_step": "Keep using it to light your desk for evening reading."
  }
 ],
 "send_first": {
  "item": "",
  "step": ""
 },
 "closing": "No need to post anything - keep using the rain jacket to stay dry cycling to the station."
}

Faulty earbuds with 2 days left - post today

Input:

{
 "task": "sort",
 "pile": "house rules: checked in order - days left below 0: too-late | off is wrong size, colour or model: exchange | off is faulty or damaged, or do not really need it, or does the job 1 or 2 of 5: return | otherwise: keep | send first: the return or exchange parcel with the fewest days left, the earliest on a tie | verdict: nothing-to-send when no parcel is going back, post-today when the send-first parcel has 3 days left or fewer, otherwise post-soon\ntotals: 4 parcels | keep: 1 | return: 1 | exchange: 1 | too-late: 1 | verdict: post-today | send first: item 1 (2 days left)\nitem 1 | wireless earbuds | job: block out the train noise on my commute | how it went: the left earbud keeps cutting out | off: faulty or damaged | does the job: 2 of 5 | days left: 2 | status: return\nitem 2 | running shoes | job: start running in the park | how it went: comfy but a size too small | off: wrong size, colour or model | does the job: 3 of 5 | days left: 10 | status: exchange\nitem 3 | stand mixer | job: bake bread at weekends | how it went: works well and the bread came out great | off: nothing | does the job: 5 of 5 | days left: 14 | status: keep\nitem 4 | smart watch | job: track my sleep | how it went: the strap irritates my wrist | off: nothing | does the job: 2 of 5 | days left: window passed | status: too-late"
}

Reply:

{
 "task": "sort",
 "verdict": "post-today",
 "headline": "Post today - 2 parcels going back and 2 staying put.",
 "items": [
  {
   "item": "1",
   "status": "return",
   "hired_for": "To block out train noise on your commute.",
   "report": "The left earbud keeps cutting out, doing the job 2 of 5.",
   "next_step": "Send the earbuds back within 2 days."
  },
  {
   "item": "2",
   "status": "exchange",
   "hired_for": "To start running in the park.",
   "report": "Comfy enough for running, but a size too small, coming in at 3 of 5.",
   "next_step": "Ask for a different size, with 10 days left to do so."
  },
  {
   "item": "3",
   "status": "keep",
   "hired_for": "To bake bread at weekends.",
   "report": "It works well and the bread came out great, earning 5 of 5.",
   "next_step": "Keep using it to bake bread on weekends."
  },
  {
   "item": "4",
   "status": "too-late",
   "hired_for": "To track your sleep.",
   "report": "The strap irritates your wrist, sitting at 2 of 5, and the window has passed.",
   "next_step": "Keep using it to track your sleep."
  }
 ],
 "send_first": {
  "item": "1",
  "step": "Send the earbuds back first, within 2 days."
 },
 "closing": "The earbuds really do need to go in the post today - everything else can wait."
}

A throw in the wrong colour and a label maker still boxed - post soon

Input:

{
 "task": "sort",
 "pile": "house rules: checked in order - days left below 0: too-late | off is wrong size, colour or model: exchange | off is faulty or damaged, or do not really need it, or does the job 1 or 2 of 5: return | otherwise: keep | send first: the return or exchange parcel with the fewest days left, the earliest on a tie | verdict: nothing-to-send when no parcel is going back, post-today when the send-first parcel has 3 days left or fewer, otherwise post-soon\ntotals: 3 parcels | keep: 1 | return: 1 | exchange: 1 | too-late: 0 | verdict: post-soon | send first: item 2 (6 days left)\nitem 1 | throw blanket | job: keep warm on the sofa | how it went: soft and warm but the colour clashes with the room | off: wrong size, colour or model | does the job: 4 of 5 | days left: 8 | status: exchange\nitem 2 | label maker | job: sort out the kitchen cupboards | how it went: still in the box | off: do not really need it | does the job: 3 of 5 | days left: 6 | status: return\nitem 3 | noise-cancelling headphones | job: focus in the open-plan office | how it went: they work but squeeze my ears after an hour | off: nothing | does the job: 3 of 5 | days left: 25 | status: keep"
}

Reply:

{
 "task": "sort",
 "verdict": "post-soon",
 "headline": "Send 2 parcels back soon, 1 stays.",
 "items": [
  {
   "item": "1",
   "status": "exchange",
   "hired_for": "Keeping you warm on the sofa.",
   "report": "It's soft and warm, doing its job well at 4 of 5, but the colour clashes with the room.",
   "next_step": "Ask for a different colour within 8 days."
  },
  {
   "item": "2",
   "status": "return",
   "hired_for": "Sorting out the kitchen cupboards.",
   "report": "It's 3 of 5, but you don't really need it.",
   "next_step": "Send the label maker back within 6 days."
  },
  {
   "item": "3",
   "status": "keep",
   "hired_for": "Helping you focus in the open-plan office.",
   "report": "They work at 3 of 5, though they squeeze your ears after an hour.",
   "next_step": "Wear them at the open-plan office to focus."
  }
 ],
 "send_first": {
  "item": "2",
  "step": "Start by sending the label maker back within 6 days."
 },
 "closing": "No rush today - just remember to send the label maker back soon."
}

Low job scores and a tie on days left - post soon, the earliest goes first

Input:

{
 "task": "sort",
 "pile": "house rules: checked in order - days left below 0: too-late | off is wrong size, colour or model: exchange | off is faulty or damaged, or do not really need it, or does the job 1 or 2 of 5: return | otherwise: keep | send first: the return or exchange parcel with the fewest days left, the earliest on a tie | verdict: nothing-to-send when no parcel is going back, post-today when the send-first parcel has 3 days left or fewer, otherwise post-soon\ntotals: 3 parcels | keep: 1 | return: 2 | exchange: 0 | too-late: 0 | verdict: post-soon | send first: item 1 (5 days left)\nitem 1 | air fryer | job: quick dinners on weeknights | how it went: used it a few times and it takes up the whole counter | off: nothing | does the job: 2 of 5 | days left: 5 | status: return\nitem 2 | yoga mat | job: stretch at home in the mornings | how it went: too thin for the hard floor | off: nothing | does the job: 1 of 5 | days left: 5 | status: return\nitem 3 | jigsaw puzzle | job: a screen-free evening activity | how it went: finished it in a weekend and loved it | off: nothing | does the job: 5 of 5 | days left: 30 | status: keep"
}

Reply:

{
 "task": "sort",
 "verdict": "post-soon",
 "headline": "Post soon - 2 parcels going back, 1 staying.",
 "items": [
  {
   "item": "1",
   "status": "return",
   "hired_for": "Quick dinners on weeknights.",
   "report": "Does the job 2 of 5 - used a few times but takes up the whole counter.",
   "next_step": "Send the air fryer back - 5 days left to post it."
  },
  {
   "item": "2",
   "status": "return",
   "hired_for": "Stretching at home in the mornings.",
   "report": "Does the job 1 of 5 - too thin for the hard floor.",
   "next_step": "Return the yoga mat - 5 days left to post it back."
  },
  {
   "item": "3",
   "status": "keep",
   "hired_for": "A screen-free evening activity.",
   "report": "Does the job 5 of 5 - finished in a weekend and loved it.",
   "next_step": "Use it for a screen-free evening activity again."
  }
 ],
 "send_first": {
  "item": "1",
  "step": "Send the air fryer back first - 5 days left."
 },
 "closing": "The air fryer's wait is nearly up - send it back soon."
}

Build the input yourself

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

const K = SendKit;
const built = K.buildInput(K.form({ items: [
  { text: "hiking boots", job: "walk the coast path", went: "rubbed my heels raw", off: "nothing", fit: 2, days: 9 },
  { text: "bread knife", job: "slice crusty loaves", went: "cuts cleanly", off: "nothing", fit: 5, days: 20 }] }));
built.input.pile;                       // the page's lines: rules, totals, verdict and one line per parcel
const wire = K.toWire(built.input);     // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict                  // the verdict the page's rule gives
// K.check(reply, built).notes          // the page's re-check of the reply
// K.md(built, reply, "my pile")        // the sort 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://send-it-back.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":"send-it-back"}' | 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": "sort", "pile": "house rules: checked in order - days left below 0: too-late ...
totals: 4 parcels | keep: 1 | return: 1 | exchange: 1 | too-late: 1 | verdict: post-today | send first: item 1 (2 days left)
item 1 | wireless earbuds | job: block out the train noise on my commute | ... | status: return
..."}
# - 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 send-it-back:<task>:<inputHash>:a<attempt> (the one task is sort), 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="send-it-back:sort:$(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: send-it-back:sort:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json