Watermarks Remover API — drive the invisible-character scanner from your own code
Watermarks Remover finds and removes hidden Unicode in text — zero-width spaces, bidirectional overrides, tag characters, private-use codepoints and homoglyph spaces. This page documents the HTTP API; the FAQ and the reference of what it detects are on the main page.
Everything the page does is available over HTTP. The character scan and the style score run in the browser and are not part of this API — what you call here is the model lane, so send your own findings with the request.
Base URL and envelope
Every call goes to https://api.skillsafe.ai/v1/app-api and carries
Authorization: Bearer <token>. Successful responses wrap the payload in
data; failures carry error.
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "INSUFFICIENT_CREDITS", "message": "...", "details": { ... } } }
Errors
| Status | Code | What it means |
|---|---|---|
| 400 | VALIDATION_ERROR | The body was not valid JSON, or a required field was the wrong type. |
| 401 | UNAUTHORIZED | Missing or expired token. Mint a new one - see step 1. |
| 402 | INSUFFICIENT_CREDITS | Not enough credits. If reason is sponsor_exhausted the publisher's daily allowance ran out and a guest should sign in; otherwise top up. |
| 403 | FORBIDDEN | A guest called an endpoint that requires an account - the document converter, for one. |
| 404 | NOT_FOUND | Unknown job id, or a collection this release does not declare. |
| 413 | INVALID_REQUEST | Body over 1 MB, or an uploaded document over 8 MB. |
| 429 | RATE_LIMITED | Too many calls. Back off and retry. |
| 503 | UNAVAILABLE | The model lane is temporarily unavailable. Holds are refunded. |
1. Get a token
Open /tokens.html in a browser signed in to SkillSafe, reveal the token and copy it. That page reads the very token this app uses, so you never have to open a storage inspector. A guest token is minted automatically and is enough to run both lanes while the publisher's daily sponsorship lasts; a personal token is needed once that allowance is used up, and always for the document converter.
Paste the token into the YOUR_TOKEN placeholder in the samples below, or read it
from wherever your project keeps its secrets. Do not commit it.
2. Who am I
Confirms the token works and reports the balance.
curl -sS 'https://api.skillsafe.ai/v1/app-api/me' \ -H 'Authorization: Bearer YOUR_TOKEN'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="GET")
with urllib.request.urlopen(req) as res:
print(json.load(res)["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
method: "GET",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
});
const { data } = await res.json();
console.log(data);package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
token := "YOUR_TOKEN"
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = "YOUR_TOKEN";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'json'
require 'net/http'
token = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await http.GetAsync("https://api.skillsafe.ai/v1/app-api/me");
Console.WriteLine(await res.Content.ReadAsStringAsync());Returns
{"subject_type": "user" | "guest", "subject_id": "...", "credits": 0}.
3. Price a run before making it
Free, no job created, and guests may call it. hold_credits is what gets
reserved, never what you are charged — settlement recomputes from actual usage and
refunds the rest. sponsor_enabled: true means the publisher is paying and the run is
free to you.
curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/estimate' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase '\''in today'\''s fast-paced world/landscape'\'' (1x)"
]
},
"text": "In today'\''s fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}'import json, urllib.request
TOKEN = "YOUR_TOKEN"
payload = {
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/estimate",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as res:
print(json.load(res)["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}),
});
const { data } = await res.json();
console.log(data);package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
token := "YOUR_TOKEN"
body := []byte(`{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = "YOUR_TOKEN";
String body = """
{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.post(HttpRequest.BodyPublishers.ofString(body))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'json'
require 'net/http'
token = "YOUR_TOKEN"
payload = {
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
uri = URI("https://api.skillsafe.ai/v1/app-api/estimate")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = "YOUR_TOKEN";
$payload = json_decode(<<<JSON
{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
JSON, true);
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{
""task"": ""explain"",
""source"": {
""name"": ""quarterly-update.md"",
""format"": ""md"",
""fidelity"": ""exact""
},
""findings"": {
""length"": 1246,
""suspicious_total"": 13,
""kinds"": [
{
""kind"": ""zwj_family"",
""label"": ""Zero-width"",
""count"": 4,
""distinct"": 3,
""confidence"": ""probable""
}
],
""hits"": [
{
""codepoint"": ""U+200B"",
""name"": ""ZERO WIDTH SPACE"",
""category"": ""Cf"",
""kind"": ""zwj_family"",
""confidence"": ""probable"",
""count"": 2,
""sample_offsets"": [
128,
253
]
},
{
""codepoint"": ""U+202E"",
""name"": ""RIGHT-TO-LEFT OVERRIDE"",
""category"": ""Cf"",
""kind"": ""bidi"",
""confidence"": ""probable"",
""count"": 1,
""sample_offsets"": [
1064
]
}
]
},
""style"": {
""score"": 0.69,
""level"": ""MEDIUM"",
""status"": ""ok"",
""word_count"": 214,
""sentence_count"": 14,
""burstiness_cv"": 0.281,
""lexical_diversity"": 0.723,
""ai_ngram_density"": 1.87,
""findings"": [
""AI cadence phrase 'in today's fast-paced world/landscape' (1x)""
]
},
""text"": ""In today's fast-paced world, organisations must delve into ..."",
""clean"": {
""removed_count"": 11,
""replaced_count"": 2
}
}", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());Returns {"hold_credits", "min_credits", "model",
"model_alias", "markup_bps", "sponsor_enabled"}. Holds differ per lane, so re-estimate when
you switch task.
4. Run and poll
POST /run returns a job id immediately. Send an Idempotency-Key header
so a retry after a dropped connection returns the original job instead of billing twice — a
hash of the input plus an attempt counter works well.
curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/run' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase '\''in today'\''s fast-paced world/landscape'\'' (1x)"
]
},
"text": "In today'\''s fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}'import json, urllib.request
TOKEN = "YOUR_TOKEN"
payload = {
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/run",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as res:
print(json.load(res)["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}),
});
const { data } = await res.json();
console.log(data);package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
token := "YOUR_TOKEN"
body := []byte(`{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = "YOUR_TOKEN";
String body = """
{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.post(HttpRequest.BodyPublishers.ofString(body))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'json'
require 'net/http'
token = "YOUR_TOKEN"
payload = {
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
uri = URI("https://api.skillsafe.ai/v1/app-api/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = "YOUR_TOKEN";
$payload = json_decode(<<<JSON
{
"task": "explain",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [
{
"kind": "zwj_family",
"label": "Zero-width",
"count": 4,
"distinct": 3,
"confidence": "probable"
}
],
"hits": [
{
"codepoint": "U+200B",
"name": "ZERO WIDTH SPACE",
"category": "Cf",
"kind": "zwj_family",
"confidence": "probable",
"count": 2,
"sample_offsets": [
128,
253
]
},
{
"codepoint": "U+202E",
"name": "RIGHT-TO-LEFT OVERRIDE",
"category": "Cf",
"kind": "bidi",
"confidence": "probable",
"count": 1,
"sample_offsets": [
1064
]
}
]
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": [
"AI cadence phrase 'in today's fast-paced world/landscape' (1x)"
]
},
"text": "In today's fast-paced world, organisations must delve into ...",
"clean": {
"removed_count": 11,
"replaced_count": 2
}
}
JSON, true);
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{
""task"": ""explain"",
""source"": {
""name"": ""quarterly-update.md"",
""format"": ""md"",
""fidelity"": ""exact""
},
""findings"": {
""length"": 1246,
""suspicious_total"": 13,
""kinds"": [
{
""kind"": ""zwj_family"",
""label"": ""Zero-width"",
""count"": 4,
""distinct"": 3,
""confidence"": ""probable""
}
],
""hits"": [
{
""codepoint"": ""U+200B"",
""name"": ""ZERO WIDTH SPACE"",
""category"": ""Cf"",
""kind"": ""zwj_family"",
""confidence"": ""probable"",
""count"": 2,
""sample_offsets"": [
128,
253
]
},
{
""codepoint"": ""U+202E"",
""name"": ""RIGHT-TO-LEFT OVERRIDE"",
""category"": ""Cf"",
""kind"": ""bidi"",
""confidence"": ""probable"",
""count"": 1,
""sample_offsets"": [
1064
]
}
]
},
""style"": {
""score"": 0.69,
""level"": ""MEDIUM"",
""status"": ""ok"",
""word_count"": 214,
""sentence_count"": 14,
""burstiness_cv"": 0.281,
""lexical_diversity"": 0.723,
""ai_ngram_density"": 1.87,
""findings"": [
""AI cadence phrase 'in today's fast-paced world/landscape' (1x)""
]
},
""text"": ""In today's fast-paced world, organisations must delve into ..."",
""clean"": {
""removed_count"": 11,
""replaced_count"": 2
}
}", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());Then poll until the job is terminal:
curl -sS 'https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789' \ -H 'Authorization: Bearer YOUR_TOKEN'
import json, urllib.request
TOKEN = "YOUR_TOKEN"
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789",
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="GET")
with urllib.request.urlopen(req) as res:
print(json.load(res)["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789", {
method: "GET",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
});
const { data } = await res.json();
console.log(data);package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
token := "YOUR_TOKEN"
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789", nil)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = "YOUR_TOKEN";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'json'
require 'net/http'
token = "YOUR_TOKEN"
uri = URI("https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
req["Content-Type"] = "application/json"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = "YOUR_TOKEN";
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var res = await http.GetAsync("https://api.skillsafe.ai/v1/app-api/jobs/job_0123456789");
Console.WriteLine(await res.Content.ReadAsStringAsync());
status is queued, running, succeeded or
failed. price_credits is the hold; charged_credits is the
real cost. Build your pricing display on the second one.
5. Stream instead
POST /run-stream takes the same body and returns Server-Sent Events: a
job event, then delta events carrying {"text"}, then
done or error.
Not usable on this app's model right now. On gemma-fast the stream
emits the job event and then error: "Run timed out" with no
delta events at all, while the identical body through /run succeeds in
about thirty seconds and settles normally. The same request streams correctly with a
"$model": "gpt-terra" override, so the fault is in the Workers AI streaming path, not
in the request. Verified live on 2026-08-17. The page therefore uses /run plus
polling, and you should too until this is fixed.
curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/run-stream' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today'\''s fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}'import json, urllib.request
TOKEN = "YOUR_TOKEN"
payload = {
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/run-stream",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as res:
print(json.load(res)["data"])const TOKEN = "YOUR_TOKEN";
const res = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream", {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}),
});
const { data } = await res.json();
console.log(data);package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
token := "YOUR_TOKEN"
body := []byte(`{
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}`)
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}import java.net.URI;
import java.net.http.*;
var token = "YOUR_TOKEN";
String body = """
{
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}
""";
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.post(HttpRequest.BodyPublishers.ofString(body))
.build();
var res = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());require 'json'
require 'net/http'
token = "YOUR_TOKEN"
payload = {
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}
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.body = JSON.generate(payload)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]<?php
$token = "YOUR_TOKEN";
$payload = json_decode(<<<JSON
{
"task": "rewrite",
"source": {
"name": "quarterly-update.md",
"format": "md",
"fidelity": "exact"
},
"findings": {
"length": 1246,
"suspicious_total": 13,
"kinds": [],
"hits": []
},
"style": {
"score": 0.69,
"level": "MEDIUM",
"status": "ok",
"word_count": 214,
"sentence_count": 14,
"burstiness_cv": 0.281,
"lexical_diversity": 0.723,
"ai_ngram_density": 1.87,
"findings": []
},
"strength": "humanize",
"blocks": [
{
"id": 1,
"text": "In today's fast-paced world, organisations must delve into ..."
},
{
"id": 4,
"text": "In conclusion, navigating the intricacies of this rich tapestry ..."
}
],
"selection": {
"selected": 2,
"total": 5,
"chars": 895
}
}
JSON, true);
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$res = json_decode(curl_exec($ch), true);
print_r($res["data"]);using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = new StringContent(@"{
""task"": ""rewrite"",
""source"": {
""name"": ""quarterly-update.md"",
""format"": ""md"",
""fidelity"": ""exact""
},
""findings"": {
""length"": 1246,
""suspicious_total"": 13,
""kinds"": [],
""hits"": []
},
""style"": {
""score"": 0.69,
""level"": ""MEDIUM"",
""status"": ""ok"",
""word_count"": 214,
""sentence_count"": 14,
""burstiness_cv"": 0.281,
""lexical_diversity"": 0.723,
""ai_ngram_density"": 1.87,
""findings"": []
},
""strength"": ""humanize"",
""blocks"": [
{
""id"": 1,
""text"": ""In today's fast-paced world, organisations must delve into ...""
},
{
""id"": 4,
""text"": ""In conclusion, navigating the intricacies of this rich tapestry ...""
}
],
""selection"": {
""selected"": 2,
""total"": 5,
""chars"": 895
}
}", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());6. The lanes
This app has one prompt and one model, and routes on the task field. Send it on every
request. If it is missing the model picks the closer lane and names its choice, which is a
fallback rather than a feature — do not rely on it.
Fields common to both lanes
| Field | Type | Meaning |
|---|---|---|
| task | string | "explain" or "rewrite" |
| source | object | {name, format, fidelity}. fidelity is "exact" for bytes read directly, "converted" for text rebuilt from a PDF or .docx — in which case carriers may have been normalized away before the scan ran |
| findings | object | Your Layer A result: {length, suspicious_total, kinds[], hits[]}. Each hit is {codepoint, name, category, kind, confidence, count, sample_offsets}. These are treated as facts the model may not contradict |
| style | object | {score, level, status, word_count, sentence_count, burstiness_cv, lexical_diversity, ai_ngram_density, findings[]} |
task: explain
Extra fields: text (an excerpt, for context only) and optionally
clean = {removed_count, replaced_count}.
Output is three sections, in this order:
## VERDICT Two or three sentences: what kind of contamination this is, whether it looks deliberate, and which single finding deserves attention first. ## RISK Two or three short bullets: what follows, and the limit that matters most. ## NEXT Two or three short bullets: what to do, in order.
The model is deliberately not asked to list the carriers. Your own scan already has
every codepoint, name, class, count and offset — rendering that is your job, not the model's. It is
also the difference between a run that completes and one that does not: a ~700-character reply
finishes in about 35 seconds, while adding a per-codepoint enumeration pushes it past 1,200
characters and the run is killed at the 60-second wall clock. Measured 2026-08-17 on
gemma-fast; the failure surfaces as an unclassified
"Run failed — the platform could not complete this request" rather than as a timeout.
Reconcile what does come back: any U+XXXX the reply names that is not in your
findings.hits is invented, and the page says so above the result.
task: rewrite
Extra fields: strength ("humanize", "paraphrase" or
"code"), blocks = [{id, text}],
selection = {selected, total, chars} for the whole document, and
batch = {index, of, blocks_in_batch}.
Send long documents in batches. A rewrite returns about as much text as it is
given, and the run is killed at sixty seconds — which on this model is roughly 700 characters of
output. One request cannot rewrite a long document, and an overrun loses the whole reply rather
than truncating it. The page therefore groups the selected paragraphs into batches of ~900
characters, submits one job per batch, and renders each batch as it lands. Do the same: pack
paragraphs to a character budget, keep a paragraph whole even if it exceeds the budget, and give
each batch its own Idempotency-Key so a retry cannot double-bill.
Because the model cannot stream (see the note in step 5), a completed batch is the finest-grained progress signal available. Rendering per batch is what makes a long run feel live — and it means a failed batch costs you one batch, not the whole document. Failed batches refund their hold; leave those paragraphs as they were and say so.
Output:
[[BLOCK 1]] The rewritten paragraph, plain text. [[BLOCK 4]] The next rewritten paragraph. [[NOTES]] Two or three sentences on what changed.
Ids are echoed back from your request, so splice each block over the range it came from — work backwards from the end of the document so earlier offsets stay valid. A block that does not come back has not been rewritten: show the original and say so rather than silently substituting it. Gemma 4 caps output at 4,096 tokens, which is why the page sends at most six paragraphs and around 8,000 characters per run.
What to send, and what to keep to yourself
The whole point of computing findings and style locally is that the
model never needs your full document. The explain lane takes an excerpt for context;
the rewrite lane takes only the paragraphs you chose to have rewritten. Everything
else stays in your process.
Doing the scan yourself
The two engines are plain, dependency-free JavaScript in this bundle:
/unicode.js exposes
WmUnicode.inspect(text, opts) and WmUnicode.clean(text, opts), and
/stylometry.js exposes WmStylometry.score(text),
scoreParagraphs(text) and selectForRewrite(paragraphs, opts). They have
no dependencies and no network access, so they run anywhere a browser or Node does.
Both are independent re-implementations of the text layers of github.com/guillaumemeyer/watermarks-remover (MIT, Guillaume Meyer). If you want the file-level, image and C2PA work, or the research harnesses for scheme-specific detection, go to the upstream project — none of that is in scope here.