Drive Lease Return from your own code
Both metered lanes are available over HTTP: the battery check (a used electric car's battery read against a typical fade for its age and miles, the range it likely gives today, red flags from the listing, questions for the seller, test-drive checks and leverage for the price talk) and the offer script (an opening line, the seller's likely worries said first, calibrated questions, what to say at each step of the offer ladder, one ask that is not money, answers to pushback and a walk-away line). A natural use is a car-shopping tool or a listing tracker that wants a second read before the buyer calls the seller: your code sends the listing and the page's numbers, then the target price and the ladder.
The division of labour first: the page estimates and prices; the model writes; the page re-checks every number. The window sticker (battery health from a report or the range screen, the typical fade band, range today in mild weather, in winter and at highway speed, the price per remaining kWh, the battery warranty left, words in the listing worth a question) and the offer ladder (the softened 85/93/97/100 steps or the classic 65/85/95/100 steps of your target, with a precise, non-round last number) come from leasekit.js, which runs in node as well as in the browser. Its check function holds each reply to the page's numbers: ladder amounts must match the page's ladder, no script line may name a price over the walk-away, dollar figures and percentages not on the sticker are flagged, no internal field names in prose. Every number is an estimate from typed inputs and rule-of-thumb rates, not an inspection.
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":"lease-return"}), 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 a required field such as task 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: both lanes are metered and need 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 comes first
Every run sends task: inspect (the battery check) or offer (the offer script). Both share the car and seller fields; each lane adds its own. The model follows only the named task's contract.
| Task | Verdicts | Reply fields |
|---|---|---|
inspect | go (battery health from a report, at or ahead of a typical fade, warranty left, nothing in the listing that needs an answer first), inspect_first (health read only from the range screen, behind a typical fade, little or no warranty left, a listing word worth a question, or a pack under 70% with warranty left to claim), walk_away (title or flood words in the listing, a pack under 70% with no warranty claim to lean on, or health far behind a typical fade) | task, verdict, title, summary, battery_read, range_read, red_flags [{flag, why}] (at most 6), questions_for_seller (at most 7), test_drive_checks (at most 7), leverage (at most 5), cautions (0-3) |
offer | offer (the target is within reach of the asking price), stretch (the target is more than 10% under asking - make the offer, expect to climb the ladder), walk (the walk-away is far under asking, or the battery check said walk away) | task, verdict, title, summary, opening_line, accusation_audit (at most 4), calibrated_questions (at most 6), ladder [{step, amount, say}] (the page's amounts, at most 4), nonmonetary_ask, if_they_say [{they, you}] (at most 5), walk_away_line |
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (LeaseKit.toWire does this). Every value is a string; an unknown number is sent as an empty string.
Input fields: inspect
| Field | Type | Notes |
|---|---|---|
task | string | inspect. |
car | string | The car as described, e.g. year, body and trim, generically (past 200 characters the middle is cut with a marker). |
seller | string | dealer, private, lease-end or online (unknown when blank). |
listing | string | The listing text as pasted; past 5,000 characters the middle is cut and the start and end kept. |
page_numbers | string (JSON) | The window sticker from LeaseKit.forModel: {model_age_years, odometer_miles, miles_per_year, battery_health_pct, battery_health_source ("report" | "range"), battery_health_from_range_pct, typical_for_age_pct, typical_band_pct: [low, high], position_vs_typical, gap_points, usable_kwh_new, usable_kwh_left, range_rated_new, range_mild_est, range_winter_est, range_highway_est, asking_usd, usd_per_kwh_left, usd_per_mile_of_range, battery_gap_usd_est, warranty_years_left, warranty_miles_left, warranty_terms_assumed, chemistry, climate, fast_charging}. Unknown values are null. |
page_flags | string (JSON) | [{level, text}] with level fix / warn / info: the sticker's own notes plus each listing word worth a question, with the word quoted. |
page_verdict | string | The page's own read: go, inspect_first or walk_away. |
retry_note | string | Only on a reformat retry. |
Input fields: offer
| Field | Type | Notes |
|---|---|---|
task | string | offer. |
car, seller | string | As for inspect. |
seller_said | string | What the seller said about the price, in their words (may be empty); past 1,500 characters the middle is cut. |
asking, target, walk_away | string | Whole US dollars, digits only: the asking price, the price the buyer wants to pay, and the most they would pay. With no walk-away typed the page assumes 4% over the target. |
ladder_style | string | soft (85/93/97/100% of the target, for car lots) or classic (65/85/95/100%). |
page_ladder | string (JSON) | [{step, pct_of_target, amount_usd}] from LeaseKit.ladder; the last amount is a precise, non-round number. The reply's ladder must use these amounts. |
battery_check | string | The note carried from a battery check (its verdict and summary, leverage and red flags, from LeaseKit.carryNote), or empty; past 1,500 characters the middle is cut. |
page_numbers | string (JSON) | {battery_health_pct, battery_health_source, typical_for_age_pct, battery_gap_usd_est, warranty_years_left, warranty_miles_left, first_offer_pct_of_asking}. |
page_flags | string (JSON) | [{level, text}]: the ladder's notes plus the listing's warn-level words. |
page_verdict | string | The page's own read: offer, stretch or walk. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with LeaseKit.parseReply, then normalize(reply, task), then check(reply, state, task), which returns {notes}: the page's notes (info, warn) shown under "The page's check" - a verdict that differs from the page's read, ladder amounts that differ from the page's ladder, a price over the walk-away, figures not on the sticker.
Worked examples
Replies produced against the app's prompt for the three sample cars (made-up cars, listings and sellers; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every verdict of both lanes. The page replays each of them free from its sample buttons.
task: inspect - off-lease hatchback - go
Input:
{
"task": "inspect",
"car": "2023 compact electric hatchback, long-range trim",
"seller": "lease-end",
"listing": "Off-lease, one owner, clean title. Lease-end inspection passed. Includes the Level 2 charging cable and both keys. Batte ... (184 characters)",
"page_numbers": "{\"model_age_years\":3.5,\"odometer_miles\":22400,\"miles_per_year\":6400,\"battery_health_pct\":95,\"battery_health_source\":\"report\",\"battery_health_from_range_pct\":97. ... (640 characters)",
"page_flags": "[{\"level\":\"info\",\"text\":\"An off-lease car: usually one driver, regular service and predictable miles - ask for the lease-end inspection sheet. (listing says: Of ... (277 characters)",
"page_verdict": "go"
}
Reply:
{
"task": "inspect",
"verdict": "go",
"title": "Off-lease hatchback, battery slightly ahead of typical",
"summary": "The battery health here comes from an actual service report, not a guess, and at 95% it sits a couple of points above typical for this car's age and mileage. Nothing in the listing raises a hard stop, and real battery-wa ...",
"battery_read": "The 95% battery health comes from a battery health report rather than a rough range-screen guess, so it's a number you can trust. That's about 2.8 points above the middle of what's typical for a car at this age and milea ...",
"range_read": "Today's estimates are about 243 miles in mild weather, dropping to about 170 in winter and about 207 on the highway - winter and highway both come in lower than the mild-weather figure, as expected. Compare these against ...",
"red_flags": [],
"questions_for_seller": [
"Can we see the lease-end inspection sheet, including anything it notes about the battery or charging system?",
"How was the car usually charged day to day, and was fast charging used often?",
"..."
],
"test_drive_checks": [
"Check the charge port for damage or corrosion and confirm the included Level 2 cable actually charges the car.",
"If you can, do a short DC fast-charge session and watch how quickly it ramps up and then tapers off.",
"..."
],
"leverage": [],
"cautions": [
"If you ever see a battery or system warning light, don't explain it away - get a qualified inspection before you buy."
]
}
task: inspect - ex-rideshare crossover - inspect first
Input:
{
"task": "inspect",
"car": "2020 electric crossover, standard trim",
"seller": "private",
"listing": "Selling my 2020 electric crossover. Used it for rideshare for two years, mostly fast charging at the station near the ai ... (203 characters)",
"page_numbers": "{\"model_age_years\":6.5,\"odometer_miles\":61000,\"miles_per_year\":9385,\"battery_health_pct\":79.5,\"battery_health_source\":\"range\",\"battery_health_from_range_pct\":79 ... (642 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"Battery health is read from the range screen only - that number moves with recent driving and temperature. Ask for a battery health rep ... (443 characters)",
"page_verdict": "inspect_first"
}
Reply:
{
"task": "inspect",
"verdict": "inspect_first",
"title": "Private rideshare EV, battery needs verifying",
"summary": "This 2020 electric crossover's battery reads right at typical for its age and mileage, but only from the range screen, not a real health report. The car's actual mileage - about 9,400 miles a year - is normal for a priva ...",
"battery_read": "The battery estimate is about 79.5% of original capacity, but it's read from the range screen rather than a real health report, so treat it as rough, not confirmed. That puts it right at typical for this age and mileage ...",
"range_read": "On today's estimate, expect roughly 205 miles in mild weather, dropping to about 144 in winter and about 174 at highway speeds. Compare those against your own typical trips, especially if winter driving or highway commut ...",
"red_flags": [
{
"flag": "rideshare",
"why": "Two years of rideshare use often means harder duty and more fast-charging sessions, even though the mileage this car has racked up (about 9,400 miles a year) comes out close to normal for a private car."
},
{
"flag": "mostly fast charging at the station near the airport",
"why": "Frequent DC fast charging in a hot climate is one of the harder combinations for long-term battery health - worth asking whether any slower, at-home charging balanced it out."
},
"..."
],
"questions_for_seller": [
"Can you pull a full battery health report from the dealer or the manufacturer's app before we finalize anything?",
"Does the app have a charging history that shows how often you fast-charged versus charged at home?",
"..."
],
"test_drive_checks": [
"Do a DC fast-charge session and watch how quickly it ramps up and whether the speed holds or tapers off early.",
"Check the range shown at a known charge level against the mild, winter, and highway estimates you've been given.",
"..."
],
"leverage": [
"The battery number you have is only a range-screen estimate, not a confirmed health report - use that uncertainty to ask for a lower price or a report before you commit.",
"Two years of rideshare use with mostly fast charging is harder duty than a typical private car sees, even though the mileage itself looks normal - that history is worth something in the price.",
"..."
],
"cautions": [
"This is a read of the listing and estimates, not a mechanic's inspection - get a qualified pre-purchase inspection, including the battery and underbody, before you buy."
]
}
task: inspect - rebuilt hatchback - walk away
Input:
{
"task": "inspect",
"car": "2019 small electric hatchback",
"seller": "dealer",
"listing": "Rebuilt title, sold as-is. Battery health report shows 68%. Great commuter, cheap to run. Cash only.",
"page_numbers": "{\"model_age_years\":7.5,\"odometer_miles\":88000,\"miles_per_year\":11733,\"battery_health_pct\":68,\"battery_health_source\":\"report\",\"battery_health_from_range_pct\":65 ... (646 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"Under a year or 12,000 miles of battery warranty left.\"},{\"level\":\"fix\",\"text\":\"Battery health under 70% and no warranty claim to lean ... (561 characters)",
"page_verdict": "walk_away"
}
Reply:
{
"task": "inspect",
"verdict": "walk_away",
"title": "Rebuilt-title hatchback, battery well behind typical",
"summary": "This 2019 small electric hatchback has a rebuilt title and is being sold as-is, with a battery health report putting it at 68% - well behind what's typical for its age and mileage. A rebuilt title often complicates or vo ...",
"battery_read": "The 68% reading comes from an actual battery health report, not just a rough range-screen guess, so it's the more trustworthy number here - the range-screen estimate alone comes in a bit lower, around 65.3%, which is in ...",
"range_read": "Range today is estimated at about 102 miles in mild weather, dropping to roughly 71 in winter and 87 at highway speeds - both lower than the mild figure, as usual. The listing calls this a \"great commuter,\" but with wint ...",
"red_flags": [
{
"flag": "Rebuilt title",
"why": "A rebuilt title can make the manufacturer's battery warranty void or hard to enforce, and insurers often price and settle these cars differently - get that confirmed in writing, not assumed."
},
{
"flag": "sold as-is",
"why": "There's no comeback if the battery turns out worse than the report says, so get the full battery documentation and any inspection done before you pay, not after."
},
"..."
],
"questions_for_seller": [
"Can I see the full battery health report itself, not just the percentage, and who performed it?",
"What caused the title to be rebuilt, and do you have the full rebuild and inspection paperwork?",
"..."
],
"test_drive_checks": [
"Do a short DC fast-charge session and watch whether the charging speed looks normal for the stated health.",
"Charge to a known percentage and see if the range shown matches the mild-weather estimate.",
"..."
],
"leverage": [
"The battery is estimated well behind typical for this age and mileage - a fair reason to ask for a lower price.",
"At 68%, this is right where a warranty claim would normally start, except the rebuilt title likely takes that option off the table - that risk is now priced into your offer, not theirs.",
"..."
],
"cautions": [
"A rebuilt title means real prior damage happened - get a qualified independent inspection of the car and the battery/pack area specifically before committing, don't rely on the seller's paperwork alone.",
"..."
]
}
task: offer - off-lease hatchback - make the offer
Input:
{
"task": "offer",
"car": "2023 compact electric hatchback, long-range trim",
"seller": "lease-end",
"seller_said": "We price our lease returns to sell, there's not much room on this one.",
"asking": "24900",
"target": "23000",
"walk_away": "24000",
"ladder_style": "soft",
"page_ladder": "[{\"step\":1,\"pct_of_target\":85,\"amount_usd\":19550},{\"step\":2,\"pct_of_target\":93,\"amount_usd\":21400},{\"step\":3,\"pct_of_target\":97,\"amount_usd\":22300},{\"step\":4,\"pct_of_target\":100,\"amount_usd\":22987}]",
"battery_check": "Battery check (go): The battery health here comes from an actual service report, not a guess, and at 95% it sits a coupl ... (389 characters)",
"page_numbers": "{\"battery_health_pct\":95,\"battery_health_source\":\"report\",\"typical_for_age_pct\":92.2,\"battery_gap_usd_est\":0,\"warranty_years_left\":4.5,\"warranty_miles_left\":776 ... (194 characters)",
"page_flags": "[]",
"page_verdict": "offer"
}
Reply:
{
"task": "offer",
"verdict": "offer",
"title": "Lease-return hatchback, battery ahead of typical",
"summary": "The battery estimate here comes from an actual report and sits a little ahead of typical for this car's age and mileage, with plenty of warranty time still left. Nothing in the numbers stands in the way of making an offe ...",
"opening_line": "It sounds like you've priced this one to move quickly since it's a lease return.",
"accusation_audit": [
"You're probably thinking this offer is low given how you've priced it.",
"You might be worried I'm going to pick this car apart to chip away at the price."
],
"calibrated_questions": [
"How did you land on the price for this one?",
"What flexibility do you have given you said there's not much room on it?",
"..."
],
"ladder": [
{
"step": 1,
"amount": 19550,
"say": "Since I still need to see the paperwork and charging history line up before I can fully trust the battery numbers, I have to start at $19,550."
},
{
"step": 2,
"amount": 21400,
"say": "I can move to $21,400, but I'd want that paperwork confirming what you've told me."
},
{
"step": 3,
"amount": 22300,
"say": "I can stretch to $22,300 if the warranty carries over the way it should."
},
{
"step": 4,
"amount": 22987,
"say": "The number I can do is $22,987. That's where I land."
}
],
"nonmonetary_ask": "I'd also want the remaining battery warranty spelled out in writing as part of this.",
"if_they_say": [
{
"they": "We price our lease returns to sell, there's not much room on this one.",
"you": "Not much room - what's built into that number?"
},
{
"they": "I can't go that low.",
"you": "How am I supposed to make this work if there's nowhere to move from here?"
},
"..."
],
"walk_away_line": "That's as far as I can go - if anything changes, you know how to reach me."
}
task: offer - ex-rideshare crossover - stretch
Input:
{
"task": "offer",
"car": "2020 electric crossover, standard trim",
"seller": "private",
"seller_said": "I've had a lot of interest, I'm not in a hurry to sell.",
"asking": "21500",
"target": "17500",
"walk_away": "19000",
"ladder_style": "soft",
"page_ladder": "[{\"step\":1,\"pct_of_target\":85,\"amount_usd\":14900},{\"step\":2,\"pct_of_target\":93,\"amount_usd\":16300},{\"step\":3,\"pct_of_target\":97,\"amount_usd\":17000},{\"step\":4,\"pct_of_target\":100,\"amount_usd\":17487}]",
"battery_check": "Battery check (inspect first): This 2020 electric crossover's battery reads right at typical for its age and mileage, bu ... (1087 characters)",
"page_numbers": "{\"battery_health_pct\":79.5,\"battery_health_source\":\"range\",\"typical_for_age_pct\":78.5,\"battery_gap_usd_est\":0,\"warranty_years_left\":1.5,\"warranty_miles_left\":39 ... (195 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"A fleet or rideshare past usually means many fast charges and high miles per year.\"}]",
"page_verdict": "stretch"
}
Reply:
{
"task": "offer",
"verdict": "stretch",
"title": "Electric crossover, battery about typical, price is a stretch",
"summary": "This reads as a stretch: the asking price sits well above your target, so plan on a few rounds rather than one quick yes. The battery looks about typical for its age and miles, but that number only comes from the range s ...",
"opening_line": "It sounds like you're in no rush and you've had plenty of interest already, so you don't need to deal with a lowball offer from me.",
"accusation_audit": [
"You're probably thinking this number sounds low given all the interest you've mentioned.",
"You might also think I'm using the battery question just to chip away at the price."
],
"calibrated_questions": [
"What would it take for you to feel good about getting a proper battery health report before we move forward?",
"How do you think the rideshare use and mostly fast charging might have affected the battery compared to a typical private car?",
"..."
],
"ladder": [
{
"step": 1,
"amount": 14900,
"say": "Based on what I'm seeing, I can start at $14,900. The battery reading you have is only from the range screen, not a confirmed health report, so I need room to cover that unknown."
},
{
"step": 2,
"amount": 16300,
"say": "I can come up to $16,300. The mileage looks normal for a private car, but two years of rideshare use with mostly fast charging is harder on a pack, and that's worth something in the price."
},
{
"step": 3,
"amount": 17000,
"say": "I can stretch to $17,000. With only about a year and a half of battery warranty left, I'd be taking on real risk if anything comes up with the pack after that."
},
{
"step": 4,
"amount": 17487,
"say": "$17,487. That's the most I can do."
}
],
"nonmonetary_ask": "Along with that last number, I'd want a current battery health report, not just the range screen reading, before we finalize anything.",
"if_they_say": [
{
"they": "I've had a lot of interest, I'm not in a hurry to sell.",
"you": "No rush at all - it sounds like you can afford to wait for the right offer. What would make taking an offer today worth it to you?"
},
{
"they": "That's way too low for this car.",
"you": "Too low? What would you need to see to feel comfortable with a number like that, given the battery reading is only an estimate from the range screen?"
},
"..."
],
"walk_away_line": "That's as far as I can go - if anything changes, you know how to reach me."
}
task: offer - rebuilt hatchback - walk
Input:
{
"task": "offer",
"car": "2019 small electric hatchback",
"seller": "dealer",
"seller_said": "That's already a bargain for an electric car.",
"asking": "9800",
"target": "7000",
"walk_away": "7500",
"ladder_style": "classic",
"page_ladder": "[{\"step\":1,\"pct_of_target\":65,\"amount_usd\":4550},{\"step\":2,\"pct_of_target\":85,\"amount_usd\":5950},{\"step\":3,\"pct_of_target\":95,\"amount_usd\":6650},{\"step\":4,\"pct_of_target\":100,\"amount_usd\":6987}]",
"battery_check": "Battery check (walk away): This 2019 small electric hatchback has a rebuilt title and is being sold as-is, with a batter ... (1182 characters)",
"page_numbers": "{\"battery_health_pct\":68,\"battery_health_source\":\"report\",\"typical_for_age_pct\":76.6,\"battery_gap_usd_est\":480,\"warranty_years_left\":0.5,\"warranty_miles_left\":1 ... (196 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"The first classic step is 46% of asking - on a car lot an opening that low can end the talk. The softened ladder starts higher.\"},{\"lev ... (400 characters)",
"page_verdict": "walk"
}
Reply:
{
"task": "offer",
"verdict": "walk",
"title": "2019 electric hatchback, battery check says walk",
"summary": "The battery estimate on this 2019 electric hatchback sits well behind typical for its age and mileage, and with a rebuilt title sold as-is there's little room to recover if the pack needs work soon - the small amount of ...",
"opening_line": "It sounds like you're confident in how this one is priced.",
"accusation_audit": [
"You're probably going to think I'm walking away over nothing.",
"It might seem like I haven't given this a fair look.",
"..."
],
"calibrated_questions": [
"What would it take to get a current battery health report before any offer makes sense?",
"How does the title history affect what warranty coverage is actually left on the battery?",
"..."
],
"ladder": [
{
"step": 1,
"amount": 4550,
"say": "Given this one is a rebuilt title sold as-is, and the battery estimate reads well behind typical for its age and mileage, $4,550 is where I'd want to start."
},
{
"step": 2,
"amount": 5950,
"say": "I can move to $5,950, but the title and the battery gap still weigh on this for me."
},
{
"step": 3,
"amount": 6650,
"say": "I can stretch to $6,650 - that's close to the edge of what the battery risk and the title let me justify."
},
{
"step": 4,
"amount": 6987,
"say": "$6,987. That's the most I can do."
}
],
"nonmonetary_ask": "a written confirmation of exactly how much battery warranty coverage remains and whether the rebuilt title affects it",
"if_they_say": [
{
"they": "That's already a bargain for an electric car.",
"you": "A bargain - how does that number account for the battery report and the title?"
},
{
"they": "This is a great deal, you won't find better.",
"you": "It seems like you're confident nobody will raise these same concerns."
},
"..."
],
"walk_away_line": "Given the battery estimate, the title, and how little warranty time is left, this isn't one I can move forward on as listed - if the price or the paperwork changes, I'm happy to take another look."
}
Build the input yourself
Load the script in node (global.window = global; require("./leasekit.js")), then:
const K = LeaseKit;
const form = { car: "2023 compact electric hatchback, long-range trim", year: "2023", miles: "22,400",
rated_range: "256", shown_range: "238", charge_pct: "95", battery_kwh: "64", soh_report: "95",
chemistry: "nmc", climate: "mild", fast_share: "rarely", asking: "24,900", seller: "lease-end",
listing: "Off-lease, one owner, clean title. ...", target: "23,000", walk_away: "24,000", ladder_style: "soft" };
const st = K.state(form); // the window sticker (st.S) and the offer ladder (st.L)
console.log(st.S.soh, st.S.band, st.S.range_mild, st.S.per_kwh, st.S.verdict);
console.log(st.L.steps.map(s => s.amount), st.L.verdict);
const wire = K.toWire(K.buildInput(st, "inspect").input); // POST this to /estimate and /run
// then: const check = K.normalize(K.parseReply(job.output.output), "inspect");
// carry it to the offer: form.carried = K.carryNote(check), form.seller_said = "what the seller said",
// then K.toWire(K.buildInput(K.state(form), "offer").input)
// const { notes } = K.check(reply, K.state(form), "offer");
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://lease-return.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://lease-return.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"]
// lease-return.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://lease-return.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://lease-return.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
}
// LeaseReturn.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 LeaseReturn {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://lease-return.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://lease-return.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://lease-return.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://lease-return.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":"lease-return"}' | 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": "lease-return"})["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: "lease-return" })).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": "lease-return"}, 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", "lease-return"), 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: "lease-return" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "lease-return"])["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 = "lease-return" }))!["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": "inspect", "car": "2023 compact electric hatchback, long-range trim", "seller": "lease-end", "listing": "Off-lease, one owner, clean title. ...", "page_numbers": "{...}", "page_flags": "[...]", "page_verdict": "go"}
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": "inspect", "car": "2023 compact electric hatchback, long-range trim", "seller": "lease-end", "listing": "Off-lease, one owner, clean title. ...", "page_numbers": "{...}", "page_flags": "[...]", "page_verdict": "go"}
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: "inspect" | "offer", car, seller, page_numbers, page_flags, page_verdict, ... (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 lane and the input: a retry with the same key never bills twice. The page uses lease-return:<task>:<inputHash>:a<attempt> (the examples below use the inspect lane; an offer run differs only in its task and fields), 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="lease-return:inspect:$(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 = "lease-return:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `lease-return:${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 := "lease-return:" + 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 = "lease-return:" + 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 = "lease-return:#{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 = "lease-return:" . $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 = $"lease-return:{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: lease-return:inspect:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "inspect", "car": "2023 compact electric hatchback, long-range trim", "seller": "lease-end", "listing": "Off-lease, one owner, clean title. ...", "page_numbers": "{...}", "page_flags": "[...]", "page_verdict": "go"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "lease-return:inspect:" + hashlib.sha256(json.dumps(rev).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 rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `lease-return:inspect:${createHash("sha256").update(JSON.stringify(rev)).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", "lease-return:inspect:"+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 rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "lease-return:inspect:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "lease-return:inspect:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
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
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: lease-return:inspect:" . substr(hash("sha256", $rev), 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 rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"lease-return:inspect:{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);