← Back Soon
API

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.

StatusCodeMeaning
400validation_errorThe 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.
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: the away run is metered and needs a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the input and the 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.

FieldRequiredWhat it holds
taskyesAlways away.
away_fromyesThe 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_untilyesThe last day away, written out the same way. The page takes spans of up to 62 days, counting both ends.
back_onyesThe 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_daysyesHow many working days the person is away, as a string (5).
audienceyesWho reads the reply: everyone (anyone who writes, strangers included) or colleagues (people they work with).
toneyescrisp (short and plain), warm (friendly) or formal (polite and businesslike).
reasonyesThe reason the person is happy to share, at most 80 characters (on holiday, at a conference), or none.
coveryesNumbered 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.
urgentyesWhat counts as urgent, at most 200 characters, or none.
notesyesAnything else the person typed, at most 200 characters, or none. Notes are for the run; they are not text to copy.
private_flagsyesThe 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_notenoOnly 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": "..."}
FieldWhat it holds
verdictask, trim or ready.
subjectAt most 60 characters: says the person is away and gives back_on as its weekday and date, without the year.
messageThe 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.
shortOne 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.
noteOne 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:

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

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')

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

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}' 

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