Draft Desk — API

Raw material in, a publish-ready article out — from your own code.

API tokens Open the app

Driving Draft Desk from your own code

Everything the page does over the network, you can do from a script: mint a token, price a run for free, then write the article — either as a job you poll, or as a stream you read as it is written. Pick your language once and every example on the page follows.

Base URL https://api.skillsafe.ai/v1/app-api
There is no /apps/{slug}/ segment — the slug is bound to the token when you mint it at /guest. Every response is the envelope {"ok":true,"data":{…}} or {"ok":false,"error":{…}}. The /run body is the input object directly, not {"input":{…}}.

The input Draft Desk accepts

FieldRequiredWhat it is
materialyesThe raw material as pasted: notes, bullets, a transcript, research fragments, or an existing draft. The page sends at most 60,000 characters, keeping both ends and dropping the middle behind a marker.
voicenoHow the author writes — sample paragraphs beat a description. Empty means the voice is inferred from the material.
audiencenoWho reads it, where it runs, what it should achieve.
formatnoAuto, Technical guide, Essay, Newsletter or Blog post. Defaults to Auto.
modenoAuto, Write from notes or Tighten an existing draft.
factsnoA one-line summary of a mechanical scan of the material (counts, banned-pattern matches, TBD markers). It is a hint the model is told to verify, never a fact.
retry_notenoOnly sent on the automatic reformat retry: a verbatim restatement of the output shape, used when a first reply did not parse. Leave it out of a normal run.

The output contract

data.output.output is plain text, and the page rejects anything that does not match this shape exactly — five tag lines, then four sections in order:

TITLE: <plain text>
FORMAT: Technical guide | Essay | Newsletter | Blog post
VERDICT: Ready to publish | Needs your facts | Not enough material
CONFIDENCE: <integer 0-100>
SUMMARY: <2-4 sentences, ends at the first blank line>

## Article
<the article in markdown, ### headings or deeper only>

## Voice notes
- <at least one bullet>

## What was cut
- <bullets, or the single bullet "Nothing cut.">

## Missing facts
- <bullets, or the single bullet "Nothing missing.">

Facts the material did not carry appear in the article as <placeholder> tokens in angle brackets and as bullets under Missing facts — never as invented values. A Ready to publish verdict alongside real Missing facts bullets is a contradiction the page flags rather than hides.

Errors

HTTPerror.codeWhat to do
401unauthorizedThe token is missing, expired or revoked. Mint a new one at /guest, or sign in on the token page.
402payment_requiredThe balance will not cover the reserve. Check /estimate against /me first — that is what makes this unreachable.
404not_foundAlmost always a wrong path: there is no /apps/{slug}/ segment on these routes.
422validation_errorThe body was not the input object, or material was empty.
429rate_limitedBack off and retry with the same Idempotency-Key.
5xxinternalRetry with the same Idempotency-Key; a replay returns deduped:true and the original job rather than billing again.

1Get a token

A guest token is enough for /me and the free /estimate. For metered runs billed to your account, sign in on the token page and copy the shell export from there — you never need the browser console.

# A guest token, no browser involved. Guests can call /me and the free
# /estimate; sign in on https://draft-desk.skillsafe.ai/tokens.html for a
# personal token so metered runs bill your account.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"draft-desk"}'

# {"ok":true,"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}

# Keep it in the environment, never in the source:
export SKILLSAFE_TOKEN="aut_..."

2Check who you are and what you have

curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"

# {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":48213}}
# subject_type is "guest" for a guest token. credits are in hundred-thousandths
# of a dollar: 10000 credits = $1.00.

3Price it — free, and it proves the model binding

/estimate creates no job and charges nothing. It is also the authoritative check that the app is wired to the tier you expect: model_alias reads gpt-terra and markup_bps is 1000. The app is bound to the alias, not to a concrete model id, so read model from this response rather than hard-coding it — an alias repoints as new models ship (it resolves to gpt-5.6-terra today) and can only ever move to a model at or below the outgoing one’s token rates.

# Free: creates no job and charges nothing. This is also the call that
# proves which model the app is bound to.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "material": "Migration notes...\\nPostgres GIN full text -> SQLite FTS5. p95 412ms -> 38ms. Index 1.8GB -> 210MB.\\nPrice: CHECK with Sam, do not guess.",
  "voice": "Short sentences. Dry. No exclamation marks.",
  "audience": "Backend engineers on our engineering blog.",
  "format": "Technical guide",
  "mode": "Write from notes",
  "facts": "Mechanical scan of the material (pattern matching, verify before repeating): 78 words, 9 numeric tokens, 0 URLs, 0 quoted passages, marked gaps: line 3 \\\"CHECK with Sam\\\"."
}'

# {"ok":true,"data":{"hold_credits":1561,"min_credits":133,
#                    "model":"gpt-5.6-terra","model_alias":"gpt-terra",
#                    "markup_bps":1000,"sponsor_enabled":false,"byok":false}}
#
# hold_credits is RESERVED, not charged - it prices the full output cap. The
# settled charge is usually far lower. Compare hold_credits against the balance
# from /me before you run, exactly as the page does.

4Write the article — job and poll

This one is metered. Send an Idempotency-Key on every run: a retried request replays the same job instead of billing a second one. Derive it from a hash of the input plus a counter you bump for each deliberate new run — a key that only advances on success will replay a failed attempt's job when you try again.

# METERED. The body is the input object DIRECTLY - not {"input": {...}}.
# Idempotency-Key makes a retried request replay one job instead of billing two.
KEY="draft-desk:$(printf '%s' "$MATERIAL" | shasum -a 256 | cut -c1-16)-g1"

JOB=$(curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll to a terminal state.
until curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
       -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
     | tee /tmp/job.json | grep -q '"status":"succeeded"\|"status":"failed"'; do
  sleep 2
done
python3 -c 'import json;print(json.load(open("/tmp/job.json"))["data"]["output"]["output"])' 

5Or stream it

/run-stream is the same run over server-sent events, which is what the page uses so the article appears as it is written. Read event: delta for the text as it arrives, but take event: done as authoritative — the delta stream can drop the tail. done also carries charged_credits (the real cost, usually well under the reserve) and truncated.

# METERED. Server-sent events: the article arrives as it is written.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @input.json

# event: job    data: {"job_id":"job_..."}
# event: delta  data: {"text":"TITLE: We moved off Postgres"}
# event: delta  data: {"text":" full text search\n"}
# event: done   data: {"output":{"output":"TITLE: ..."},"charged_credits":740,
#                      "truncated":false}
#
# The done payload is authoritative - the deltas can drop the tail.

Doing the free lane yourself

The material scan, the banned-pattern catalog, the article re-lint and the grounding check all run in the browser and need no API at all — they are in draftlint.js, and the reply parser and renderers are in report.js. If you are scripting this, the useful part is the grounding check: pull the figures and URLs back out of data.output.output and assert every one appears in the material you sent.