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.
What it does
- Your user sends a page of text. We pick its most striking moment and draw it in the style and era you choose (or
"style_id": "auto"). - Same pictures, styles, eras and safety rules as our own illustrate page. Pictures are labelled as AI made.
- Each of your users gets their own sub-key, so limits, usage and revoking work per user.
- Results are kept for 48 hours, then deleted. The page text itself is not stored.
Pricing
| Price | $1 per active user per month |
|---|---|
| Active user | made at least one picture that month. Signed-up but idle users are free. |
| Included | 30 pictures per active user per month |
| Minimum | $10 a month (covers up to 10 active users) |
| Over 30 in a month | that 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
- Get an ordinary API key with your email and click the verification link we send.
- Call
POST /v1/partner/signupwith 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. - 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).
- Invoices and receipts come from Polar. You can cancel any time; your users keep working until the paid month ends.
Grace period
- If a renewal payment fails, everything keeps working for 10 days while Polar retries.
- During that time
GET /v1/partner/meshows"status": "payment_failed"andgrace.pause_at, and every answer (to you and to your users) carries the headerX-Partner-Warning: payment_failed. - On day 10, if still unpaid, new pictures pause:
402 {"code": "PARTNER_PAUSED"}. Polling, downloads and deletes keep working, and nothing is deleted early. - When the payment goes through, pictures resume at once.
- One grace period per 90 days: a second failed payment within 90 days pauses at once. A chargeback pauses at once too, until we review it.
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].urlLimits and fair use
- Each user: 30 pictures a month, 10 a day, 3 picture requests a minute, 10 requests a minute in all. You can set lower monthly and daily caps.
- Your whole account: 60 picture requests a minute, plus a daily ceiling that grows as you pay renewals on time (300 a day at the start, 1,000 after one paid renewal, 3,000 after three; more on request).
- New users per day are capped the same way: 100, then 500, then 2,000.
- While your account is new, this month's active users are capped at the larger of 50 and three times your last invoice in dollars. Beyond that a new user's first picture gets
403 PARTNER_REVIEWuntil we have looked; existing active users are not affected. - The partner key works only from servers: a request carrying a browser
Originheader is refused. Sub-keys and tokens can only make, poll, download and delete pictures (no OCR, translation or whole books). If the partner key leaks, write to us and we rotate it. - Requests our safety filter refuses (
POLICY_BLOCKED,CUSTOM_BLOCKED) are counted. More than 10 refusals in a day pauses that user for 24 hours. - A chargeback or payment dispute pauses the account until we look at it.
- When the service is busy you may get
503withRetry-After. It is never charged: wait and try again with the sameIdempotency-Key.
Error codes
Status + code | Meaning |
|---|---|
403 EMAIL_NOT_VERIFIED | sign-up: click the verification link first |
403 PARTNER_NOT_LIVE | the first $10 is not paid yet |
402 PARTNER_PAUSED | grace over, chargeback or cancelled: pictures paused; polling and downloads still work |
403 PARTNER_REVIEW | a new user would pass the unpaid-exposure ceiling (see limits) |
429 USER_MONTHLY_LIMIT / USER_DAILY_LIMIT | that user's 30 a month / 10 a day (or your lower cap); Retry-After says when |
429 BURST / PARTNER_DAILY_LIMIT / PARTNER_NEW_USERS_LIMIT | per-minute burst, your daily ceiling, your new-users-per-day cap |
429 USER_PAUSED | more than 10 refused requests today: that user is paused 24 hours |
403 SCOPE | wrong key for this endpoint (partner key on a picture call, sub-key on anything but pictures) |
403 PARTNER_KEY_IN_BROWSER | the partner key was used with an Origin header |
401 TOKEN_INVALID / 403 TOKEN_ORIGIN | token expired, tampered or its user revoked / page origin not registered |
403 POLICY_BLOCKED / CUSTOM_BLOCKED | the 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].