← Text Rewriter / API
Get a token

Drive Text Rewriter from your own code

Everything the web app does goes through one public surface. Base URL:

https://api.skillsafe.ai/v1/app-api

Every request carries Authorization: Bearer <token> and Content-Type: application/json. Every response is a JSON envelope: {"ok":true,"data":{...}} on success and {"ok":false,"error":{"code":"...","message":"..."}} on failure. Read error.code, not the HTTP status alone.

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.
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

Text Rewriter is a single-contract app. There is no task field and no lane router: one contract handles a first rewrite and a follow-up, distinguished by shape ("rewrite" or "revision"). What varies is the dials object, which is where the register targeting lives.

{
  "shape": "rewrite",
  "source_text": "Please be advised that reimbursement will not be provided prior to 14 March 2026.",
  "source_clipped": false,
  "audience": "residents with no legal training",
  "purpose": "make the deadline unmistakable",
  "dials": {
    "formality": "much_plainer",
    "technicality": "less_jargon",
    "length": "shorter",
    "warmth": "warmer",
    "voice": "prefer_active",
    "convention": "keep"
  },
  "target_words": 40,
  "facts": {
    "numbers": [],
    "dates": [
      "14 March 2026"
    ],
    "names": [],
    "negations": [
      "reimbursement will not be provided prior to 14 March 2026"
    ],
    "conditions": [],
    "obligations": []
  },
  "measured": {
    "words": 13,
    "sentences": 1,
    "mean_sentence": 13.0,
    "reading_grade": 14.1,
    "passive_count": 1,
    "nominalisation_count": 0,
    "formula_count": 2,
    "convention_verdict": "unmarked"
  }
}

The body is the input object itself. Wrapping it in {"input": ...} returns 200 and quietly hides every field from the model, which is far worse than an error.

facts and measured are produced by a free pass over the source in the browser. You can send them empty and the rewrite still works; sending them is what lets the model preserve deliberately rather than by luck, and what the reconciliation afterwards is checked against.

Dials

DialValues
formalitymuch_plainer, plainer, keep, more_formal, much_more_formal
technicalityexplain_for_lay, less_jargon, keep, more_technical
lengthmuch_shorter, shorter, keep, longer
warmthwarmer, keep, cooler
voiceprefer_active, keep, prefer_passive
conventionkeep, british, american

Register and convention only. The app does not rewrite text as an ethnic or racial dialect, a national accent, or a non-native speaker's English; such a request is declined in not_done and the rest of the work is still done.

The output contract

The reply is one JSON object. These are the keys the web app's renderer reads:

{
  "title":          "a label you would recognise in a list",
  "audience_read":  "who the model understood it was writing for",
  "source_register": {
    "summary":  "what the source sounds like and what it costs the reader",
    "markers":  [{"marker": "", "evidence": "verbatim from source", "effect": ""}]
  },
  "rewrite":        "the rewritten text, and nothing else",
  "moves":          [{"id": "M1", "axis": "", "kind": "",
                      "before": "verbatim from source",
                      "after":  "verbatim from rewrite",
                      "why":    "why this edit serves this reader"}],
  "preserved":      ["things kept unchanged on purpose"],
  "judgement_calls":[{"call": "", "alternative": ""}],
  "not_done":       ["anything asked for and not done, and why"],
  "residual_risk":  "what a human should still check"
}

axis is one of formality, technicality, length, warmth, voice, convention, structure. kind is one of 28 edit labels — nominalisation_to_verb, passive_to_active, jargon_glossed, formula_replaced, cut_redundant and so on; an unrecognised value normalises to other rather than breaking the render.

before and after are verbatim spans. They must be findable by plain string search in the source and in rewrite respectively, because the interface highlights them. Step 7 shows how to assert that.

1. A tiny client

One helper, reused by every call below. It sets the bearer token, sets the content type only when there is a body, and reads the envelope.

2. Get a token

A guest token is minted for any visitor and can call /me and /estimate. A rewrite is metered, so /run needs a personal token. The token page shows the one this browser holds and will copy a shell export for you.

3. Who am I

Returns subject_type, subject_id and credits. Note that subject_type == "user" is the only reliable test for signed-in; there is no email or name field to check.

4. What will it cost

Free, and it starts no job. Returns hold_credits (a reservation priced against the full output cap, usually far above what is charged), min_credits, model and model_alias. The endpoint does not validate the body — a bare string or an empty array returns a plausible-looking estimate. Assert the shape on your side before you send it.

5. Run it, and poll

Returns a job. Poll GET /run/{job_id} until it reaches a terminal state, then read output.output. Always send an Idempotency-Key header: a content hash of the input plus an attempt counter, so a network blip or a reformat retry cannot double-bill.

6. Or stream it

Server-sent events. Each data: line carries a delta of the JSON object; concatenate them and parse once at the end. Streaming is what lets a progress indicator advance on real signals — the contract's keys arrive in order, so the appearance of "rewrite" or "moves" in the raw stream is evidence rather than a timer.

7. Check the result

The move list is only worth having if the spans are real. These two assertions catch the two failure modes that matter: a move pointing at text that is not in the source, and a figure that did not survive the rewrite. The web app runs a fuller version of this client-side over negations, dates, names, conditions and obligation strength.

Rate limits and etiquette

Back off on 429 rather than retrying immediately. /estimate is free but not unlimited — debounce it behind user input rather than calling it per keystroke. If you are batching, an Idempotency-Key per item means a partial failure can be resumed without paying twice for the items that already succeeded.