# Study Solutions Humanizer API

Rewrites AI-generated text so it reads as human-written. REST + JSON over HTTPS, paid from a prepaid balance.

- Base URL: `https://www.studysolutions.app/api/v1`
- Auth: `Authorization: Bearer <key>` (`x-api-key: <key>` also works). Keys start with `ss_live_`. One key per account: create or regenerate it at https://www.studysolutions.app/developers. It is shown once; regenerating makes the old key stop working immediately.
- Call the API from server code. A key in browser or app code can be copied by anyone who loads it.
- Price: $0.05 per 1,000 words of input, charged when the request starts. Runs that fail or time out are refunded in full. Top up $10–$500 at a time; the balance never expires.
- Limits: 25,000 words per request; 3 requests in flight per account (the next one gets 429 with `Retry-After`). A run gets up to 280 s: set your client timeout to 300 s.

## POST /humanize

Body (JSON):

| Field | Type | Required | Notes |
|---|---|---|---|
| `text` | string | yes | Up to 25,000 words. Formatting (headings, lists, blank lines) is kept. The reply comes back in the language you send. |
| `model` | string | no | `"studysolutions"` (default). |
| `stream` | boolean | no | `false` (default): one JSON response when the whole document is done. `true`: NDJSON events as paragraphs finish (see Streaming). |

Response `200`:

```json
{
  "id": "req_8f2c1e0b9d4a4c7f8e3b2a1d0c9e8f7a",
  "object": "humanization",
  "model": "studysolutions",
  "output": "AI has reshaped how students learn…",
  "usage": {
    "input_words": 412,
    "output_words": 405,
    "cost_usd": 0.0206
  },
  "balance_usd": 9.9794,
  "latency_ms": 3840
}
```

`id` is also in the `X-Request-Id` header; quote it to support. `usage.cost_usd` is what this request cost; `balance_usd` is what is left.

cURL:

```bash
curl https://www.studysolutions.app/api/v1/humanize \
  -H "Authorization: Bearer $SS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Your AI-generated text goes here."}'
```

JavaScript:

```javascript
const res = await fetch("https://www.studysolutions.app/api/v1/humanize", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ text: "Your AI-generated text goes here." }),
});

const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data.output);
```

Python:

```python
import os, requests

res = requests.post(
    "https://www.studysolutions.app/api/v1/humanize",
    headers={"Authorization": f"Bearer {os.environ['SS_API_KEY']}"},
    json={"text": "Your AI-generated text goes here."},
    timeout=300,
)
res.raise_for_status()
print(res.json()["output"])
```

## Streaming

With `"stream": true` the reply is `application/x-ndjson`: one JSON event per line.

- `start`: accepted and charged. Carries `id`, `model` and `usage` (`input_words`, `cost_usd`).
- `progress`: `{"paragraphs": n}`, how many paragraphs the document has.
- `paragraph`: `{"index": i, "text": "..."}`, one finished paragraph at its position (0-based). Paragraphs are written in parallel, so they arrive in any order; lines that are kept as they are (headings, short lines) arrive first. A paragraph can be sent again with the same index if it is revised: keep the latest.
- `done`: the whole document in `output` (exact, with your original line breaks), `usage` with `output_words`, `balance_usd`, `latency_ms`.
- `error`: `{"error": {"type", "message", "request_id"}}`. The run failed, nothing is charged, and the stream ends.

```json
{"type":"start","id":"req_8f2c…","model":"studysolutions","usage":{"input_words":412,"cost_usd":0.0206}}
{"type":"progress","paragraphs":3}
{"type":"paragraph","index":1,"text":"…"}
{"type":"paragraph","index":0,"text":"…"}
{"type":"paragraph","index":2,"text":"…"}
{"type":"done","id":"req_8f2c…","output":"…","usage":{…,"output_words":405},"balance_usd":9.9794,"latency_ms":3840}
```

JavaScript:

```javascript
const res = await fetch("https://www.studysolutions.app/api/v1/humanize", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SS_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ text, stream: true }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
const paragraphs = [];
let buffer = "";

for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop();
  for (const line of lines) {
    if (!line) continue;
    const event = JSON.parse(line);
    if (event.type === "paragraph") paragraphs[event.index] = event.text;
    if (event.type === "done") console.log(event.output, event.usage);
    if (event.type === "error") throw new Error(event.error.message);
  }
}
```

Python:

```python
import json, os, requests

with requests.post(
    "https://www.studysolutions.app/api/v1/humanize",
    headers={"Authorization": f"Bearer {os.environ['SS_API_KEY']}"},
    json={"text": text, "stream": True},
    stream=True,
    timeout=300,
) as res:
    for line in res.iter_lines():
        if not line:
            continue
        event = json.loads(line)
        if event["type"] == "paragraph":
            print(event["index"], event["text"])
        elif event["type"] == "done":
            print(event["usage"])
        elif event["type"] == "error":
            raise RuntimeError(event["error"]["message"])
```

## GET /balance

The prepaid balance, and how many words it buys at the current rate.

cURL:

```bash
curl https://www.studysolutions.app/api/v1/balance \
  -H "Authorization: Bearer $SS_API_KEY"
```

```json
{
  "object": "balance",
  "balance_usd": 9.9794,
  "humanize_words_available": 199588,
  "currency": "usd"
}
```

## GET /models

Public, no key needed. Lists the models and the price.

cURL:

```bash
curl https://www.studysolutions.app/api/v1/models
```

## Errors

Every error has the same shape. Branch on `error.type`; messages can change.

```json
{
  "error": {
    "type": "insufficient_balance",
    "message": "Balance is too low for this request.",
    "balance_usd": 0.0012,
    "cost_usd": 0.0206
  }
}
```

| Status | Type | When |
|---|---|---|
| 400 | `invalid_request` | Body is not JSON, `text` is missing or empty, a field has the wrong type, or the model is not available yet. |
| 413 | `text_too_long` | Over 25,000 words (or 250,000 characters) in one request. |
| 401 | `invalid_api_key` | Key missing, malformed, unknown or revoked. |
| 402 | `insufficient_balance` | Balance is below the cost of this request. |
| 403 | `account_disabled` | The account is suspended or deleted. |
| 429 | `concurrency_limit` | 3 requests already in flight on this account. Retry when one finishes. |
| 502 | `engine_error` | The engine failed mid-run. The charge is refunded. |
| 504 | `timeout` | The run took longer than 280 s. The charge is refunded. |
| 503 | `service_unavailable` | Temporary problem on our side. Safe to retry. |

Response headers: `X-Request-Id` (same as the body id), `X-Concurrency-Limit` (requests allowed in flight), `X-Concurrency-Remaining` (free slots when this request started), `Retry-After` (seconds; on 429 only).
