Drive Back Soon from your own code
The one metered task, away, is available over HTTP. It takes the days someone is away and the first working day back (written out), who reads the reply, a tone, the reason they are happy to share, who covers what while they are gone, what counts as urgent, any notes, and the private details the page found, and returns an out-of-office subject line, the message people receive, a short status line for chat or a calendar, what it kept out and why, and - when the reply cannot be finished - what to add first.
The division of labour first: the page does the date maths (the working days away and the first working day back), the privacy scan, a free plain draft and the values to paste into Gmail's vacation responder; the model writes the subject, the message and the short status in the chosen tone, copies every cover contact exactly and keeps private details out. The page reads the form (dates as YYYY-MM-DD), works out the return day (weekends skipped unless they are worked), flags personal phone numbers and email addresses, where the person is going, health or family reasons and an empty home, and sends all of it. After the reply it re-checks every part: the return date written exactly, no other dates, every cover contact copied exactly, no private detail and no invented contact. Your code can do the same with the engine files the page loads (see "Build the input yourself").
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":"back-soon"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, away_from, away_until, back_on, working_days, audience, tone, reason, cover, urgent, notes, private_flags) 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: the away run is metered and needs a personal sign-in. |
| 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, the input and the rules
Every run sends task: away (the one task is away); a missing or different one is still answered as away. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always away. |
away_from | yes | The first day away, written out with the weekday and the year: Monday 9 November 2026. The form takes YYYY-MM-DD; AwayKit.fmt writes it out. |
away_until | yes | The last day away, written out the same way. The page takes spans of up to 62 days, counting both ends. |
back_on | yes | The first working day back, written out the same way: the next working day after away_until (weekends skipped unless they are worked), unless the person set a later day themselves. |
working_days | yes | How many working days the person is away, as a string (5). |
audience | yes | Who reads the reply: everyone (anyone who writes, strangers included) or colleagues (people they work with). |
tone | yes | crisp (short and plain), warm (friendly) or formal (polite and businesslike). |
reason | yes | The reason the person is happy to share, at most 80 characters (on holiday, at a conference), or none. |
cover | yes | Numbered lines N. who - what - how to reach them, at most four, 140 characters each, in the person's words, or none. Build it with AwayKit.buildInput, which strips bullets and numbering. |
urgent | yes | What counts as urgent, at most 200 characters, or none. |
notes | yes | Anything else the person typed, at most 200 characters, or none. Notes are for the run; they are not text to copy. |
private_flags | yes | The page's privacy scan, field: kind (the words) joined with ; - for example notes: where you are going (in Lisbon) - or none. Only a guess: the run may find more. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
What the privacy scan flags, in the reason, the urgent line, the notes and each cover line: a personal email address (gmail, outlook, icloud and the like), a phone number that may be personal (one not written as a desk, office, switchboard or support line), a personal contact (mobile, text me, WhatsApp), where the person is going (only in the reason and the notes), a health or family reason, and that their home is empty. Work addresses and desk phones pass. A cover line is reachable when it gives an email address, a phone number or a desk, channel, portal or team inbox.
The reply takes the first verdict that applies: ask when urgent is given but cover is none, or a cover line names someone with no way to reach them - the message carries no contact line and missing says what to add; trim when the input holds a private detail the reply keeps out (anything in private_flags, or a personal phone or email, a destination, a health or family reason, an empty home the scan missed) - each one is listed in left_out; else ready.
The reply
One JSON object:
{"task": "away", "verdict": "trim", "subject": "Back Monday 16 November", "message": "...\n...", "short": "...", "left_out": [{"detail": "...", "why": "..."}], "missing": [], "note": "..."}
| Field | What it holds |
|---|---|
verdict | ask, trim or ready. |
subject | At most 60 characters: says the person is away and gives back_on as its weekday and date, without the year. |
message | The reply people receive, at most 110 words, plain text with \n between lines: back_on exactly as written (the away dates may appear too, and no other dates, days or times), every cover line's person or team, what they cover and how to reach them copied exactly, what to do with urgent matters when there is a contact for them, the reason only in its own words, in the chosen tone. |
short | One or two sentences, at most 30 words, for a chat status or a calendar note: the away dates, the day back and the first cover contact's name. |
left_out[] | {detail, why}, one per private detail kept out of the reply, detail as the person typed it. [] when nothing was left out. |
missing[] | For ask only: one short sentence per gap to fill before the reply can go out. [] otherwise. |
note | One sentence on why the verdict fits. |
The page reads the reply with ReplyKit.normalize, works out the verdict the form implies with ReplyKit.expected(form) and checks the reply with ReplyKit.check(reply, built). check lists, under "The page's check", a missing verdict or one other than the expected one (a trim where the page expected ready is only a note: the run found something the scan did not), a missing or long subject or one without the day back, a missing message or one that does not give the day back exactly, any weekday or month that is none of the person's dates, a flagged private detail still in the subject, message or short line, an email address or number nobody typed, a cover contact left out of the message, a contact invented when there is none, an ask that says nothing to add, a flagged detail missing from left_out, a long or dateless short line, words such as "still" or "always" the form never uses, an exclamation mark in a formal reply, and any internal field name. The verdict on the page reads "Add a contact first", "Ready - private details left out" or "Ready to paste".
What the page hands over for Gmail (AwayKit.gmail(form, subject)), to paste into Settings > See all settings > General > Vacation responder: first (the first day), last (ends - the last day away), subject (the reply's subject), the message, and contactsOnly - true for colleagues, the box "Only send a response to people in my Contacts". The free draft (AwayKit.template(form)) is a plain subject and message built only from the form, leaving out every flagged line.
Worked examples - every sample
These are the free samples on the page: the exact input the page sends, the reply that replays for free, and what the page does with it - the dates it worked out, its privacy scan, the verdict it expects, the Gmail values and its own check. Together they cover the verdicts trim, ready, ask.
A week's holiday
Input:
{
"task": "away",
"away_from": "Monday 9 November 2026",
"away_until": "Friday 13 November 2026",
"back_on": "Monday 16 November 2026",
"working_days": "5",
"audience": "everyone",
"tone": "warm",
"reason": "on holiday",
"cover": "1. Nima Ravensholt - invoices and orders - nima.ravensholt@northwind.example\n2. The support team - anything about deliveries - help@northwind.example",
"urgent": "a delivery that has gone missing",
"notes": "I'll be in Lisbon with patchy signal; text me on 07700 900123 if the warehouse floods",
"private_flags": "notes: a phone number that may be personal (07700 900123); notes: where you are going (in Lisbon)"
}
Reply:
{
"task": "away",
"verdict": "trim",
"subject": "Back Monday 16 November",
"message": "Thank you for your message. I'm on holiday from Monday 9 November 2026 until Friday 13 November 2026, and I'm back on Monday 16 November 2026.\nFor invoices and orders, contact Nima Ravensholt at nima.ravensholt@northwind.example.\nFor anything about deliveries, including a delivery that has gone missing, contact the support team at help@northwind.example.\nThank you for your patience.",
"short": "Away Monday 9 November to Friday 13 November, back Monday 16 November. Contact Nima Ravensholt.",
"left_out": [
{
"detail": "07700 900123",
"why": "A personal phone number should not reach anyone who writes in."
},
{
"detail": "in Lisbon",
"why": "Where you are staying is private and not for a public reply."
}
],
"missing": [],
"note": "The personal phone number and the Lisbon location were left out of the reply because both are private and the message may reach anyone who writes in."
}
The message as people receive it:
Thank you for your message. I'm on holiday from Monday 9 November 2026 until Friday 13 November 2026, and I'm back on Monday 16 November 2026.
For invoices and orders, contact Nima Ravensholt at nima.ravensholt@northwind.example.
For anything about deliveries, including a delivery that has gone missing, contact the support team at help@northwind.example.
Thank you for your patience.
The page's dates: 5 days away, 5 working days; back on Monday 16 November 2026 (the next working day).
The page's privacy scan:
- notes: a phone number that may be personal ("07700 900123")
- notes: where you are going ("in Lisbon")
The verdict the page expects (ReplyKit.expected): trim; the reply says trim - "Ready - private details left out".
For Gmail's vacation responder: first day Monday 9 November 2026; ends Friday 13 November 2026; subject "Back Monday 16 November"; only people in my Contacts: no.
The page's check: kept.
No notes.
Three days at a conference
Input:
{
"task": "away",
"away_from": "Wednesday 18 November 2026",
"away_until": "Friday 20 November 2026",
"back_on": "Monday 23 November 2026",
"working_days": "3",
"audience": "colleagues",
"tone": "crisp",
"reason": "at a conference",
"cover": "1. Oren Quillby - release sign-offs - oren.quillby@acme.example",
"urgent": "none",
"notes": "I'll check messages once each evening",
"private_flags": "none"
}
Reply:
{
"task": "away",
"verdict": "ready",
"subject": "Away, back Monday 23 November",
"message": "I'm away at a conference from Wednesday 18 November to Friday 20 November, back Monday 23 November.\nFor release sign-offs, contact Oren Quillby at oren.quillby@acme.example.\nI'll check messages once each evening.",
"short": "Away Wednesday 18 November to Friday 20 November, back Monday 23 November. For release sign-offs, contact Oren Quillby.",
"left_out": [],
"missing": [],
"note": "The reply gives the away and return dates and the one contact for release sign-offs, with nothing private to leave out."
}
The message as people receive it:
I'm away at a conference from Wednesday 18 November to Friday 20 November, back Monday 23 November.
For release sign-offs, contact Oren Quillby at oren.quillby@acme.example.
I'll check messages once each evening.
The page's dates: 3 days away, 3 working days; back on Monday 23 November 2026 (the next working day).
The page's privacy scan:
Nothing flagged.
The verdict the page expects (ReplyKit.expected): ready; the reply says ready - "Ready to paste".
For Gmail's vacation responder: first day Wednesday 18 November 2026; ends Friday 20 November 2026; subject "Away, back Monday 23 November"; only people in my Contacts: yes.
The page's check: kept.
No notes.
Winter leave, no cover yet
Input:
{
"task": "away",
"away_from": "Monday 14 December 2026",
"away_until": "Friday 1 January 2027",
"back_on": "Monday 4 January 2027",
"working_days": "15",
"audience": "everyone",
"tone": "formal",
"reason": "none",
"cover": "none",
"urgent": "contract renewals and signed orders",
"notes": "none",
"private_flags": "none"
}
Reply:
{
"task": "away",
"verdict": "ask",
"subject": "Away, back Monday 4 January",
"message": "Thank you for your message. I am away from Monday 14 December 2026 until Friday 1 January 2027 and will return on Monday 4 January 2027.",
"short": "Away Monday 14 December 2026 to Friday 1 January 2027, back Monday 4 January.",
"left_out": [],
"missing": [
"Name a contact for contract renewals and signed orders, and give an email, phone number, or desk to reach them."
],
"note": "The reply cannot name a contact for contract renewals and signed orders because none was given."
}
The message as people receive it:
Thank you for your message. I am away from Monday 14 December 2026 until Friday 1 January 2027 and will return on Monday 4 January 2027.
The page's dates: 19 days away, 15 working days; back on Monday 4 January 2027 (the next working day).
The page's privacy scan:
Nothing flagged.
The verdict the page expects (ReplyKit.expected): ask; the reply says ask - "Add a contact first".
For Gmail's vacation responder: first day Monday 14 December 2026; ends Friday 1 January 2027; subject "Away, back Monday 4 January"; only people in my Contacts: no.
The page's check: kept.
No notes.
Build the input yourself
Load the engine in node (global.window = global; require("./awaykit.js"); require("./replykit.js")), then:
const K = AwayKit, R = ReplyKit;
const form = K.form({ from: "2026-11-23", until: "2026-11-27", back: "", weekends: "no", audience: "everyone", tone: "crisp",
reason: "on leave", cover: "Tamsin Elverfold - client questions - tamsin.elverfold@acme.example", urgent: "", notes: "" });
const built = K.buildInput(form); // built.ok is false, with built.note, until both dates are set and in order
const wire = K.toWire(built.input); // POST this to /estimate and /run
// form.back, form.work, form.flags // the first working day back, the working days away, the privacy scan
// K.template(form) // the free draft: {subject, message}
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.expected(form) // the verdict the form implies: ask, trim or ready
// R.check(reply, built).notes // the page's re-check of every part
// K.gmail(form, reply.subject) // {first, last, subject, contactsOnly} for Gmail's vacation responder
Form keys: from and until (the first and last day away, YYYY-MM-DD), back (a later return day, YYYY-MM-DD, or empty for the next working day), weekends (yes if weekends are worked), audience (everyone or colleagues), tone (crisp, warm or formal), reason, cover (one person or team per line, with a way to reach them), urgent and notes.
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://back-soon.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://back-soon.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"]
// back-soon.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://back-soon.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://back-soon.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
}
// BackSoon.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 BackSoon {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://back-soon.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://back-soon.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://back-soon.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://back-soon.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":"back-soon"}' | 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": "back-soon"})["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: "back-soon" })).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": "back-soon"}, 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", "back-soon"), 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: "back-soon" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "back-soon"])["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 = "back-soon" }))!["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": "away", "away_from": "Monday 23 November 2026", "away_until": "Friday 27 November 2026", "back_on": "Monday 30 November 2026", "working_days": "5", "audience": "everyone", "tone": "crisp", "reason": "on leave", "cover": "1. Tamsin Elverfold - client questions - tamsin.elverfold@acme.example", "urgent": "none", "notes": "none", "private_flags": "none"}
# - see the field table
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": "away", "away_from": "Monday 23 November 2026", "away_until": "Friday 27 November 2026", "back_on": "Monday 30 November 2026", "working_days": "5", "audience": "everyone", "tone": "crisp", "reason": "on leave", "cover": "1. Tamsin Elverfold - client questions - tamsin.elverfold@acme.example", "urgent": "none", "notes": "none", "private_flags": "none"}
# - see the field table
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: "away", away_from, away_until, back_on, working_days, audience, tone, reason, cover, urgent, notes, private_flags} (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 back-soon:<task>:<inputHash>:a<attempt> (the one task is away), 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="back-soon:away:$(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 = "back-soon:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"), reply.get("subject"))
import { createHash } from "node:crypto";
const key = `back-soon:${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, reply.subject);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "back-soon:" + 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 }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"], reply["subject"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "back-soon:" + 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.get("verdict") + " " + reply.get("subject"));
require "digest"
key = "back-soon:#{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["verdict"], reply["subject"]].inspect
$key = "back-soon:" . $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["verdict"] ?? ""), " ", ($reply["subject"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"back-soon:{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!["verdict"]} {reply["subject"]}");
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: back-soon:away:$(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": "away", "away_from": "Monday 23 November 2026", "away_until": "Friday 27 November 2026", "back_on": "Monday 30 November 2026", "working_days": "5", "audience": "everyone", "tone": "crisp", "reason": "on leave", "cover": "1. Tamsin Elverfold - client questions - tamsin.elverfold@acme.example", "urgent": "none", "notes": "none", "private_flags": "none"}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "back-soon:away:" + 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": `back-soon:away:${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", "back-soon:away:"+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", "back-soon:away:" + 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" => "back-soon:away:#{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: back-soon:away:" . 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", $"back-soon:away:{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);