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
| Code | Meaning | What to do |
|---|---|---|
UNAUTHORIZED | Missing, 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_CREDITS | Balance below min_credits. | Top up. Call /estimate first — it is free and tells you the hold. |
VALIDATION_ERROR | The input object failed validation. | Check error.details. The run body is the input object itself — do not wrap it in {"input": ...}. |
RATE_LIMITED | Too many requests. | Back off and retry. Do not tight-loop. |
JOB_FAILED | The 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
| Dial | Values |
|---|---|
formality | much_plainer, plainer, keep, more_formal, much_more_formal |
technicality | explain_for_lay, less_jargon, keep, more_technical |
length | much_shorter, shorter, keep, longer |
warmth | warmer, keep, cooler |
voice | prefer_active, keep, prefer_passive |
convention | keep, 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.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"
# Every call in this guide reuses these two.
# Read error.code, never the HTTP status alone.
call() {
curl -s -X "$1" "$BASE$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
${3:+-d "$3"}
}
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!json.ok) throw new Error(json.error.code + ": " + json.error.message);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body []byte) ([]byte, error) {
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
import java.net.URI;
import java.net.http.*;
public class TextRewriter {
static final String TOKEN = "YOUR_TOKEN";
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.method(method, pub);
if (body != null) b.header("Content-Type", "application/json");
return HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
def call(method, path, body = nil)
uri = URI(BASE.to_s + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)
end
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
function call($method, $path, $body = null) {
global $token, $base;
$headers = ["Authorization: Bearer $token"];
if ($body !== null) { $headers[] = "Content-Type: application/json"; }
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN";
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(string method, string path, string body = null)
{
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
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.
# Easiest: open https://text-rewriter.skillsafe.ai/tokens.html and press Copy shell export. # It prints a line you can paste straight into your shell: export SKILLSAFE_TOKEN="ssat_..." TOKEN="$SKILLSAFE_TOKEN" # A guest token is enough for /me and /estimate. # Running a rewrite is metered and needs a personal token, which means signing in.
TOKEN = "YOUR_TOKEN" # from https://text-rewriter.skillsafe.ai/tokens.html # Keep it out of source control. Read it from a file or your secret store # rather than pasting it into a committed script. # # A GUEST token is minted for any visitor and can call /me and /estimate. # A PERSONAL token comes from signing in and is what /run requires, # because a rewrite is metered against your balance.
const TOKEN = "YOUR_TOKEN"; // from https://text-rewriter.skillsafe.ai/tokens.html // In a browser on the app's own origin the SDK already holds this and you do // not need to touch it. This guide is for calling from your own code. // // Guest token -> /me and /estimate // Personal token -> /run and /run-stream (metered)
const token = "YOUR_TOKEN" // from https://text-rewriter.skillsafe.ai/tokens.html // Guest tokens cover /me and /estimate. // /run needs a personal token because the rewrite is metered.
static final String TOKEN = "YOUR_TOKEN"; // https://text-rewriter.skillsafe.ai/tokens.html // Guest tokens cover /me and /estimate. // /run needs a personal token because the rewrite is metered.
TOKEN = "YOUR_TOKEN" # from https://text-rewriter.skillsafe.ai/tokens.html # Guest tokens cover /me and /estimate. # /run needs a personal token because the rewrite is metered.
<?php $token = "YOUR_TOKEN"; // from https://text-rewriter.skillsafe.ai/tokens.html // Guest tokens cover /me and /estimate. // /run needs a personal token because the rewrite is metered.
const string Token = "YOUR_TOKEN"; // https://text-rewriter.skillsafe.ai/tokens.html // Guest tokens cover /me and /estimate. // /run needs a personal token because the rewrite is metered.
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.
TOKEN="YOUR_TOKEN" curl -s -X GET "https://api.skillsafe.ai/v1/app-api/me" \ -H "Authorization: Bearer $TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)
res = call("GET", "/me")
print(json.dumps(res["data"], indent=2))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!json.ok) throw new Error(json.error.code + ": " + json.error.message);
return json.data;
}
const data = await call("GET", "/me");
console.log(data);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body []byte) ([]byte, error) {
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
out, err := call("GET", "/me", nil)
if err != nil {
panic(err)
}
var v map[string]any
json.Unmarshal(out, &v)
fmt.Println(v["data"])
}
import java.net.URI;
import java.net.http.*;
public class TextRewriter {
static final String TOKEN = "YOUR_TOKEN";
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.method(method, pub);
if (body != null) b.header("Content-Type", "application/json");
return HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
System.out.println(call("GET", "/me", null));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
def call(method, path, body = nil)
uri = URI(BASE.to_s + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)
end
res = call("GET", "/me")
puts JSON.pretty_generate(res["data"])
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
function call($method, $path, $body = null) {
global $token, $base;
$headers = ["Authorization: Bearer $token"];
if ($body !== null) { $headers[] = "Content-Type: application/json"; }
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
$res = call("GET", "/me");
print_r($res["data"]);
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN";
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(string method, string path, string body = null)
{
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
Console.WriteLine(await Call("GET", "/me"));
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.
TOKEN="YOUR_TOKEN"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"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"
}
}
JSON
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)
payload = {
"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"
}
}
res = call("POST", "/estimate", payload)
print(json.dumps(res["data"], indent=2))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!json.ok) throw new Error(json.error.code + ": " + json.error.message);
return json.data;
}
const payload = {
"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"
}
};
const data = await call("POST", "/estimate", payload);
console.log(data);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body []byte) ([]byte, error) {
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
payload := []byte(`{
"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"
}
}`)
out, err := call("POST", "/estimate", payload)
if err != nil {
panic(err)
}
var v map[string]any
json.Unmarshal(out, &v)
fmt.Println(v["data"])
}
import java.net.URI;
import java.net.http.*;
public class TextRewriter {
static final String TOKEN = "YOUR_TOKEN";
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.method(method, pub);
if (body != null) b.header("Content-Type", "application/json");
return HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"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"
}
}
""";
System.out.println(call("POST", "/estimate", payload));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
def call(method, path, body = nil)
uri = URI(BASE.to_s + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)
end
payload = JSON.parse(<<~JSON)
{
"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"
}
}
JSON
res = call("POST", "/estimate", payload)
puts JSON.pretty_generate(res["data"])
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
function call($method, $path, $body = null) {
global $token, $base;
$headers = ["Authorization: Bearer $token"];
if ($body !== null) { $headers[] = "Content-Type: application/json"; }
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
$payload = json_decode(<<<'JSON'
{
"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"
}
}
JSON, true);
$res = call("POST", "/estimate", $payload);
print_r($res["data"]);
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN";
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(string method, string path, string body = null)
{
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var payload = @"{
""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""
}
}";
Console.WriteLine(await Call("POST", "/estimate", payload));
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.
TOKEN="YOUR_TOKEN"
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"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"
}
}
JSON
import json, urllib.request
TOKEN = "YOUR_TOKEN"
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
if data is not None:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
return json.load(r)
payload = {
"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"
}
}
res = call("POST", "/run", payload)
print(json.dumps(res["data"], indent=2))
const TOKEN = "YOUR_TOKEN";
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!json.ok) throw new Error(json.error.code + ": " + json.error.message);
return json.data;
}
const payload = {
"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"
}
};
const data = await call("POST", "/run", payload);
console.log(data);
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN"
const base = "https://api.skillsafe.ai/v1/app-api"
func call(method, path string, body []byte) ([]byte, error) {
var rdr io.Reader
if body != nil {
rdr = bytes.NewReader(body)
}
req, err := http.NewRequest(method, base+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func main() {
payload := []byte(`{
"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"
}
}`)
out, err := call("POST", "/run", payload)
if err != nil {
panic(err)
}
var v map[string]any
json.Unmarshal(out, &v)
fmt.Println(v["data"])
}
import java.net.URI;
import java.net.http.*;
public class TextRewriter {
static final String TOKEN = "YOUR_TOKEN";
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.method(method, pub);
if (body != null) b.header("Content-Type", "application/json");
return HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"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"
}
}
""";
System.out.println(call("POST", "/run", payload));
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN"
BASE = URI("https://api.skillsafe.ai/v1/app-api")
def call(method, path, body = nil)
uri = URI(BASE.to_s + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }[method]
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)
end
payload = JSON.parse(<<~JSON)
{
"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"
}
}
JSON
res = call("POST", "/run", payload)
puts JSON.pretty_generate(res["data"])
<?php
$token = "YOUR_TOKEN";
$base = "https://api.skillsafe.ai/v1/app-api";
function call($method, $path, $body = null) {
global $token, $base;
$headers = ["Authorization: Bearer $token"];
if ($body !== null) { $headers[] = "Content-Type: application/json"; }
$ch = curl_init($base . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$out = curl_exec($ch);
curl_close($ch);
return json_decode($out, true);
}
$payload = json_decode(<<<'JSON'
{
"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"
}
}
JSON, true);
$res = call("POST", "/run", $payload);
print_r($res["data"]);
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
const string Token = "YOUR_TOKEN";
const string Base = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<string> Call(string method, string path, string body = null)
{
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
var payload = @"{
""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""
}
}";
Console.WriteLine(await Call("POST", "/run", payload));
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.
curl -N -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: text-rewriter:rewrite:$HASH:a1" \
-d @- <<'JSON'
{
"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"
}
}
JSON
# Server-sent events. Each data: line carries a delta of the JSON
# object. Concatenate them, then parse once at the end.
import json, urllib.request
TOKEN = "YOUR_TOKEN"
payload = {
"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"
}
}
req = urllib.request.Request("https://api.skillsafe.ai/v1/app-api/run-stream",
data=json.dumps(payload).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "text-rewriter:rewrite:" + digest + ":a1")
raw = ""
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
raw += json.loads(line[5:].strip()).get("delta", "")
result = json.loads(raw)
print(result["rewrite"])
const payload = {
"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"
}
};
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": `text-rewriter:rewrite:${digest}:a1`
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
raw += (JSON.parse(line.slice(5).trim()).delta || "");
}
}
const result = JSON.parse(raw);
console.log(result.rewrite);
// Read the response body line by line and accumulate the deltas.
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "text-rewriter:rewrite:"+digest+":a1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var raw strings.Builder
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var ev struct{ Delta string `json:"delta"` }
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &ev)
raw.WriteString(ev.Delta)
}
var result map[string]any
json.Unmarshal([]byte(raw.String()), &result)
fmt.Println(result["rewrite"])
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "text-rewriter:rewrite:" + digest + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder raw = new StringBuilder();
HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> raw.append(deltaOf(l.substring(5).trim())));
System.out.println(raw);
uri = URI("https://api.skillsafe.ai/v1/app-api/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "text-rewriter:rewrite:#{digest}:a1"
req.body = JSON.dump(payload)
raw = ""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
raw << (JSON.parse(line[5..].strip)["delta"] || "")
end
end
end
end
puts JSON.parse(raw)["rewrite"]
<?php
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream");
$raw = "";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer $token",
"Content-Type: application/json",
"Idempotency-Key: text-rewriter:rewrite:$digest:a1",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$raw) {
foreach (explode("\n", $chunk) as $line) {
if (strpos($line, "data:") !== 0) { continue; }
$ev = json_decode(trim(substr($line, 5)), true);
$raw .= $ev["delta"] ?? "";
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$result = json_decode($raw, true);
echo $result["rewrite"];
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Content = new StringContent(payload, Encoding.UTF8, "application/json");
req.Headers.Add("Idempotency-Key", $"text-rewriter:rewrite:{digest}:a1");
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var raw = new StringBuilder();
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line is null || !line.StartsWith("data:")) continue;
raw.Append(DeltaOf(line[5..].Trim()));
}
Console.WriteLine(raw.ToString());
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.
# The reply is one JSON object. These two checks are worth running on # every result, because they catch the two ways it can be wrong. # 1. Every move's "before" must occur verbatim in the source. jq -r '.moves[].before' result.json | while read -r span; do grep -qF "$span" source.txt || echo "MOVE SPAN NOT IN SOURCE: $span" done # 2. Every figure in the source should survive into the rewrite. grep -oE '[0-9][0-9,.]*' source.txt | sort -u | while read -r n; do jq -r .rewrite result.json | grep -qF "$n" || echo "FIGURE MISSING: $n" done
import json, re
result = json.loads(raw)
source = payload["source_text"]
# A move whose before-span is not in the source is a fabricated edit.
for m in result["moves"]:
if m["before"] and m["before"] not in source:
print("move span not in source:", m["id"], m["before"])
# Figures are the cheapest fact to verify and the most damaging to lose.
nums = set(re.findall(r"[0-9][0-9,.]*", source))
missing = [n for n in nums if n not in result["rewrite"]]
if missing:
print("figures missing from the rewrite:", missing)
# The full check the web app runs - negations, dates, names, conditions,
# obligation strength - is in reconcile.js and runs client-side.
const result = JSON.parse(raw);
const source = payload.source_text;
// A move whose before-span is not in the source is a fabricated edit.
for (const m of result.moves) {
if (m.before && !source.includes(m.before)) {
console.warn("move span not in source:", m.id, m.before);
}
}
// Figures are the cheapest fact to verify and the most damaging to lose.
const nums = new Set(source.match(/[0-9][0-9,.]*/g) || []);
for (const n of nums) {
if (!result.rewrite.includes(n)) console.warn("figure missing:", n);
}
var result struct {
Rewrite string `json:"rewrite"`
Moves []struct {
ID string `json:"id"`
Before string `json:"before"`
} `json:"moves"`
}
json.Unmarshal([]byte(raw.String()), &result)
for _, m := range result.Moves {
if m.Before != "" && !strings.Contains(source, m.Before) {
fmt.Println("move span not in source:", m.ID, m.Before)
}
}
for _, n := range regexp.MustCompile(`[0-9][0-9,.]*`).FindAllString(source, -1) {
if !strings.Contains(result.Rewrite, n) {
fmt.Println("figure missing:", n)
}
}
// Verify each move points at real text before trusting the move list.
for (Move m : result.moves()) {
if (!m.before().isEmpty() && !source.contains(m.before())) {
System.out.println("move span not in source: " + m.id());
}
}
// And that every figure survived.
Matcher mt = Pattern.compile("[0-9][0-9,.]*").matcher(source);
while (mt.find()) {
if (!result.rewrite().contains(mt.group())) {
System.out.println("figure missing: " + mt.group());
}
}
result = JSON.parse(raw)
source = payload["source_text"]
# A move whose before-span is not in the source is a fabricated edit.
result["moves"].each do |m|
next if m["before"].to_s.empty?
puts "move span not in source: #{m['id']}" unless source.include?(m["before"])
end
# Figures are the cheapest fact to verify.
source.scan(/[0-9][0-9,.]*/).uniq.each do |n|
puts "figure missing: #{n}" unless result["rewrite"].include?(n)
end
<?php
$result = json_decode($raw, true);
$source = $payload["source_text"];
// A move whose before-span is not in the source is a fabricated edit.
foreach ($result["moves"] as $m) {
if ($m["before"] !== "" && strpos($source, $m["before"]) === false) {
echo "move span not in source: {$m['id']}\n";
}
}
// Figures are the cheapest fact to verify.
preg_match_all('/[0-9][0-9,.]*/', $source, $matches);
foreach (array_unique($matches[0]) as $n) {
if (strpos($result["rewrite"], $n) === false) { echo "figure missing: $n\n"; }
}
var result = JsonSerializer.Deserialize<Result>(raw.ToString());
// A move whose before-span is not in the source is a fabricated edit.
foreach (var m in result.Moves)
{
if (!string.IsNullOrEmpty(m.Before) && !source.Contains(m.Before))
Console.WriteLine($"move span not in source: {m.Id}");
}
// And that every figure survived.
foreach (Match n in Regex.Matches(source, @"[0-9][0-9,.]*"))
{
if (!result.Rewrite.Contains(n.Value))
Console.WriteLine($"figure missing: {n.Value}");
}
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.