The flow
Upload, poll, download
Documents are jobs in our worker queue. The integration is three steps:
POST /api/v1/documents/upload— returns immediately with ajob_idandstatus: pending.GET /api/v1/documents/{job_id}— poll untilstatusiscompletedorfailed.GET /api/v1/documents/{job_id}/download— pull the formatted file back as a binary stream.
Upload is synchronous in that it acknowledges the job; the actual humanization happens on a background worker. Typical jobs complete in 15–90 seconds depending on length.
Step 1
Upload
POST /api/v1/documents/upload takes multipart form-data. The only required field is file; everything else has a sensible default.
file— the document. Accepts.docx,.pdf, and.txt.tone— one ofbalanced,academic,pastoral,casual,professional,creative,journalistic. Defaults tobalanced.domain— one ofacademic,legal,pastoral,medical,marketing,technical. Optional.humanness_level— integer 0–100. Higher values lean further from the source register. Defaults to 50.voice_profile_id— UUID of a voice profile to apply. Optional. Pro and Enterprise plans only.
Response shape (HTTP 201):
{
"id": "doc_01HX...",
"job_id": "abc123",
"status": "pending",
"progress": 0,
"created_at": "2026-05-13T14:22:11Z"
}curl
curl -X POST https://api.inksong.app/api/v1/documents/upload \ -H "X-API-Key: ink_live_YOUR_KEY" \ -F "file=@draft.docx" \ -F "tone=balanced" \ -F "domain=academic" \ -F "humanness_level=60"
Python
import os
import requests
with open("draft.docx", "rb") as fh:
response = requests.post(
"https://api.inksong.app/api/v1/documents/upload",
headers={"X-API-Key": os.environ["INKSONG_API_KEY"]},
files={"file": fh},
data={
"tone": "balanced",
"domain": "academic",
"humanness_level": 60,
},
)
response.raise_for_status()
job = response.json()
print(job["job_id"])JavaScript
import fs from "node:fs";
const form = new FormData();
form.set("file", new Blob([fs.readFileSync("draft.docx")]), "draft.docx");
form.set("tone", "balanced");
form.set("domain", "academic");
form.set("humanness_level", "60");
const response = await fetch("https://api.inksong.app/api/v1/documents/upload", {
method: "POST",
headers: { "X-API-Key": process.env.INKSONG_API_KEY },
body: form,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const job = await response.json();
console.log(job.job_id);One honest note about file formats: PDF input is accepted, but the formatted output is always returned as .docx. PDFs are read for text content; we don’t round-trip the layout.
Step 2
Poll
GET /api/v1/documents/{job_id} returns the full document record. Status moves through four values:
pending— queued, worker hasn’t picked it up yet.processing— a worker is running it. Theprogressfield is populated 0–100 during this phase.completed— ready to download. Humanized text and AI scores are present in the response.failed— the worker hit an unrecoverable error. The body includes adetailfield with the reason.
Response shape when complete:
{
"id": "doc_01HX...",
"job_id": "abc123",
"status": "completed",
"progress": 100,
"original_text": "...",
"humanized_text": "...",
"original_ai_score": 87,
"humanized_ai_score": 12,
"created_at": "2026-05-13T14:22:11Z"
}Poll at most every 2 seconds. Read-side rate limits are 60/min/IP — aggressive polling will trip them.
curl
while true; do
resp=$(curl -s https://api.inksong.app/api/v1/documents/abc123 \
-H "X-API-Key: ink_live_YOUR_KEY")
status=$(echo "$resp" | jq -r .status)
echo "$status"
[[ "$status" == "completed" || "$status" == "failed" ]] && break
sleep 2
donePython
import os
import time
import requests
def wait_for(job_id, timeout=300):
headers = {"X-API-Key": os.environ["INKSONG_API_KEY"]}
deadline = time.time() + timeout
while time.time() < deadline:
resp = requests.get(
f"https://api.inksong.app/api/v1/documents/{job_id}",
headers=headers,
)
resp.raise_for_status()
doc = resp.json()
if doc["status"] in ("completed", "failed"):
return doc
time.sleep(2)
raise TimeoutError(f"job {job_id} did not finish in {timeout}s")
doc = wait_for("abc123")
print(doc["humanized_text"])JavaScript
async function waitFor(jobId, { timeoutMs = 300_000 } = {}) {
const headers = { "X-API-Key": process.env.INKSONG_API_KEY };
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const resp = await fetch(
`https://api.inksong.app/api/v1/documents/${jobId}`,
{ headers },
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const doc = await resp.json();
if (doc.status === "completed" || doc.status === "failed") {
return doc;
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`job ${jobId} timed out`);
}
const doc = await waitFor("abc123");
console.log(doc.humanized_text);Step 3
Download
GET /api/v1/documents/{job_id}/download returns the formatted file as a binary stream. The Content-Disposition header includes a filename derived from the original upload. Output is always .docx, regardless of input format.
curl
curl -L -o humanized.docx \ https://api.inksong.app/api/v1/documents/abc123/download \ -H "X-API-Key: ink_live_YOUR_KEY"
Python
import os
import requests
with requests.get(
"https://api.inksong.app/api/v1/documents/abc123/download",
headers={"X-API-Key": os.environ["INKSONG_API_KEY"]},
stream=True,
) as resp:
resp.raise_for_status()
with open("humanized.docx", "wb") as fh:
for chunk in resp.iter_content(chunk_size=8192):
fh.write(chunk)JavaScript
import fs from "node:fs/promises";
const resp = await fetch(
"https://api.inksong.app/api/v1/documents/abc123/download",
{ headers: { "X-API-Key": process.env.INKSONG_API_KEY } },
);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const buffer = Buffer.from(await resp.arrayBuffer());
await fs.writeFile("humanized.docx", buffer);Listing
Your documents, paginated
GET /api/v1/documents/ returns a paginated list. limit is between 1 and 100 (default 20). offset defaults to 0. Results are ordered by created_at descending.
curl
curl "https://api.inksong.app/api/v1/documents/?limit=20&offset=0" \ -H "X-API-Key: ink_live_YOUR_KEY"
See also
Common patterns
- Tired of polling? Webhooks (planned) will replace this loop with a push from us.
- Hitting 429s? Review rate limits and the
Retry-Aftercontract. - Need to handle failures gracefully? See error response shapes.