Python

A requests-based client with retries, idempotent submits and a wait helper. Works in a script, a Django or FastAPI service, or a notebook.

The client

silvertext.py
import os
import random
import time
from typing import Any, Optional

import requests

FINAL = {"done", "rejected", "failed", "cancelled"}


class SilvertextError(Exception):
    def __init__(self, status: int, code: str, message: str):
        super().__init__(message)
        self.status = status
        self.code = code


class Silvertext:
    def __init__(self, key: Optional[str] = None, base: str = "https://api.silvertext.com"):
        self.key = key or os.environ["SILVERTEXT_API_KEY"]
        self.base = base
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {self.key}"

    def request(self, method: str, path: str, attempts: int = 4, **kwargs) -> Any:
        delay = 1.0
        for attempt in range(1, attempts + 1):
            try:
                res = self.session.request(method, self.base + path, timeout=60, **kwargs)
            except requests.RequestException:
                if attempt >= attempts:
                    raise
                res = None
            if res is not None and res.ok:
                return res.json() if res.content else None
            retryable = res is None or res.status_code == 429 or res.status_code >= 500
            if not retryable or attempt >= attempts:
                body = res.json() if res is not None and res.content else {}
                err = body.get("error", {})
                raise SilvertextError(res.status_code if res is not None else 0, err.get("code", "network"), err.get("message", "request failed"))
            retry_after = float(res.headers.get("Retry-After", 0)) if res is not None else 0
            time.sleep(retry_after if retry_after > 0 else delay + random.random() * 0.5)
            delay = min(delay * 2, 30)

    def create_check(self, body: dict, idempotency_key: Optional[str] = None) -> str:
        headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
        return self.request("POST", "/v1/checks", json=body, headers=headers)["id"]

    def create_check_from_file(self, path: str, options: Optional[dict] = None, title: Optional[str] = None) -> str:
        import json
        with open(path, "rb") as f:
            data = {"options": json.dumps(options or {})}
            if title:
                data["title"] = title
            return self.request("POST", "/v1/checks", files={"file": (os.path.basename(path), f)}, data=data)["id"]

    def get_check(self, check_id: str, view: Optional[str] = None, include: Optional[list] = None) -> dict:
        params = {"include": ",".join(include)} if include else ({"view": view} if view else {})
        return self.request("GET", f"/v1/checks/{check_id}", params=params)

    def cancel_check(self, check_id: str) -> None:
        self.request("POST", f"/v1/checks/{check_id}/cancel")

    def delete_check(self, check_id: str) -> None:
        self.request("DELETE", f"/v1/checks/{check_id}")

    def usage(self) -> dict:
        return self.request("GET", "/v1/me/usage")

    def wait_for_check(self, check_id: str, deadline_s: float = 600) -> dict:
        started = time.monotonic()
        delay = 3.0
        while True:
            check = self.get_check(check_id, view="summary")
            if check["status"] in FINAL:
                return check
            if time.monotonic() - started > deadline_s:
                try:
                    self.cancel_check(check_id)
                finally:
                    raise TimeoutError(f"check {check_id} did not finish in time")
            time.sleep(delay)
            if time.monotonic() - started > 60:
                delay = min(delay + 2, 10)

Using it

from silvertext import Silvertext

st = Silvertext()

check_id = st.create_check(
    {"title": "Q3 launch post", "text": draft, "options": {"citations": False}},
    idempotency_key=f"post:{post_id}:v{version}",
)
summary = st.wait_for_check(check_id)
if summary["status"] != "done":
    raise RuntimeError((summary.get("rejection") or {}).get("message", summary["status"]))

report = st.get_check(check_id, include=["issues", "sources"])["report"]
grammar = [i for i in report["issues"] if i["category"] == "grammar"]

# A file, as a multipart upload
file_id = st.create_check_from_file("thesis-draft.docx", options={"plagiarism": True, "aiContent": True})

Applying fixes to the text

Offsets in issue["original"] index the checked text. Apply replacements from the end backwards so earlier offsets stay valid:

def apply_fixes(text: str, issues: list) -> str:
    fixable = [i for i in issues if i.get("fix") and "replacement" in i["fix"]]
    for issue in sorted(fixable, key=lambda i: i["original"]["start"], reverse=True):
        s, e = issue["original"]["start"], issue["original"]["end"]
        text = text[:s] + issue["fix"]["replacement"] + text[e:]
    return text

On this page