API Docs

Documents, the core flow

Upload a draft. Poll until the worker finishes. Download the result. Three endpoints, three languages.

The flow

Upload, poll, download

Documents are jobs in our worker queue. The integration is three steps:

  1. POST /api/v1/documents/upload— returns immediately with a job_id and status: pending.
  2. GET /api/v1/documents/{job_id}— poll until status is completed or failed.
  3. 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 of balanced, academic, pastoral, casual, professional, creative, journalistic. Defaults to balanced.
  • domain — one of academic, 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. The progressfield 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 a detail field 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
done

Python

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