Auto-Reader

Illustrate Partner: pictures inside your app

Give your readers a picture of any page they are reading, in 40 art styles and 29 historical eras. You call one API from your server (or hand your web page a short-lived token), we draw, and you pay a flat $1 a month for each reader who actually used it.

Get a partner key

What it does

Pricing

Price$1 per active user per month
Active usermade at least one picture that month. Signed-up but idle users are free.
Included30 pictures per active user per month
Minimum$10 a month (covers up to 10 active users)
Over 30 in a monththat user is paused until the 1st of next month

Your bill is max($10, active users x $1). Refused, failed and cancelled pictures never count, and never make a user active. A two-version request ("count": 2) counts as two pictures.

How billing works

  1. Get an ordinary API key with your email and click the verification link we send.
  2. Call POST /v1/partner/signup with that key. You get your partner key (nsk_partner_...) at once, plus a Polar checkout link for the first $10 (card on file). The partner key starts working the moment that payment is in.
  3. Each month Polar charges $10 at the start of the month, plus $1 for each active user above 10 in the month before (usage in arrears).
  4. Invoices and receipts come from Polar. You can cancel any time; your users keep working until the paid month ends.

Grace period

Quickstart (curl)

Base URL: https://api.auto-reader.com. Two kinds of key: the partner key (nsk_partner_..., server side only, manages users, cannot make pictures) and a user sub-key (nsk_live_..., makes pictures for one user, billed to you).

0. Sign up

# a. an ordinary key with your email (we send a verification link: click it)
curl -X POST https://api.auto-reader.com/signup \
  -H "Content-Type: application/json" -d '{"email": "[email protected]"}'
# -> {"api_key":"nsk_live_...", ...}

# b. become a partner with that key
curl -X POST https://api.auto-reader.com/v1/partner/signup \
  -H "Authorization: Bearer $NSK_LIVE" -H "Content-Type: application/json" -d '{}'
# -> {"partner_id":"ptn_...","partner_key":"nsk_partner_...","status":"pending",
#     "checkout_url":"https://..."}   keep the partner key: it is your server credential

# c. pay the first $10 at checkout_url. Then:
curl -H "Authorization: Bearer $NSK_PARTNER" https://api.auto-reader.com/v1/partner/me
# -> {"status":"active", ...}   (403 EMAIL_NOT_VERIFIED in step b = click the link first)

1. Create (or fetch) a key for one of your users

Same external_id, same key: calling it again is safe (201 when created, 200 when returned). external_id is 1-128 characters of A-Z a-z 0-9 . _ : @ -.

curl -X POST https://api.auto-reader.com/v1/partner/users \
  -H "Authorization: Bearer $NSK_PARTNER" -H "Content-Type: application/json" \
  -d '{"external_id": "user-123"}'
# -> {"external_id":"user-123","api_key":"nsk_live_...","created":true,"partner_status":"active",
#     "pictures_month":0,"pictures_left_month":30,"pictures_today":0,"pictures_left_today":10}

2. Illustrate a page

curl -X POST https://api.auto-reader.com/v1/illustrate \
  -H "Authorization: Bearer $USER_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"text": "The old sailor knocked on the inn door...", "style_id": "auto"}'
# -> 202 {"job_id":"img_...","status":"queued","poll":"/v1/jobs/img_...","eta_seconds":8, ...}

3. Poll, then download

Poll every 3 seconds. A picture usually takes a few seconds, longer when the queue is busy.

curl -H "Authorization: Bearer $USER_KEY" https://api.auto-reader.com/v1/jobs/img_...
# -> {"status":"done","images":[{"n":1,"url":"/v1/jobs/img_.../image/1", ...}]}
curl -H "Authorization: Bearer $USER_KEY" -o page.webp \
  https://api.auto-reader.com/v1/jobs/img_.../image/1

4. Web page: a short-lived widget token

Do not put the partner key anywhere but your server. For a web page, register the page's origins once, then let your backend mint a token for one user (ttl 30 to 900 seconds, default 900). The browser uses it as its Bearer key; it only works for /v1/illustrate and /v1/jobs/*, and only when the browser's Origin header is one you registered.

curl -X POST https://api.auto-reader.com/v1/partner/settings \
  -H "Authorization: Bearer $NSK_PARTNER" -H "Content-Type: application/json" \
  -d '{"allowed_origins": ["https://yourapp.com"]}'

curl -X POST https://api.auto-reader.com/v1/partner/tokens \
  -H "Authorization: Bearer $NSK_PARTNER" -H "Content-Type: application/json" \
  -d '{"external_id": "user-123", "ttl": 900}'
# -> {"token":"nsk_tok_...","expires_at":"2026-11-09T12:15:00.000Z","external_id":"user-123"}

Native apps send no Origin, so tokens are not for them: give the app that user's own sub-key from your backend instead (it can only make pictures for that one user, and you can revoke it any time). A drop-in button for web pages is coming.

5. Usage and bill so far

curl -H "Authorization: Bearer $NSK_PARTNER" https://api.auto-reader.com/v1/partner/me
# -> {"status":"active","trust_level":0,"month":"2026-11","active_users":12,"pictures_month":140,
#     "pictures_today":9,"estimated_bill_usd":12,"limits":{...},"grace":null,"allowed_origins":[...]}
# (/v1/partner/usage is the same answer)

Users: look up, list, remove

Removing a user revokes the sub-key; running jobs end and nothing more is billed for that user. Their monthly count is remembered, so re-creating the same external_id does not give a fresh 30.

curl -H "Authorization: Bearer $NSK_PARTNER" https://api.auto-reader.com/v1/partner/users/user-123
curl -H "Authorization: Bearer $NSK_PARTNER" "https://api.auto-reader.com/v1/partner/users?cursor="
# -> {"users":[{"external_id":"user-123"}, ...],"cursor":null}
curl -X DELETE -H "Authorization: Bearer $NSK_PARTNER" \
  https://api.auto-reader.com/v1/partner/users/user-123
# -> {"external_id":"user-123","revoked":true}

Settings

Lower the per-user caps for your whole account, or register web origins (up to 20, https://host[:port]).

curl -X POST https://api.auto-reader.com/v1/partner/settings \
  -H "Authorization: Bearer $NSK_PARTNER" -H "Content-Type: application/json" \
  -d '{"user_monthly": 20, "user_daily": 5, "allowed_origins": ["https://yourapp.com"]}'

Code samples

JavaScript (Node, server side)

// Node 18+ (server side). Never ship the partner key to a browser or an app.
const API = "https://api.auto-reader.com";
const P = { Authorization: "Bearer " + process.env.NSK_PARTNER, "Content-Type": "application/json" };

async function illustrate(externalId, text) {
  const u = await (await fetch(API + "/v1/partner/users", { method: "POST", headers: P,
    body: JSON.stringify({ external_id: externalId }) })).json();
  const H = { Authorization: "Bearer " + u.api_key, "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID() };
  const r = await fetch(API + "/v1/illustrate", { method: "POST", headers: H,
    body: JSON.stringify({ text, style_id: "auto" }) });
  const job = await r.json();
  if (r.status !== 202) throw new Error(job.code || job.error);   // e.g. USER_MONTHLY_LIMIT, PARTNER_PAUSED
  for (;;) {
    await new Promise(res => setTimeout(res, 3000));
    const j = await (await fetch(API + job.poll, { headers: H })).json();
    if (j.status === "done") return Buffer.from(await (await fetch(API + j.images[0].url, { headers: H })).arrayBuffer());
    if (j.status === "failed" || j.status === "cancelled") throw new Error(j.error || j.status);
  }
}

Python

import os, time, uuid, requests

API = "https://api.auto-reader.com"
P = {"Authorization": "Bearer " + os.environ["NSK_PARTNER"]}

def illustrate(external_id, text):
    key = requests.post(API + "/v1/partner/users", headers=P,
                        json={"external_id": external_id}).json()["api_key"]
    h = {"Authorization": "Bearer " + key}
    r = requests.post(API + "/v1/illustrate", json={"text": text, "style_id": "auto"},
                      headers={**h, "Idempotency-Key": str(uuid.uuid4())})
    job = r.json()
    if r.status_code != 202:
        raise RuntimeError(job.get("code") or job.get("error"))
    while True:
        time.sleep(3)
        j = requests.get(API + job["poll"], headers=h).json()
        if j["status"] == "done":
            return requests.get(API + j["images"][0]["url"], headers=h).content
        if j["status"] in ("failed", "cancelled"):
            raise RuntimeError(j.get("error") or j["status"])

Browser (with a widget token)

// Your backend: POST /v1/partner/tokens with the partner key, return {token} to the page.
const { token } = await (await fetch("/api/illustrate-token")).json();   // your own endpoint
const H = { Authorization: "Bearer " + token, "Content-Type": "application/json",
  "Idempotency-Key": crypto.randomUUID() };
const job = await (await fetch("https://api.auto-reader.com/v1/illustrate", { method: "POST",
  headers: H, body: JSON.stringify({ text: pageText, style_id: "auto" }) })).json();
// poll job.poll every 3 s with the same header, then show images[0].url (fetch it with the header, make a blob URL)

Kotlin (Android, OkHttp)

// Android: ask YOUR backend for this user's sub-key (it calls /v1/partner/users), then call the API with it.
val userKey = myBackend.illustrateKey()
val req = Request.Builder()
    .url("https://api.auto-reader.com/v1/illustrate")
    .header("Authorization", "Bearer $userKey")
    .header("Idempotency-Key", UUID.randomUUID().toString())
    .post("""{"text": ${JSONObject.quote(pageText)}, "style_id": "auto"}"""
        .toRequestBody("application/json".toMediaType()))
    .build()
val job = JSONObject(client.newCall(req).execute().body!!.string())
// then poll job.getString("poll") every 3 s and download images[0].url

Swift (iOS)

// iOS: same flow, the user's sub-key from your own backend.
let userKey = try await myBackend.illustrateKey()
var req = URLRequest(url: URL(string: "https://api.auto-reader.com/v1/illustrate")!)
req.httpMethod = "POST"
req.setValue("Bearer \(userKey)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue(UUID().uuidString, forHTTPHeaderField: "Idempotency-Key")
req.httpBody = try JSONSerialization.data(withJSONObject: ["text": pageText, "style_id": "auto"])
let (data, _) = try await URLSession.shared.data(for: req)
// decode {"job_id","poll"}, poll every 3 s, then download images[0].url

Limits and fair use

Error codes

Status + codeMeaning
403 EMAIL_NOT_VERIFIEDsign-up: click the verification link first
403 PARTNER_NOT_LIVEthe first $10 is not paid yet
402 PARTNER_PAUSEDgrace over, chargeback or cancelled: pictures paused; polling and downloads still work
403 PARTNER_REVIEWa new user would pass the unpaid-exposure ceiling (see limits)
429 USER_MONTHLY_LIMIT / USER_DAILY_LIMITthat user's 30 a month / 10 a day (or your lower cap); Retry-After says when
429 BURST / PARTNER_DAILY_LIMIT / PARTNER_NEW_USERS_LIMITper-minute burst, your daily ceiling, your new-users-per-day cap
429 USER_PAUSEDmore than 10 refused requests today: that user is paused 24 hours
403 SCOPEwrong key for this endpoint (partner key on a picture call, sub-key on anything but pictures)
403 PARTNER_KEY_IN_BROWSERthe partner key was used with an Origin header
401 TOKEN_INVALID / 403 TOKEN_ORIGINtoken expired, tampered or its user revoked / page origin not registered
403 POLICY_BLOCKED / CUSTOM_BLOCKEDthe passage or the custom words cannot be drawn; never retry automatically

Picture answers also carry X-Credits-Charged: 0 (your users never spend credits) and, in a grace period, X-Partner-Warning: payment_failed.

FAQ

Do idle users cost anything?

No. Only users who made at least one picture that month count.

Can my users choose a style?

Yes. Pass style_id and, if you like, era. The catalogue is the one on the illustrate page (GET /v1/illustrate/status lists it); "auto" picks a style from the text.

Do pictures carry a mark?

No Auto-Reader mark and no QR code. Pictures are labelled as AI made in their metadata, as on our own site.

Can my users call OCR, translation or whole books with their sub-key?

No. Sub-keys can only make, poll, download and delete single pictures (/v1/illustrate, /v1/illustrate/status, /v1/jobs/*) and read their own usage (/v1/usage).

Do you keep my users' text?

No. The text becomes a short scene description that is kept with the picture for 48 hours, then both are deleted.

What if a user hits 30 pictures?

Their next request gets 429 USER_MONTHLY_LIMIT until the 1st of next month. Other users are not affected.

Where is the full API reference?

Picture calls are the public /v1/illustrate API: send text (one page, up to 4,000 characters), style_id, optional era, count (1 or 2), detail (standard or detailed) and purpose (page, chapter or cover), with an Idempotency-Key header so a retry never makes two pictures. You get 202 with a job_id; poll /v1/jobs/{id} until done; download each images[].url; DELETE /v1/jobs/{id} removes it early. GET /v1/illustrate/status tells you whether the service is open. Questions: [email protected].