← Leave the Dishes
Checkout card API
Tokens

Drive Leave the Dishes from your own code

The one metered task, trim, is available over HTTP. It takes a holiday-rental checkout list (one request per line), an optional line about the place and a voice for the card, and returns a verdict (rewrite the list, trim a few, or ready to hang), every request sorted into one slot with a reason and a line for guests, a short door-hanger card and the cleaner's list.

The division of labour first: the page owns the slips, the keyword guess and the verdict rule; the model owns the judgement and the words; the page re-checks the reply. All three come from slipkit.js, which you can run yourself (see Build the input yourself). It is a writing aid, not any platform's policy text.

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":"leave-the-dishes"}), 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, requests, tone, presort) 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: writing a card 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 slots and the verdict

Every run sends task: trim; a missing or different one is still answered as trim. Each request gets one slot, checked in a fixed order:

  1. local - the request itself names a local safety or property reason or fixture (wildlife or bears, a locking or animal-proof bin, a septic system, freezing pipes, embers or ashes, a hot tub cover).
  2. soften - washing or putting away dishes, stripping beds or laundry, carrying trash outside or off the property, or not using an amenity. Each has a light version: used dishes in the sink; the used towels gathered in one place; trash sorted into the indoor bins; the amenity turned off when leaving.
  3. cleaner - any other cleaning or tidying that takes real time: vacuuming, mopping, wiping, scrubbing, cleaning a room or an appliance, making the beds.
  4. keep - everything else: a light ask or a rule that is not a chore.

The verdict then follows the slots, first match wins: rewrite if three or more requests are cleaner; otherwise trim if any is cleaner or soften; otherwise ready. The page computes the same rule over the reply's own slots (SlipKit.verdictFor) and flags a reply that disagrees.

The input

Every value is a string (objects are never sent). app.js builds it with SlipKit.buildInput(form).input.

FieldRequiredWhat it holds
taskyesAlways trim.
requestsyesNumbered lines, 1. ..., one request per line in the host's words. At most 24 requests are sent; a request over 240 characters keeps its start and end around [...].
placenoA line about the place, or not given. Over 600 characters keeps its start and end.
toneyesStarts with warm, brisk or playful, then a short description in brackets.
presortyesThe page's keyword guess, one line per request: <n> | <slot> | <topic>. Topics: dishes, laundry, trash, utilities, cleaning, locks, timing, local, rule, other. Only a guess; the slot rules win.
clip_notenoWhat the page shortened, or absent.
retry_notenoSent only on the page's one reformat retry.

The reply

The reply is one JSON object in job.output.output. The page parses it with SlipKit.parseReply and SlipKit.normalize (a cut-off stream is closed by SlipKit.closeJson and shown as "N of 7 sections recovered").

{"task": "trim", "verdict": "rewrite | trim | ready", "verdict_reason": "...", "title": "...", "summary": "...",
 "requests": [{"n": 1, "slot": "keep | soften | cleaner | local", "why": "...", "guest_line": "... (empty for cleaner)"}],
 "card": {"heading": "...", "lines": ["... 1 to 7 lines"], "signoff": "..."},
 "cleaner_list": ["..."]}
FieldRule
requestsExactly one entry per numbered request, same numbers and order. guest_line is the tidied request (keep), its light version (soften), the request with its reason (local), or empty (cleaner).
cardHeading of at most 6 words; 1 to 7 lines of at most 16 words built only from the lines for guests, in request order; a sign-off of at most 8 words in the chosen voice. A time appears only if a request gives it.
cleaner_listOne line per cleaner request and per soften request about dishes, laundry or trash, in the host's own words. A softened amenity leaves no line.
summaryTwo sentences: the counts per slot (zeros left out), then what the card now asks.

The page then checks the reply against its own facts (SlipKit.check): every request sorted exactly once, the verdict against the reply's own slots, the keyword guess where it differs, a line for guests on every request that stays and none on a cleaner's job, no real cleaning and no time on the card that the list does not give, one cleaner's line per moved chore, no booking platform named, and no internal field ids in the prose.

Worked examples, one per verdict

These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.

City flat, already light - ready

Input:

{
 "task": "trim",
 "requests": "1. Check out by 11am\n2. Put used dishes in the sink\n3. Leave used towels in the bathtub\n4. Turn off the lights and the heating\n5. Lock the door and drop the keys in the lockbox",
 "place": "Two-bedroom flat on the third floor.",
 "tone": "warm (friendly and thankful, like a note from a host who hopes you come back)",
 "presort": "1 | keep | timing\n2 | keep | dishes\n3 | keep | laundry\n4 | keep | utilities\n5 | keep | locks"
}

Reply:

{
 "task": "trim",
 "verdict": "ready",
 "verdict_reason": "All five of your requests already stay as they are, with none needing to be softened or sent to the cleaner.",
 "title": "Your Checkout List Is Already Light",
 "summary": "All five of your requests stay as they are. Your card now asks guests to check out on time, put dishes in the sink, gather towels, turn off the lights and heating, and lock up with the keys in the lockbox.",
 "requests": [
  {
   "n": 1,
   "slot": "keep",
   "why": "You're just asking guests to leave by a set time, not do a chore.",
   "guest_line": "Check out by 11am."
  },
  {
   "n": 2,
   "slot": "keep",
   "why": "You're already asking for the light version, dishes left in the sink.",
   "guest_line": "Put used dishes in the sink."
  },
  {
   "n": 3,
   "slot": "keep",
   "why": "You're already asking for the light version, towels gathered in one place.",
   "guest_line": "Leave used towels in the bathtub."
  },
  {
   "n": 4,
   "slot": "keep",
   "why": "You're already asking guests to turn things off, which is the light version.",
   "guest_line": "Turn off the lights and the heating."
  },
  {
   "n": 5,
   "slot": "keep",
   "why": "You're asking guests to lock up and leave the keys, a normal light step.",
   "guest_line": "Lock the door and drop the keys in the lockbox."
  }
 ],
 "card": {
  "heading": "A Few Quick Things",
  "lines": [
   "Check out by 11am.",
   "Put used dishes in the sink.",
   "Leave used towels in the bathtub.",
   "Turn off the lights and the heating.",
   "Lock the door and drop the keys in the lockbox."
  ],
  "signoff": "Thanks so much - hope to see you again!"
 },
 "cleaner_list": []
}

Seaside cottage, a few heavy asks - trim

Input:

{
 "task": "trim",
 "requests": "1. Please wash, dry and put away all the dishes\n2. Strip the beds and put the sheets in the washing machine\n3. Sort the recycling into the blue box under the sink\n4. Please don't use the air conditioning at night\n5. Vacuum the living room rug\n6. Check out is at 10am - leave the keys on the kitchen table",
 "place": "Small seaside cottage, two bedrooms, sleeps four.",
 "tone": "playful (light and cheerful, one gentle pun at most)",
 "presort": "1 | soften | dishes\n2 | soften | laundry\n3 | keep | trash\n4 | soften | utilities\n5 | cleaner | cleaning\n6 | keep | locks"
}

Reply:

{
 "task": "trim",
 "verdict": "trim",
 "verdict_reason": "You have three softened requests and one that goes to the cleaner, so the list comes out shorter for guests.",
 "title": "A Lighter Checkout List for Guests",
 "summary": "Of your six requests, two stay as they are, three are softened, and one goes to the cleaner. The card now asks guests to put dishes in the sink, gather used towels in one place, sort the recycling into the indoor box, switch off the air conditioning on the way out, and check out at 10am with the keys on the kitchen table.",
 "requests": [
  {
   "n": 1,
   "slot": "soften",
   "why": "You asked for the dishes done, and that request can be softened to a quick sink tidy.",
   "guest_line": "Put the used dishes in the sink."
  },
  {
   "n": 2,
   "slot": "soften",
   "why": "Beds and laundry take real time, so this request is softened to just gathering towels.",
   "guest_line": "Gather the used towels in one place."
  },
  {
   "n": 3,
   "slot": "keep",
   "why": "Sorting recycling into an indoor box is already a light ask, so it stays as it is.",
   "guest_line": "Sort the recycling into the blue box under the sink."
  },
  {
   "n": 4,
   "slot": "soften",
   "why": "Limiting the air conditioning is softened to simply turning it off on the way out.",
   "guest_line": "Turn off the air conditioning when you leave."
  },
  {
   "n": 5,
   "slot": "cleaner",
   "why": "Vacuuming takes real time and effort, so this one goes to the cleaner.",
   "guest_line": ""
  },
  {
   "n": 6,
   "slot": "keep",
   "why": "Checkout time and key placement are simple rules, so this stays as it is.",
   "guest_line": "Check out at 10am and leave the keys on the kitchen table."
  }
 ],
 "card": {
  "heading": "A Few Small Favors Before Leaving",
  "lines": [
   "Pop the used dishes in the sink.",
   "Gather the used towels in one place.",
   "Sort the recycling into the blue box under the sink.",
   "Switch off the air conditioning on your way out.",
   "Check out at 10am and leave the keys on the kitchen table."
  ],
  "signoff": "Thanks so much, safe travels!"
 },
 "cleaner_list": [
  "Wash, dry and put away the dishes.",
  "Strip the beds and put the sheets in the washing machine.",
  "Vacuum the living room rug."
 ]
}

Hill cabin, a whole cleaning shift - rewrite

Input:

{
 "task": "trim",
 "requests": "1. Vacuum and mop all the floors\n2. Wipe down the kitchen counters and the stove\n3. Clean the bathroom, including the shower\n4. Take all trash to the dumpster at the end of the road\n5. Start a load of towels before you leave\n6. Bears visit at night - put food waste in the locking bin by the shed\n7. Clean out the fridge\n8. Close all the windows and lock the front door",
 "place": "Log cabin up a gravel road in the hills. Bears come through in autumn.",
 "tone": "brisk (short, clear and polite, a checklist feel)",
 "presort": "1 | cleaner | cleaning\n2 | cleaner | cleaning\n3 | cleaner | cleaning\n4 | soften | trash\n5 | soften | laundry\n6 | local | local\n7 | cleaner | cleaning\n8 | keep | locks"
}

Reply:

{
 "task": "trim",
 "verdict": "rewrite",
 "verdict_reason": "Four of your requests are heavy cleaning tasks, which is enough on its own to call for a full redo of the list.",
 "title": "Your Checkout List Needs A Redo",
 "summary": "One request stays as it is, two are softened, four go to the cleaner, and one is kept for a local reason. The card now asks guests to sort trash and recycling into the indoor bins, gather the used towels, put food waste in the locking bin because bears visit at night, and close the windows and lock the door.",
 "requests": [
  {
   "n": 1,
   "slot": "cleaner",
   "why": "Vacuuming and mopping floors takes real time, so this goes to the cleaner.",
   "guest_line": ""
  },
  {
   "n": 2,
   "slot": "cleaner",
   "why": "Wiping down counters and the stove is cleaning that takes effort, so it goes to the cleaner.",
   "guest_line": ""
  },
  {
   "n": 3,
   "slot": "cleaner",
   "why": "Cleaning the bathroom and shower is real cleaning, so it goes to the cleaner.",
   "guest_line": ""
  },
  {
   "n": 4,
   "slot": "soften",
   "why": "Taking trash to the dumpster has a light version, so this is softened to sorting bins indoors.",
   "guest_line": "Sort the trash and recycling into the indoor bins."
  },
  {
   "n": 5,
   "slot": "soften",
   "why": "Starting a load of towels has a light version, so this is softened to gathering towels.",
   "guest_line": "Gather the used towels in one place."
  },
  {
   "n": 6,
   "slot": "local",
   "why": "Bears visiting at night is a safety reason tied to this place, so this is kept for a local reason.",
   "guest_line": "Put food waste in the locking bin by the shed, since bears visit at night."
  },
  {
   "n": 7,
   "slot": "cleaner",
   "why": "Cleaning out the fridge takes real effort, so it goes to the cleaner.",
   "guest_line": ""
  },
  {
   "n": 8,
   "slot": "keep",
   "why": "Closing windows and locking the door is a light ask already, so this stays as it is.",
   "guest_line": "Close all the windows and lock the front door."
  }
 ],
 "card": {
  "heading": "Before You Leave",
  "lines": [
   "Sort the trash and recycling into the indoor bins.",
   "Gather the used towels in one place.",
   "Put food waste in the locking bin by the shed, since bears visit at night.",
   "Close all the windows and lock the front door."
  ],
  "signoff": "Thanks for taking care of this!"
 },
 "cleaner_list": [
  "Vacuum and mop all the floors",
  "Wipe down the kitchen counters and the stove",
  "Clean the bathroom, including the shower",
  "Take all trash to the dumpster at the end of the road",
  "Start a load of towels",
  "Clean out the fridge"
 ]
}

Build the input yourself

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

const K = SlipKit;
const form = K.form({ tone: "warm", place: "Studio flat by the station.",
  requests: "Check out by 10am\nWash and put away the dishes\nVacuum the rug\nLeave the key in the lockbox" });
K.slips(form);                                   // one slip per line, with the page's slot and topic guess
K.expected(K.slips(form));                       // {verdict: "trim", why: [...]} - the page's rule over its guesses
K.draft(K.slips(form));                          // the free door-hanger draft and cleaner's list
const built = K.buildInput(form);
const wire = K.toWire(built.input);              // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes                   // the page's re-check of the reply
// K.md(built, reply)                            // the card and the requests 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://leave-the-dishes.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":"leave-the-dishes"}' | 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": "trim", "requests": "1. Please wash, dry and put away all the dishes\n2. Strip the beds and put the sheets in the washing machine\n3. Sort the recycling into the blue box under the sink\n4. Please don't use the air conditioning at night\n5. Vacuum the living room rug\n6. Check out is at 10am - leave the keys on the kitchen table", "place": "Small seaside cottage, two bedrooms, sleeps four.", "tone": "playful (light and cheerful, one gentle pun at most)", "presort": "1 | soften | dishes\n2 | soften | laundry\n3 | keep | trash\n4 | soften | utilities\n5 | cleaner | cleaning\n6 | keep | locks"}
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 leave-the-dishes:<task>:<inputHash>:a<attempt> (the one task is trim), 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="leave-the-dishes:trim:$(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: leave-the-dishes:trim:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json