Developers
API documentation
Rewrite text programmatically with a small, predictable REST API. Same models, same per-task billing.
Introduction
Rewrites are asynchronous: you create a job, then poll it until it finishes. Most jobs complete within a few seconds; the first job after an idle period may take up to a minute while a GPU worker starts.
Step 1
POST /rewrites
Credits are reserved
Step 2
GET /rewrites/{id}
Poll with backoff
Step 3
status: completed
Read job.output
Authentication
Create a key under Dashboard → API (requires a verified email) and send it as a Bearer token. Keys are shown once and stored only as a SHA-256 hash — rotate them any time.
Authorization: Bearer rw_live_••••••••••••••••••••••••
Treat API keys like passwords. Never embed them in client-side code or public repositories.
Quick start
# 1. Submit a rewrite
curl -X POST https://ghostverb.com/api/v1/rewrites \
-H "Authorization: Bearer $GHOSTVERB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Your text here…", "style": "voice", "intensity": "balanced"}'
# 2. Poll until status is completed / failed / cancelled
curl https://ghostverb.com/api/v1/rewrites/JOB_ID \
-H "Authorization: Bearer $GHOSTVERB_API_KEY"
import os, time, requests
API = "https://ghostverb.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['GHOSTVERB_API_KEY']}"}
job = requests.post(f"{API}/rewrites", headers=HEADERS, json={
"text": "Your text here…",
"style": "voice",
"intensity": "balanced",
}, timeout=30).json()["job"]
delay = 2
while job["status"] not in ("completed", "failed", "cancelled"):
time.sleep(delay)
delay = min(delay * 1.5, 10)
job = requests.get(f"{API}/rewrites/{job['id']}", headers=HEADERS, timeout=30).json()["job"]
print(job["output"] if job["status"] == "completed" else job["error"])
const API = 'https://ghostverb.com/api/v1';
const headers = { Authorization: `Bearer ${process.env.GHOSTVERB_API_KEY}`, 'Content-Type': 'application/json' };
let { job } = await fetch(`${API}/rewrites`, {
method: 'POST',
headers,
body: JSON.stringify({ text: 'Your text here…', style: 'voice', intensity: 'balanced' }),
}).then((r) => r.json());
let delay = 2000;
while (!['completed', 'failed', 'cancelled'].includes(job.status)) {
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.5, 10000);
({ job } = await fetch(`${API}/rewrites/${job.id}`, { headers }).then((r) => r.json()));
}
console.log(job.status === 'completed' ? job.output : job.error);
$api = 'https://ghostverb.com/api/v1';
$headers = ['Authorization: Bearer ' . getenv('GHOSTVERB_API_KEY'), 'Content-Type: application/json'];
$request = function (string $method, string $url, ?array $body = null) use ($headers): array {
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
return json_decode(curl_exec($ch), true);
};
$job = $request('POST', "$api/rewrites", ['text' => 'Your text here…', 'style' => 'voice'])['job'];
while (!in_array($job['status'], ['completed', 'failed', 'cancelled'], true)) {
sleep(3);
$job = $request('GET', "$api/rewrites/{$job['id']}")['job'];
}
echo $job['output'] ?? $job['error'];
Endpoints
/api/v1/rewrites
— Create a rewrite
Validates the text, charges credits and queues the job. Returns 202 with the job object.
| text | string | required | 5–5,000 words |
| style | string | optional | voice · natural · formal · casual · academic · simple · creative (default voice) |
| intensity | string | optional | light · balanced · heavy (default balanced) |
/api/v1/rewrites/{id}
— Retrieve a rewrite
Returns the job with its latest status. output is set once status is completed.
/api/v1/rewrites
— List rewrites
Paginated list of your jobs, newest first (without text).
| page | integer | optional | Page number, default 1 |
| per_page | integer | optional | 1–100, default 20 |
/api/v1/quote
— Get a quote
Counts words and returns the credit cost without charging anything.
| text | string | required | The text to price |
/api/v1/me
— Account & balance
Your email, current credit balance and pricing parameters.
The job object
{
"job": {
"id": "6f1c2a1e-9d4b-4c55-8a3e-2f0b8c1d7e90",
"status": "completed",
"style": "voice",
"intensity": "balanced",
"word_count": 412,
"output_word_count": 398,
"credits_charged": 5,
"execution_ms": 4210,
"error": null,
"created_at": "2026-10-06 09:24:41",
"completed_at": "2026-10-06 09:24:46",
"output": "The rewritten text…"
}
}
- status
- pending → queued → processing → completed | failed | cancelled
- credits_charged
- Credits reserved for the job. Refunded automatically when status is failed or cancelled.
- output
- The rewritten text. Only present when status is completed.
- execution_ms
- GPU processing time for the job.
- error
- Human-readable reason when a job fails.
Errors & limits
Errors return a JSON body like {"error": "message", "code": "…"}.
| Status | Meaning |
|---|---|
| 401 | Missing or invalid API key |
| 402 | Insufficient credits (code insufficient_credits, includes required and balance) |
| 403 | Email not verified (code email_unverified) |
| 404 | Job not found or not yours |
| 422 | Validation error — too short, too long or missing text |
| 429 | Rate limit exceeded — retry after the Retry-After header |
| 503 | Rewrite engine temporarily unavailable — credits were refunded |
Rate limit: 60 requests per minute per key. Maximum 5,000 words per job.
Best practices
- Poll with exponential backoff (start at 2s, cap at 10s) instead of tight loops.
- Use POST /quote to show users the cost before submitting.
- Split very long documents at paragraph boundaries into multiple jobs.
- Store API keys in environment variables or a secrets manager, and rotate them if exposed.
- Handle 402 by prompting the user to top up, and 503 by retrying later — credits are never lost.