← Writing Prompt Generator / API
Get a token

Drive Writing Prompt Generator from your own code

Everything the web app does goes through one public surface. The base URL is https://api.skillsafe.ai/v1/app-api, every request carries Authorization: Bearer <token> and Content-Type: application/json, and every response is a JSON envelope: {"ok":true,"data":{…}} on success, {"ok":false,"error":{"code":"…","message":"…"}} on failure. Read error.code, not the HTTP status alone.

Get a token on the token page — it reads the one this browser already holds, so you never need the developer console. A guest token is enough for /me and /estimate; writing a prompt is metered and needs a personal token.

Errors

CodeMeaningWhat to do
UNAUTHORIZEDMissing, expired or wrong-app token.Mint a guest token or sign in again. A cold 401 from /me before any token exists is normal, not a defect.
INSUFFICIENT_CREDITSBalance below min_credits.Top up. Call /estimate first — it is free and tells you the hold.
VALIDATION_ERRORThe input object failed validation.Check error.details. The run body is the input object itself — do not wrap it in {"input": …}.
RATE_LIMITEDToo many requests.Back off and retry. Do not tight-loop.
JOB_FAILEDThe run started and did not complete.Retry with the same Idempotency-Key so a partial charge is not doubled.

The input object

Writing Prompt Generator is a single-contract app. There is no task field and no lane router; one contract handles both a first draw and a second run pushed away from it, distinguished by shape.

FieldTypeRequiredWhat it is
shapestringyes"draft" or "push".
briefobjectyesThe drawn coordinate. See below.
brief.coordinateobjectyesNine axis ids. Echoed back as coordinate_used.
brief.enginearrayyes{axis,label,value} for the situation elements. Each must be present in the finished prompt.
brief.surfacearrayyesSame shape, for how and where it is told.
brief.left_openarrayyesElements the model is told about and must not state anywhere in the reply. An element here is not repeated in engine or surface.
brief.pushed_offarrayyesCorrections the sampler made within this draw. Informational; may be empty.
brief.still_near_archetypestring or nullyesA tired premise this coordinate is still within reach of, or null.
genre, genre_pull, tired_in_genrestring, string, arrayyesGenre id, what the genre does to a situation, and the moves already worn out inside it.
form, length_notestring, stringyesflash, short, novella or novel, and what that length asks of a premise.
tonestringyesA register, or "any".
steer, steer_clippedstring, booleanyesThe writer's own words about what they want it near. Often empty.
boundary_actsarrayyesThe acts the app refuses.
prior, moved_by, avoid_noteobject, array, stringpush onlyThe prompt just read, the engine elements that changed relative to it, and the writer's own note about what not to repeat.

The output contract

One JSON object, no fence, no prose around it. Keys: title, prompt, who, want, against, constraint, cost, pressure, withheld (array), openings (array of {frame,line}), complications (array), avoid (array), why_it_holds, form_note, coordinate_used (object), and on a push also moved_from.

Note the deliberate absence: the contract carries no length guidance for any field. A per-field sentence count writes the cadence as well as the contract, and every prompt then arrives the same size regardless of what it is about.

Step 1 — a client helper

Four calls, one envelope, one auth header. Everything below assumes this helper exists. Never hard-code a live token into a file you commit.

Step 2 — who you are

/me returns exactly three fields: subject_type, subject_id and credits. There is no email and no display name — the signed-in test is subject_type === "user".

Step 3 — build the brief

The coordinate is drawn client-side; there is no endpoint that draws one for you. Mirror the sampler in /draw.js, or hand over a coordinate you chose. This is the complete input object for a draft:

Step 4 — estimate, and guard your own shape

/estimate is free and starts no job. It returns model, model_alias, markup_bps, hold_credits and min_credits.

One thing about this endpoint is worth more than the rest of this page. It performs no validation of the request body. A bare JSON string, a null and an empty array all return ok:true with a correct model binding and an identical hold_credits. So the usual “the estimate came back with the right model, therefore the wiring is right” check proves the model binding and nothing at all about your input shape. The only place a malformed body is ever caught is in your own code, before the call.

Step 5 — run and poll

POST /run returns a job_id; poll GET /jobs/{job_id} until status is succeeded or failed. The model's text is at data.output.output, the real cost at data.charged_credits — usually far below the hold, which prices the full output cap.

Step 6 — or stream it

POST /run-stream is the same call over SSE. Events are job, delta, done and error. Accumulate every delta; if the stream ends early, parse what arrived rather than discarding it — a reply cut off mid-array still contains most of a usable prompt.

Step 7 — parse it, and check it

The reply is one JSON object, occasionally inside a fence. Strip the fence, take everything from the first {, and parse.

Then run the check that matters: does constraint actually exclude anything? A constraint that yields to effort, cleverness or waiting is a difficulty, and a difficulty belongs in against. This is the single highest-value assertion against a prompt generator's output, and it is structural rather than a matter of taste.

The coordinate vocabulary

The nine axes and every option id are in /axes.js, served from this origin and readable without a token. An API client can mirror the sampler from /draw.js or supply its own coordinate; the ids in brief.coordinate are the only part the reconciler compares against.

AxisBandWhat it fixes
stanceengineWhose story it is, by position relative to what has already happened.
wantengineWhat they are after, specific enough to be refused.
againstengineThe obstacle class, with its own reasons.
constraintengineThe exclusive choice. Every option is shaped “A or B, not both”.
pivotengineThe turn available, including an option for no turn at all.
pressuresurfaceThe clock, including “nothing is forcing it”.
withheldsurfaceWhat the prompt deliberately does not supply.
framesurfaceNarrative distance and shape.
arenasurfaceSetting as pressure rather than decoration.

Distance between two coordinates is measured on the engine band only, and is family-aware: two options in the same family count as most of the way to identical. That is what stops a swapped setting from registering as variety.

Rate limits and etiquette

Back to the app · Tokens · llms.txt