Developer documentation

StoryMint API Docs

Everything you need to generate AI-powered children's books programmatically. Custom branding, multiple formats, and code examples in every major language.

API access requires a paid plan. Only Developer, Max, and Schools & Organizations plans include API keys and programmatic book generation. Compare plans →

Quick start

Get your first book generated in under 5 minutes.

5-minute setup

  1. Sign up at story-mint.web.app/signup
  2. Subscribe to the Developer, Max, or Schools & Organizations plan (API access required)
  3. Create an API key at your API dashboard
  4. Make your first request — see the examples below
  5. Poll status until status: "done", then download

4 Book formats

Picture books, comics, manga-style chapter comics, and full novels Beta with AI-generated covers.

Custom branding

Set author name, series, art style, and choose from 8+ visual styles for your brand.

Up to 200 pages

Higher plans unlock more pages per book. Comics max at 200, picture books at 100, novels at 40 chapters.

Full downloads

ZIP packages with PDF, individual pages, cover images, and metadata JSON.

Audiobook generation

Turn any completed book into an MP3 audiobook with AI narration (Max plan+).

AI chat editing

Send natural-language edit instructions and the AI rewrites pages in real time.

Lowest-code integration path

  1. Check auth first with GET /api/ping so you know the key and plan are valid.
  2. Generate with POST /api/generate.
  3. Poll GET /api/status/{job_id} until you see done.
  4. Fetch or download with GET /api/book/{job_id} for JSON or GET /api/download/{job_id} for the ZIP.

Authentication

Every request must include one of two authentication methods:

Option 1 — API key (recommended)

Pass your key in the X-API-Key header. API keys inherit your account's tier and quota.

X-API-Key: sm_your_key_here

Option 2 — Firebase Bearer token

If you're building a frontend, pass the Firebase ID token:

Authorization: Bearer <firebase_id_token>

Security: API keys are hashed with SHA-256 before storage — we never store your raw key. Treat your key like a password.

Base URL

https://felice-uninterrupted-vicente.ngrok-free.dev

All endpoint paths below are relative to this base. Example: POST https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate

Rate limits

Rate limits are applied per-account based on your plan tier:

PlanBooks / monthMax picture pagesMax comic pagesMax novel chaptersAPI calls
Free1 trial24
Plus155020020
Max505020030Unlimited
Developer10050150 test
Schools & OrgsUnlimited10020040Unlimited

When you exceed your quota, the API returns 403 with a descriptive error message. Quotas reset on the 1st of each month. Page limits are per-book, not per-month.

Generate a book

POST /api/generate Quota

Start an asynchronous book generation job. Returns a job_id you'll use to poll status and retrieve the result.

Request body (JSON)

ParameterTypeRequiredDescription
prompt string required Story description, up to 600 characters. E.g. "A brave little raccoon who learns to share with forest friends"
book_format string optional One of: picture, comic, chapter_comic, novel Beta. Defaults to picture. See formats.
author string optional Author name printed on the cover and title page. Great for custom branding.
series string optional Series name if this book belongs to a branded collection. Shown on the cover.
art_style string optional Visual style for illustrations. See Art styles. Default: cartoon.
ollama_model string optional Specific LLM model name from /api/models. Leave empty for auto-select.
sd_model string optional Specific Stable Diffusion model from /api/models. Leave empty for auto-select.
num_pages integer optional Number of pages (picture/comic) or chapters (novel). Clamped to your plan's limits — paid comics top out at 200 pages, picture books reach 100 pages on Schools & Organizations, and novels are beta with plan-based chapter caps.
save_mode string optional temporary_cloud (default), cloud_library, or local_until_export.

Response

{
  "job_id": "a1b2c3d4",
  "status": "running"
}

How it looks — modern prompt preview

This is what an API request looks like mapped to the StoryMint Create UI. Your API call sets all of these fields programmatically:

Your API request → StoryMint Create screen
Generate
Format: picture
Pages: 12
Style: watercolor
Author: My Brand
Series: Forest Friends
Model: Auto

Every chip above maps to a JSON field in the request body. The prompt is the textarea, and options become key-value pairs.

Full example — create a branded picture book

import requests

API_KEY = "sm_your_key_here"
BASE    = "https://felice-uninterrupted-vicente.ngrok-free.dev"

resp = requests.post(
    f"{BASE}/api/generate",
    headers={"X-API-Key": API_KEY},
    json={
        "prompt": "A brave little raccoon who learns to share",
        "book_format": "picture",
        "author": "My Brand Publishing",
        "series": "Forest Friends",
        "art_style": "watercolor",
        "num_pages": 12,
    },
)
data = resp.json()
print(f"Job started: {data['job_id']}")
const API_KEY = "sm_your_key_here";
const BASE    = "https://felice-uninterrupted-vicente.ngrok-free.dev";

const resp = await fetch(`${BASE}/api/generate`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": API_KEY,
  },
  body: JSON.stringify({
    prompt: "A brave little raccoon who learns to share",
    book_format: "picture",
    author: "My Brand Publishing",
    series: "Forest Friends",
    art_style: "watercolor",
    num_pages: 12,
  }),
});
const data = await resp.json();
console.log("Job started:", data.job_id);
import java.net.http.*;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();
String body = """
  {
    "prompt": "A brave little raccoon who learns to share",
    "book_format": "picture",
    "author": "My Brand Publishing",
    "series": "Forest Friends",
    "art_style": "watercolor",
    "num_pages": 12
  }
  """;

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate"))
    .header("Content-Type", "application/json")
    .header("X-API-Key", "sm_your_key_here")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
curl -X POST https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sm_your_key_here" \
  -d '{
    "prompt": "A brave little raccoon who learns to share",
    "book_format": "picture",
    "author": "My Brand Publishing",
    "series": "Forest Friends",
    "art_style": "watercolor",
    "num_pages": 12
  }'
using System.Net.Http;
using System.Text;
using System.Text.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "sm_your_key_here");

var payload = new {
    prompt = "A brave little raccoon who learns to share",
    book_format = "picture",
    author = "My Brand Publishing",
    series = "Forest Friends",
    art_style = "watercolor",
    num_pages = 12,
};

var content = new StringContent(
    JsonSerializer.Serialize(payload),
    Encoding.UTF8, "application/json");

var resp = await client.PostAsync(
    "https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate",
    content);

var json = await resp.Content.ReadAsStringAsync();
Console.WriteLine(json);
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "io"
)

func main() {
    body, _ := json.Marshal(map[string]interface{}{
        "prompt":      "A brave little raccoon who learns to share",
        "book_format": "picture",
        "author":      "My Brand Publishing",
        "series":      "Forest Friends",
        "art_style":   "watercolor",
        "num_pages":   12,
    })
    req, _ := http.NewRequest("POST",
        "https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate",
        bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", "sm_your_key_here")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    data, _ := io.ReadAll(resp.Body)
    fmt.Println(string(data))
}

Check status

GET /api/status/{job_id}

Poll this endpoint to track generation progress. Typically takes 30–120 seconds for picture books, 2–5 minutes for comics, and 5–15 minutes for novels.

Response

{
  "status": "running",
  "step": "Generating illustrations",
  "msg": "Drawing page 4 of 12...",
  "pct": 45,
  "title": "Rusty's Big Day",
  "format": "picture",
  "can_download": false
}

Status values: runningdone or error.

Polling example (Python)

import time, requests

API_KEY = "sm_your_key_here"
BASE    = "https://felice-uninterrupted-vicente.ngrok-free.dev"
job_id  = "a1b2c3d4"  # from /api/generate

while True:
    r = requests.get(f"{BASE}/api/status/{job_id}",
                     headers={"X-API-Key": API_KEY})
    info = r.json()
    print(f"[{info.get('pct', 0)}%] {info.get('msg', '')}")

    if info["status"] == "done":
        print(f"Book ready! Title: {info.get('title')}")
        break
    elif info["status"] == "error":
        print(f"Error: {info.get('msg')}")
        break

    time.sleep(3)  # poll every 3 seconds

SSE progress stream

GET /api/stream/{job_id}

Real-time Server-Sent Events stream. More efficient than polling — you receive updates the instant they happen.

Event format

data: {"step":"generating","msg":"Writing page 3...","pct":25}
data: {"step":"illustrating","msg":"Drawing page 3...","pct":40}
data: {"step":"done","msg":"Book complete!","pct":100}

JavaScript example

const BASE   = "https://felice-uninterrupted-vicente.ngrok-free.dev";
const jobId  = "a1b2c3d4";

const evtSrc = new EventSource(`${BASE}/api/stream/${jobId}`);
evtSrc.onmessage = (e) => {
  const data = JSON.parse(e.data);
  console.log(`[${data.pct}%] ${data.msg}`);
  if (data.step === "done" || data.step === "error") {
    evtSrc.close();
  }
};

Get book data

GET /api/book/{job_id}

Retrieve the full book preview: pages, cover, text, metadata, and image URLs. Only available after the job status is done.

Response (picture book)

{
  "title": "Rusty's Big Day",
  "author": "My Brand Publishing",
  "series": "Forest Friends",
  "format": "picture",
  "pages": [
    {
      "page_num": 1,
      "text": "Once upon a time, in a hollow oak tree...",
      "image_url": "/api/book/a1b2c3d4/image/page_01.png"
    }
  ],
  "cover_url": "/api/book/a1b2c3d4/image/cover.jpg",
  "can_download": true,
  "metadata": {
    "total_pages": 12,
    "art_style": "watercolor",
    "model_used": "gemma3:12b"
  }
}

Chat edit

POST /api/book/{job_id}/chat

Send natural-language instructions to modify a completed book. The AI rewrites the affected pages.

Request body

{
  "message": "Make the ending happier and add a rainbow"
}

Response

{
  "assistant_message": "I've updated pages 10-12 with a happier ending featuring a rainbow celebration!",
  "pages": [ ... ]
}

Python example

resp = requests.post(
    f"{BASE}/api/book/{job_id}/chat",
    headers={"X-API-Key": API_KEY},
    json={"message": "Change the main character's name to Luna"},
)
edit = resp.json()
print(edit["assistant_message"])

Download package

GET /api/download/{job_id} Paid plans

Download the complete book as a ZIP package. Requires Plus, Max, Developer, or the Schools & Organizations plan.

ZIP contents

book_package.zip
├── cover.jpg
├── book.pdf
├── pages/
│   ├── page_01.png
│   ├── page_02.png
│   └── ...
└── metadata.json

Python download example

resp = requests.get(
    f"{BASE}/api/download/{job_id}",
    headers={"X-API-Key": API_KEY},
)
if resp.status_code == 200:
    with open("my_book.zip", "wb") as f:
        f.write(resp.content)
    print("Downloaded!")
else:
    print("Error:", resp.json())

Audiobook

POST /api/audiobook/{job_id} Max+

Generate an MP3 audiobook with AI narration from a completed book. Available on Max and Schools & Organizations plans.

Response

Returns the MP3 file directly as audio/mpeg. Save it to disk:

resp = requests.post(
    f"{BASE}/api/audiobook/{job_id}",
    headers={"X-API-Key": API_KEY},
)
with open("audiobook.mp3", "wb") as f:
    f.write(resp.content)

List models

GET /api/models

Returns available AI models for your tier. Use model names in the ollama_model and sd_model fields when generating.

Response

{
  "ollama": [
    "gemma3:4b",
    "gemma3:12b",
    "deepseek-r1:8b",
    "mistral:7b",
    "qwen3:8b"
  ],
  "sd": [
    "animagineXLV31_v31",
    "juggernautXL_v9",
    "starlightXL_v3"
  ]
}

Models vary by tier. If you request a model not available on your plan, the API falls back to auto-select.

Ping auth check

GET /api/ping

Run a low-cost auth check before you generate. This confirms API access, tells you which plan is attached to the key, and increments the separate ping counter used by the Developer plan.

Response

{
  "status": "ok",
  "tier": "developer",
  "plan_display_name": "Developer",
  "api_test_calls_this_month": 12,
  "api_test_calls_limit": 150,
  "timestamp": "2026-03-27T18:41:22.115000+00:00"
}

Developer includes 150 ping calls per month. Max and Schools & Organizations include API access and currently have no separate ping cap.

Tier & usage

GET /api/tier

Get your current plan, usage counts, limits, and remaining quota.

Response

{
  "tier": "developer",
  "plan_display_name": "Developer",
  "books_this_month": 12,
  "books_limit": 100,
  "next_reset": "2026-04-01",
  "api_access": true,
  "api_test_calls_this_month": 12,
  "api_test_calls_limit": 150,
  "full_download": true,
  "max_pages": 50,
  "novel_max_chapters": 0,
  "org_type": ""
}

Custom branding

StoryMint is designed for white-label book generation. Use these fields to brand every book with your identity:

FieldWhere it appearsExample
author Cover, title page, PDF metadata "Maple Leaf Publishing"
series Cover subtitle, spine (if applicable) "Tiny Explorers Vol. 3"
art_style All illustrations in the book "watercolor"
book_format Entire book layout and structure "novel"

Branding workflow

# Create a branded series of books
for topic in ["Space", "Ocean", "Jungle"]:
    resp = requests.post(f"{BASE}/api/generate",
        headers={"X-API-Key": API_KEY},
        json={
            "prompt": f"A child explores the {topic.lower()}",
            "author": "Your Publishing Company",
            "series": f"Little Explorers: {topic}",
            "art_style": "watercolor",
            "book_format": "picture",
        })
    print(f"Started {topic}: {resp.json()['job_id']}")

Profile customization

GET PUT /api/profile

Read or update your publisher profile (display name, bio, website). This information appears on marketplace listings.

requests.put(f"{BASE}/api/profile",
    headers={"X-API-Key": API_KEY},
    json={
        "display_name": "Maple Leaf Publishing",
        "bio": "We create magical stories for curious kids.",
        "website": "https://mapleleaf.example.com",
    })

Art styles

Choose a visual style for all illustrations in your book. Pass the art_style value in your generate request.

cartoon

Bright, bold cartoon style. Best for younger audiences (ages 3–7). Default.

watercolor

Soft watercolor paintings. Elegant and whimsical, great for premium branding.

sketch

Pencil-sketch illustrations with a hand-drawn feel.

pixel

Retro pixel-art style. Fun for game-themed or nostalgic stories.

flat

Modern flat-design illustrations with clean lines and solid colours.

manga

Japanese manga style with expressive characters. Ideal for chapter comics.

western

Western comic-book style with dynamic inking and vivid panels.

graphic_novel

Mature graphic-novel aesthetic with detailed, cinematic panels.

Plans & page limits

The higher your plan, the more pages you can generate per book — up to 200 pages max. Here's what each tier unlocks:

Free

$0
  • 24 picture pages max
  • 0 comic pages
  • 0 novel chapters
  • 1 trial book total

Plus

CA$9.99/mo
  • 50 picture pages max
  • 200 comic pages max
  • 20 novel chapters max Beta
  • 15 books / month

Schools & Organizations

CA$99.99/mo
  • 100 picture pages max
  • 200 comic pages max
  • 40 novel chapters max Beta
  • Unlimited books
  • 10 teacher or team seats included

Developer

CA$25/mo
  • 50 picture pages max
  • 100 books / month
  • 150 API test calls
  • Full API access
  • No comics or novels

Visual page scale

See how max pages scale by plan:

24Free
50Plus
50Max
50Dev
100School

Comic pages go up to 200 across paid creative plans. Novels are in Beta and max out at 40 chapters on Schools & Organizations.

Book formats

FormatValueOutputMax pagesBest for
Picture book picture Full-page illustrations with overlaid text 24–100 pages (by plan) Ages 2–7, simple stories
Comic comic Panel-based comic pages with speech bubbles Up to 200 pages Ages 6–12, action stories
Chapter comic (manga) chapter_comic Multi-chapter comic with cover page per chapter Up to 200 pages Ages 8+, serialized stories
Novel Beta novel Long-form text with AI cover, formatted PDF 5–40 chapters (by plan) Ages 10+, 15k–60k+ words

Beta Novel generation is currently in beta. Novels use AI to write full-length chapter books (15,000–60,000+ words) with auto-generated covers. During beta:

  • Generation takes 5–15 minutes depending on chapter count
  • Chapter quality improves with higher-tier LLMs (Max and Schools & Organizations plans)
  • Novel editing via chat is supported but works best for small tweaks
  • PDF output includes professional formatting with chapter headings and page numbers
  • Novel availability is subject to plan — Plus gets 3/month, Max gets 10/month, Schools & Organizations is unlimited

API key management

GET /api/keys

List all your API keys with prefix, label, created/last-used dates, and status.

POST /api/keys

Create a new API key. The raw key (sm_...) is returned only once — save it immediately.

Request

{ "label": "My Production App" }

Response

{
  "key": "sm_abc123xyz...",
  "key_id": "k_8f2e1a",
  "label": "My Production App"
}
DELETE /api/keys/{key_id}

Revoke an API key. It becomes permanently inactive. Create a new one if needed.

Teams (Schools & Organizations)

Schools & Organizations users can create shared workspaces with teacher or team-member seats, invite collaborators by email, and share finished books into a common library.

GET POST /api/teams

GET lists your teams. POST creates a new team.

Create team

resp = requests.post(f"{BASE}/api/teams",
    headers={"X-API-Key": API_KEY},
    json={"name": "Oakwood Elementary"})
team = resp.json()
print(f"Workspace: {team['id']} ({team['workspace_type']}), seats: {team['max_seats']}")
POST /api/teams/{team_id}/invite

Invite a member by email. The response includes an invite id you can use for acceptance flows.

{
  "email": "teacher@school.edu"
}
POST /api/teams/invite/{invite_id}/accept

Accept a pending invite after the invited user signs in. Returns the updated workspace object.

GET /api/teams/{team_id}/books

List all books in the shared team library.

POST /api/teams/{team_id}/books/{book_id}

Share a finished book into the shared workspace library so every member can access it.

Full example — Python

Complete end-to-end workflow: generate, poll, download.

import requests, time

API_KEY = "sm_your_key_here"
BASE    = "https://felice-uninterrupted-vicente.ngrok-free.dev"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# 1. Generate
gen = requests.post(f"{BASE}/api/generate", headers=HEADERS, json={
    "prompt": "A cat astronaut discovers a candy planet",
    "book_format": "comic",
    "author": "Starlight Studios",
    "art_style": "cartoon",
    "num_pages": 30,
}).json()
job_id = gen["job_id"]
print(f"Job: {job_id}")

# 2. Poll until done
while True:
    status = requests.get(f"{BASE}/api/status/{job_id}",
                          headers=HEADERS).json()
    print(f"  [{status.get('pct', 0):3d}%] {status.get('msg', '')}")
    if status["status"] in ("done", "error"):
        break
    time.sleep(3)

if status["status"] == "error":
    print("Generation failed:", status.get("msg"))
    exit(1)

# 3. Get book data
book = requests.get(f"{BASE}/api/book/{job_id}",
                    headers=HEADERS).json()
print(f"\nTitle: {book['title']}")
print(f"Pages: {len(book.get('pages', []))}")

# 4. AI edit
edit = requests.post(f"{BASE}/api/book/{job_id}/chat",
    headers=HEADERS,
    json={"message": "Add more humor to page 3"}).json()
print(f"Edit: {edit.get('assistant_message')}")

# 5. Download ZIP
dl = requests.get(f"{BASE}/api/download/{job_id}",
                  headers=HEADERS)
if dl.status_code == 200:
    with open(f"{book['title']}.zip", "wb") as f:
        f.write(dl.content)
    print("Downloaded!")
else:
    print("Download error:", dl.json())

Full example — JavaScript (Node.js / Browser)

const API_KEY = "sm_your_key_here";
const BASE    = "https://felice-uninterrupted-vicente.ngrok-free.dev";
const headers = {
  "Content-Type": "application/json",
  "X-API-Key": API_KEY,
};

// 1. Generate
const genResp = await fetch(`${BASE}/api/generate`, {
  method: "POST", headers,
  body: JSON.stringify({
    prompt: "A cat astronaut discovers a candy planet",
    book_format: "comic",
    author: "Starlight Studios",
    art_style: "cartoon",
    num_pages: 30,
  }),
});
const { job_id } = await genResp.json();
console.log("Job:", job_id);

// 2. Poll
let status;
do {
  await new Promise(r => setTimeout(r, 3000));
  const r = await fetch(`${BASE}/api/status/${job_id}`, { headers });
  status = await r.json();
  console.log(`  [${status.pct x 0}%] ${status.msg x ""}`);
} while (status.status === "running");

// 3. Get book
const book = await (await fetch(`${BASE}/api/book/${job_id}`,
  { headers })).json();
console.log("Title:", book.title);

// 4. Chat edit
const edit = await (await fetch(`${BASE}/api/book/${job_id}/chat`, {
  method: "POST", headers,
  body: JSON.stringify({ message: "Make the ending funnier" }),
})).json();
console.log("Edit:", edit.assistant_message);

// 5. Download (Node.js)
const dlResp = await fetch(`${BASE}/api/download/${job_id}`,
  { headers });
if (dlResp.ok) {
  const fs = await import("fs");
  const buf = Buffer.from(await dlResp.arrayBuffer());
  fs.writeFileSync(`${book.title}.zip`, buf);
  console.log("Downloaded!");
}

// 5b. Download (Browser)
// const blob = await dlResp.blob();
// const a = document.createElement("a");
// a.href = URL.createObjectURL(blob);
// a.download = `${book.title}.zip`;
// a.click();

Full example — Java

import java.net.http.*;
import java.net.URI;
import java.nio.file.*;

public class StoryMintExample {
    static final String API_KEY = "sm_your_key_here";
    static final String BASE = "https://felice-uninterrupted-vicente.ngrok-free.dev";
    static final HttpClient client = HttpClient.newHttpClient();

    static HttpRequest.Builder req(String path) {
        return HttpRequest.newBuilder()
            .uri(URI.create(BASE + path))
            .header("X-API-Key", API_KEY)
            .header("Content-Type", "application/json");
    }

    public static void main(String[] args) throws Exception {
        // 1. Generate
        String body = """
            {
              "prompt": "A cat astronaut discovers a candy planet",
              "book_format": "comic",
              "author": "Starlight Studios",
              "art_style": "cartoon",
              "num_pages": 30
            }""";
        var genResp = client.send(
            req("/api/generate").POST(
                HttpRequest.BodyPublishers.ofString(body)).build(),
            HttpResponse.BodyHandlers.ofString());
        System.out.println("Generate: " + genResp.body());
        // Parse job_id from JSON response

        String jobId = "PARSE_FROM_RESPONSE";

        // 2. Poll status
        while (true) {
            var statusResp = client.send(
                req("/api/status/" + jobId).GET().build(),
                HttpResponse.BodyHandlers.ofString());
            System.out.println("Status: " + statusResp.body());
            if (statusResp.body().contains("\"done\"") ||
                statusResp.body().contains("\"error\"")) break;
            Thread.sleep(3000);
        }

        // 3. Download
        var dlResp = client.send(
            req("/api/download/" + jobId).GET().build(),
            HttpResponse.BodyHandlers.ofByteArray());
        if (dlResp.statusCode() == 200) {
            Files.write(Path.of("book.zip"), dlResp.body());
            System.out.println("Downloaded!");
        }
    }
}

Full example — cURL

# 1. Generate a book
JOB=$(curl -s -X POST \
  https://felice-uninterrupted-vicente.ngrok-free.dev/api/generate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sm_your_key_here" \
  -d '{
    "prompt": "A cat astronaut discovers a candy planet",
    "book_format": "comic",
    "author": "Starlight Studios",
    "art_style": "cartoon",
    "num_pages": 30
  }')
echo "Generate response: $JOB"
JOB_ID=$(echo $JOB | grep -o '"job_id":"[^"]*"' | cut -d'"' -f4)

# 2. Poll status until done
while true; do
  STATUS=$(curl -s \
    https://felice-uninterrupted-vicente.ngrok-free.dev/api/status/$JOB_ID \
    -H "X-API-Key: sm_your_key_here")
  echo "Status: $STATUS"
  echo "$STATUS" | grep -q '"done"\|"error"' && break
  sleep 3
done

# 3. Get book data
curl -s \
  https://felice-uninterrupted-vicente.ngrok-free.dev/api/book/$JOB_ID \
  -H "X-API-Key: sm_your_key_here" | python3 -m json.tool

# 4. Download ZIP
curl -o book.zip \
  https://felice-uninterrupted-vicente.ngrok-free.dev/api/download/$JOB_ID \
  -H "X-API-Key: sm_your_key_here"

Full example — C#

using System.Net.Http;
using System.Text;
using System.Text.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "sm_your_key_here");
var baseUrl = "https://felice-uninterrupted-vicente.ngrok-free.dev";

// 1. Generate
var genPayload = JsonSerializer.Serialize(new {
    prompt = "A cat astronaut discovers a candy planet",
    book_format = "comic",
    author = "Starlight Studios",
    art_style = "cartoon",
    num_pages = 30,
});
var genResp = await client.PostAsync($"{baseUrl}/api/generate",
    new StringContent(genPayload, Encoding.UTF8, "application/json"));
var genJson = JsonDocument.Parse(await genResp.Content.ReadAsStringAsync());
var jobId = genJson.RootElement.GetProperty("job_id").GetString();
Console.WriteLine($"Job: {jobId}");

// 2. Poll
while (true) {
    var statusResp = await client.GetStringAsync($"{baseUrl}/api/status/{jobId}");
    Console.WriteLine($"Status: {statusResp}");
    if (statusResp.Contains("\"done\"") || statusResp.Contains("\"error\"")) break;
    await Task.Delay(3000);
}

// 3. Download
var dlResp = await client.GetAsync($"{baseUrl}/api/download/{jobId}");
if (dlResp.IsSuccessStatusCode) {
    var bytes = await dlResp.Content.ReadAsByteArrayAsync();
    await File.WriteAllBytesAsync("book.zip", bytes);
    Console.WriteLine("Downloaded!");
}

Full example — Go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

const apiKey = "sm_your_key_here"
const base = "https://felice-uninterrupted-vicente.ngrok-free.dev"

func apiReq(method, path string, body interface{}) (*http.Response, error) {
    var buf io.Reader
    if body != nil {
        b, _ := json.Marshal(body)
        buf = bytes.NewBuffer(b)
    }
    req, _ := http.NewRequest(method, base+path, buf)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", apiKey)
    return http.DefaultClient.Do(req)
}

func main() {
    // 1. Generate
    resp, _ := apiReq("POST", "/api/generate", map[string]interface{}{
        "prompt":      "A cat astronaut discovers a candy planet",
        "book_format": "comic",
        "author":      "Starlight Studios",
        "art_style":   "cartoon",
        "num_pages":   30,
    })
    var gen map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&gen)
    resp.Body.Close()
    jobID := gen["job_id"].(string)
    fmt.Println("Job:", jobID)

    // 2. Poll
    for {
        resp, _ := apiReq("GET", "/api/status/"+jobID, nil)
        var st map[string]interface{}
        json.NewDecoder(resp.Body).Decode(&st)
        resp.Body.Close()
        fmt.Printf("  [%.0f%%] %s\n", st["pct"], st["msg"])
        if st["status"] == "done" || st["status"] == "error" {
            break
        }
        time.Sleep(3 * time.Second)
    }

    // 3. Download
    resp, _ = apiReq("GET", "/api/download/"+jobID, nil)
    defer resp.Body.Close()
    if resp.StatusCode == 200 {
        f, _ := os.Create("book.zip")
        io.Copy(f, resp.Body)
        f.Close()
        fmt.Println("Downloaded!")
    }
}

Error codes

  • 200 Success
  • 201 Resource created (API key, team)
  • 400 Bad request — missing or invalid parameters
  • 401 Unauthorized — missing or invalid API key / token
  • 403 Forbidden — quota exceeded or feature not available on your plan
  • 404 Not found — invalid job_id or resource
  • 429 Too many requests — rate limited
  • 500 Internal server error — retry after a few seconds
  • 503 Service unavailable — generation engine temporarily offline

Error response format

{
  "error": "Descriptive error message here",
  "detail": "Additional context (optional)"
}

Handling errors (Python)

resp = requests.post(f"{BASE}/api/generate",
    headers=HEADERS, json=payload)

if resp.status_code == 200:
    job_id = resp.json()["job_id"]
elif resp.status_code == 403:
    print("Quota exceeded:", resp.json().get("error"))
elif resp.status_code == 401:
    print("Invalid API key. Check your X-API-Key header.")
else:
    print(f"Error {resp.status_code}:", resp.json())

Webhooks

Generation-complete webhooks are not available in this public build yet. Use status polling or SSE streaming to track job progress instead.

Interested? Let us know at support@story-mint.com.