POST https://patina.vibetip.help/api/rewrite exposes the hosted rewrite service to both the same-origin playground and cross-origin API clients. Cross-origin requests may use Content-Type and Authorization; responses send Access-Control-Allow-Origin: *. Do not use credentialed CORS requests.
free: no authentication. It uses the server key and is limited per client IP.byok: send your ownprovider,model, andapiKeyin the JSON body. The key is used for that request.pro: send a Polar license key inAuthorization: Bearer <license-key>. The Polar license key is also the API key; purchase it through the playground checkout. Do not send it in the request body.
Free, BYOK, and Pro use the same rewrite pipeline and quality gates. Paid access buys programmatic access and higher limits, not better output.
For a reviewed draft, mode: "verify" checks text against original without
generating a new rewrite. It uses the same authentication, input limits, quota,
number guard and meaning checks. Each verification consumes one request; Pro
also meters its submitted characters. Send baseHash, formatted as
sha256: followed by the lowercase SHA-256 digest of the original UTF-8 text.
Conversation history is not accepted in this mode. A stale hash returns 409
with code source_changed before quota admission or model calls, for both
JSON and NDJSON requests. Text must be well-formed Unicode: unpaired surrogates
are rejected rather than replaced during UTF-8 hashing. Invalid input receives
400; an invalid generated output is refused with output_invalid_unicode.
Optional controls apply to the canonical original (text for a first request,
original for refinement or verification):
| Field | Contract |
|---|---|
protectedSpans |
Up to 20 non-overlapping {start, end} ranges in JavaScript UTF-16 offsets. Ranges must not split a surrogate pair. Protected literals retain their exact spelling, occurrence counts and order. |
includeEdits |
Boolean. On success, return editReview with baseHash, outputHash, offsetEncoding: "utf-16" and edits: [{start, end, replacement}]. Applying every edit reconstructs rewrite exactly. |
baseHash |
Optional source binding for rewriting; required for verify. |
Edits can cover several sentences when alignment is ambiguous. Selecting only
some edits creates a new draft: previous scores and receipts do not verify it.
Submit that exact draft with mode: "verify" before treating it as approved.
Its successful response binds fresh scores and a receipt to those exact bytes.
Protected text is checked before scoring; violations return 422 with
protected_text_failed. Edit review supports up to 20,000 UTF-16 units per
text; a generated output beyond this bound returns edit_output_too_long
when edits were requested. Source text and protected literals are not added to
analytics or persisted by these controls.
Set Content-Type: application/json and send this body:
{
"mode": "first",
"lang": "en",
"tier": "pro",
"text": "This is the text to rewrite.",
"documentType": "blog",
"persona": "blog-essay",
"register": "professional"
}Required fields:
| Field | Values |
|---|---|
mode |
first, refine, or verify |
lang |
ko, en, zh, or ja |
tier |
free, byok, or pro |
text |
Non-empty string |
Optional style fields are documentType, persona, and register; edit controls are described above. documentType defaults to default; valid values are default, blog, academic, technical, formal, social, email, legal, medical, marketing, narrative, instructional, casual-conversation, code-comment, commit-message, release-notes, and namuwiki (namuwiki is Korean-only). register is casual or professional. A persona must be one offered for the selected language.
For mode: "refine", original is required and must be the original source text. history is optional; it is an array of { "role": "user" | "assistant", "content": "..." } turns. The server retains at most 6 recent turns and 12 KiB of history text. BYOK additionally requires an allowed provider, model, and non-empty apiKey; free and Pro reject a body apiKey.
These are the current server-enforced defaults; Pro values may be reduced or raised by deployment configuration.
| Tier | Text/original maximum | Concurrency | Request quota | Other quota |
|---|---|---|---|---|
| Free | 4,000 characters | 1 | 20/day; 10/hour burst | — |
| BYOK | 20,000 characters | 2/IP | Provider account quota applies; 120/hour burst; 480/day | Admission caps bound Patina function, connection, and egress usage, not your provider cost |
| Pro | 20,000 characters | 3 | 200/day; 100/month | 50,000 characters/month |
The meaning-preservation gates require MPS and fidelity scores of at least 70.
When Accept is absent, */*, or does not request JSON, the response is 200 application/x-ndjson. Each line is one JSON object. A successful response is start, zero or more delta frames, then done.
{"type":"start"}
{"type":"delta","text":"Here is a clearer version"}
{"type":"delta","text":" of the sentence."}
{"type":"done","rewrite":"Here is a clearer version of the sentence.","mps":{"mps":96},"fidelity":{"fidelity":94},"signals":{"before":{"overall":72},"after":{"overall":18}},"diff":{"beforeChars":39,"afterChars":48},"receipt":{"schemaVersion":"patina-rewrite-receipt-v2","receiptHash":"sha256:..."}}
mps, fidelity, signals, diff, and receipt are emitted on done. The score values are mps.mps and fidelity.fidelity; signals.before.overall and signals.after.overall are the deterministic signal scores.
delta frames are provisional display text, not an accepted rewrite. They are emitted from the first rewrite attempt before number-safety, MPS, and fidelity gates finish. API consumers must treat only done.rewrite as authoritative, replace any accumulated deltas with it, and discard provisional text when the terminal frame is error.
A streaming terminal error is also an NDJSON frame, for example:
{"type":"error","code":"stream_failed","error":"upstream request failed"}
{"type":"error","code":"floor_failed","failed":["mps"],"rewrite":"...","mps":{"mps":65},"fidelity":{"fidelity":93},"signals":{"before":{"overall":72},"after":{"overall":18}},"diff":{"beforeChars":39,"afterChars":48}}
Other terminal stream codes are number_safety_failed and scoring_failed.
Send Accept: application/json to buffer the same pipeline result and receive one 200 application/json object instead of NDJSON:
{
"ok": true,
"rewrite": "Here is a clearer version of the sentence.",
"mps": { "mps": 96 },
"fidelity": { "fidelity": 94 },
"signals": { "before": { "overall": 72 }, "after": { "overall": 18 } },
"diff": { "beforeChars": 39, "afterChars": 48 },
"receipt": { "schemaVersion": "patina-rewrite-receipt-v2", "receiptHash": "sha256:..." }
}Terminal failures in JSON mode return { "ok": false, "code": "...", "error": "..." } with the runner's semantics: safety-gate refusals (floor_failed, number_safety_failed) use 422 — the text was processed and deliberately rejected to protect meaning and exact numbers — while upstream stream/scoring failures use 500.
Errors are JSON objects, including for JSON-mode callers:
{ "error": "hourly burst exceeded" }Validation errors use 400; an over-limit text or refine original uses 413. A missing, malformed, or duplicated Pro Authorization header uses 401 (pro license required); a well-formed license key that does not entitle uses 403 (license not entitled). Quota and concurrency denials use 429; quota/entitlement infrastructure or service unavailability uses 503. JSON-mode terminal failures use 422 (safety-gate refusal) or 500 (upstream failure) as described above.
Possible quota error strings include daily quota exceeded, hourly burst exceeded, concurrent limit exceeded, monthly rewrite limit reached, and monthly character limit reached. A Pro monthly-character denial additionally includes remainingMonthlyChars and limitMonthlyChars.
Streaming Pro request:
curl --no-buffer https://patina.vibetip.help/api/rewrite \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PATINA_LICENSE_KEY" \
--data '{"mode":"first","lang":"en","tier":"pro","text":"This is the text to rewrite."}'Single JSON response:
curl https://patina.vibetip.help/api/rewrite \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $PATINA_LICENSE_KEY" \
--data '{"mode":"first","lang":"en","tier":"pro","text":"This is the text to rewrite."}'Node.js streaming and JSON clients:
const url = 'https://patina.vibetip.help/api/rewrite';
const body = { mode: 'first', lang: 'en', tier: 'pro', text: 'This is the text to rewrite.' };
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.PATINA_LICENSE_KEY}`,
};
const streamResponse = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
let pending = '';
for await (const chunk of streamResponse.body.pipeThrough(new TextDecoderStream())) {
pending += chunk;
const lines = pending.split('\n');
pending = lines.pop();
for (const line of lines) console.log(JSON.parse(line));
}
const jsonResponse = await fetch(url, {
method: 'POST', headers: { ...headers, Accept: 'application/json' }, body: JSON.stringify(body),
});
console.log(await jsonResponse.json());Python JSON client:
import os
import requests
response = requests.post(
'https://patina.vibetip.help/api/rewrite',
headers={
'Accept': 'application/json',
'Authorization': f"Bearer {os.environ['PATINA_LICENSE_KEY']}",
},
json={'mode': 'first', 'lang': 'en', 'tier': 'pro', 'text': 'This is the text to rewrite.'},
)
response.raise_for_status()
print(response.json())This document describes the current pre-GA path, /api/rewrite. Before the paid API reaches general availability, Patina will expose an explicit protocol version (/api/v1/rewrite or an equivalent version field) so breaking changes cannot silently alter existing clients.
Limits and score floors above are sourced from src/web-rewrite-contract.js: TIER_LIMITS, CONTEXT_LIMITS, MPS_FLOOR, FIDELITY_FLOOR, SUPPORTED_LANGS, WEB_DOCUMENT_TYPES, WEB_REGISTERS, and QUOTA_REASONS. Request validation and response behavior are implemented by validateRewriteRequest in that module and api/rewrite.js.