Leanpub API Documentation
The Leanpub API allows authors to programmatically preview, publish, and manage their books and courses.
Note: We recently had some authentication issues with the API, but these should now be fixed. If you are still experiencing authentication problems, please email hello@leanpub.com.
Quickstart
Replace YOUR_API_KEY with your actual API key, YOUR_BOOK_SLUG with your book's slug, and YOUR_USERNAME with your username.
This is based on an API test script written in Ruby which is shown at the bottom of this page.
# ===========================================
# PART 1: Verify API Key
# ===========================================
# Verify your API key
curl "https://leanpub.com/current_user.json?api_key=YOUR_API_KEY"
# ===========================================
# PART 2: Book Lifecycle
# ===========================================
# Check if book exists (before creating)
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
# Create a new book (Browser mode)
curl -X POST -H "Content-Type: application/json" \
-d '{"api_key":"YOUR_API_KEY","title":"My Book","slug":"YOUR_BOOK_SLUG","sync_mode":"monaco"}' \
"https://leanpub.com/books.json"
# Check if book exists (after creating)
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
# Get book summary
curl "https://leanpub.com/YOUR_BOOK_SLUG.json?api_key=YOUR_API_KEY"
# Preview book
curl -X POST -d "api_key=YOUR_API_KEY" \
"https://leanpub.com/YOUR_BOOK_SLUG/preview.json"
# Check job status (poll until empty {} response)
curl "https://leanpub.com/YOUR_BOOK_SLUG/job_status.json?api_key=YOUR_API_KEY"
# Register interest in the unpublished book
curl -X POST -H "Content-Type: application/json" \
-d '{"api_key":"YOUR_API_KEY","name":"Test User","email":"test@example.com","share_email_with_author":true}' \
"https://leanpub.com/YOUR_BOOK_SLUG/interested.json"
# Get interested readers (before publish)
curl "https://leanpub.com/YOUR_BOOK_SLUG/interested_readers.json?api_key=YOUR_API_KEY"
# Publish book
curl -X POST \
-d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=false" \
-d "publish[release_notes]=Initial publish via API" \
"https://leanpub.com/YOUR_BOOK_SLUG/publish.json"
# Check job status (poll until empty {} response)
curl "https://leanpub.com/YOUR_BOOK_SLUG/job_status.json?api_key=YOUR_API_KEY"
# Unpublish book
curl -X POST -d "api_key=YOUR_API_KEY" \
"https://leanpub.com/YOUR_BOOK_SLUG/unpublish.json"
# Check book state (should be unpublished)
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
# Re-publish book (required before retire)
curl -X POST \
-d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=false" \
"https://leanpub.com/YOUR_BOOK_SLUG/publish.json"
# Check job status (poll until empty {} response)
curl "https://leanpub.com/YOUR_BOOK_SLUG/job_status.json?api_key=YOUR_API_KEY"
# Retire book
curl -X POST -d "api_key=YOUR_API_KEY" \
"https://leanpub.com/YOUR_BOOK_SLUG/retire.json"
# Check book state (should be retired)
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
# Close book
curl -X POST -d "api_key=YOUR_API_KEY" \
"https://leanpub.com/YOUR_BOOK_SLUG/close.json"
# Check book state (should be closed)
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
# ===========================================
# PART 3: Existing Book Endpoints
# ===========================================
# Get royalties (JSON)
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.json?api_key=YOUR_API_KEY"
# Get royalties (XML)
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.xml?api_key=YOUR_API_KEY"
# Get book reader emails
curl "https://leanpub.com/YOUR_BOOK_SLUG/reader_emails.json?api_key=YOUR_API_KEY"
# Get individual purchases (JSON)
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.json?api_key=YOUR_API_KEY"
# Get individual purchases (XML)
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.xml?api_key=YOUR_API_KEY"
# List coupons (JSON)
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons.json?api_key=YOUR_API_KEY"
# List coupons (XML)
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons.xml?api_key=YOUR_API_KEY"
# Create coupon
curl -X POST -H "Content-Type: application/json" \
-d '{"api_key":"YOUR_API_KEY","coupon":{"coupon_code":"TESTCODE","package_discounts_attributes":[{"package_slug":"book","discounted_price":4.99}],"start_date":"2026-01-01","end_date":"2026-12-31","max_uses":100,"note":"Test coupon"}}' \
"https://leanpub.com/YOUR_BOOK_SLUG/coupons.json"
# Get single coupon
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons/TESTCODE.json?api_key=YOUR_API_KEY"
# Update coupon (suspend it)
curl -X PUT \
-d "api_key=YOUR_API_KEY" \
-d "suspended=true" \
-d "note=Suspended via API" \
"https://leanpub.com/YOUR_BOOK_SLUG/coupons/TESTCODE.json"
Authentication
All API requests require authentication via an API key.
Getting Your API Key
- You need a Pro plan to access the API
- Go to your API Key settings to generate one
- Keep your API key secret—it provides full access to your books
Using Your API Key
Include your API key in every request using one of these methods:
Query parameter (GET requests):
GET https://leanpub.com/{slug}.json?api_key=YOUR_API_KEY
Form data (POST/PUT requests):
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/YOUR_BOOK_SLUG/preview.json
JSON body (POST/PUT requests):
curl -H "Content-Type: application/json" \
-d '{"api_key":"YOUR_API_KEY"}' \
https://leanpub.com/YOUR_BOOK_SLUG/preview.json
Book Information
Get Book Summary
Returns detailed information about a book.
GET https://leanpub.com/{slug}.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG.json?api_key=YOUR_API_KEY"
Response:
{
"slug": "YOUR_BOOK_SLUG",
"title": "Your Book Title",
"subtitle": "An Optional Subtitle",
"about_the_book": "Description of the book...",
"author_string": "Author Name",
"url": "https://leanpub.com/YOUR_BOOK_SLUG",
"title_page_url": "https://...",
"image": "https://...",
"minimum_paid_price": "9.99",
"suggested_price": "19.99",
"page_count": 150,
"page_count_published": 148,
"word_count": 45000,
"word_count_published": 44500,
"total_copies_sold": 1234,
"total_revenue": "15000.00",
"last_published_at": "2024-01-15T19:21:50Z",
"meta_description": "SEO description",
"possible_reader_count": 50,
"pdf_preview_url": "https://leanpub.com/s/...",
"epub_preview_url": "https://leanpub.com/s/...",
"pdf_published_url": "https://leanpub.com/s/...",
"epub_published_url": "https://leanpub.com/s/..."
}
The pdf_preview_url, epub_preview_url, pdf_published_url, and epub_published_url fields are secret URLs for downloading your book files. Keep them private.
Create Book
Creates a new book. Supports two writing modes: Browser (monaco) and GitHub.
POST https://leanpub.com/books.json
Parameters:
title(Required) - The book titleslug(Required) - URL-friendly identifier (must be unique)sync_mode(Optional) - Writing mode:monaco(default) orgithublanguage_id(Optional) - Language ID (defaults to English)publisher_id(Optional) - Publisher ID for organization publishing. Omit this for self-publishing (most authors)github_path(Required for GitHub) - GitHub repo path (e.g.,username/repo)publish_branch(Optional) - Branch for publishing (default:main)preview_branch(Optional) - Branch for previews (default:main)
Example (Browser mode):
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"title": "My New Book",
"slug": "my-new-book",
"sync_mode": "monaco"
}' \
https://leanpub.com/books.json
Example (GitHub mode):
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"title": "My GitHub Book",
"slug": "my-github-book",
"sync_mode": "github",
"github_path": "myusername/my-book-repo",
"publish_branch": "main",
"preview_branch": "main"
}' \
https://leanpub.com/books.json
Response (success):
{
"success": true,
"book": {
"slug": "my-new-book",
"title": "My New Book",
"state": "unpublished",
"url": "https://leanpub.com/my-new-book"
}
}
Response (error):
{
"success": false,
"errors": ["Slug has already been taken"]
}
Create Bundle
Creates a new bundle. Bundles are collections of books sold together at a discount.
POST https://leanpub.com/bundles.json
Parameters:
title(Required) - The bundle titleslug(Required) - URL-friendly identifier (must be unique)publisher_id(Optional) - Publisher ID for organization publishing. Omit this for self-publishing (most authors)
Example:
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"title": "My Bundle",
"slug": "my-bundle"
}' \
https://leanpub.com/bundles.json
Response (success):
{
"success": true,
"bundle": {
"slug": "my-bundle",
"title": "My Bundle",
"state": "unpublished",
"url": "https://leanpub.com/b/my-bundle"
}
}
Response (error):
{
"success": false,
"errors": ["Slug has already been taken"]
}
Create Course
Creates a new course. Courses are interactive learning experiences with lessons and quizzes.
POST https://leanpub.com/courses.json
Parameters:
title(Required) - The course titleslug(Required) - URL-friendly identifier (must be unique)language_id(Required) - Language ID for the coursepublisher_id(Optional) - Publisher ID for organization publishing. Omit this for self-publishing (most authors)sync_mode(Optional) - Writing mode:monaco(default) orgithubgithub_path(Required for GitHub) - GitHub repo path (e.g.,username/repo)publish_branch(Optional) - Branch for publishing (default:main)preview_branch(Optional) - Branch for previews (default:main)
Example:
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"title": "My Course",
"slug": "my-course",
"language_id": "1",
"sync_mode": "monaco"
}' \
https://leanpub.com/courses.json
Response (success):
{
"success": true,
"course": {
"slug": "my-course",
"title": "My Course",
"state": "unpublished",
"url": "https://leanpub.com/c/my-course"
}
}
Response (error):
{
"success": false,
"errors": ["Slug has already been taken"]
}
Create Track
Creates a new track. Tracks are curated collections of courses that guide learners through a structured learning path.
POST https://leanpub.com/tracks.json
Parameters:
title(Required) - The track titleslug(Required) - URL-friendly identifier (must be unique)publisher_id(Optional) - Publisher ID for organization publishing. Omit this for self-publishing (most authors)
Example:
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"title": "My Track",
"slug": "my-track"
}' \
https://leanpub.com/tracks.json
Response (success):
{
"success": true,
"track": {
"slug": "my-track",
"title": "My Track",
"state": "unpublished",
"url": "https://leanpub.com/t/my-track"
}
}
Response (error):
{
"success": false,
"errors": ["Slug has already been taken"]
}
Check Book Exists
Checks if a book exists and you are an author of it. Returns book info if you have access, or 404 if the book doesn't exist or you're not an author.
GET https://leanpub.com/{slug}/exists.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG/exists.json?api_key=YOUR_API_KEY"
Response (success - you are an author):
{
"exists": true,
"book": {
"slug": "YOUR_BOOK_SLUG",
"title": "My Book",
"state": "published"
}
}
Response (not found or not an author):
{
"exists": false,
"error": "Book not found or you are not an author of this book"
}
Preview & Publish
Preview Book
Starts a full preview generation of your book.
POST https://leanpub.com/{slug}/preview.json
Example:
curl -X POST -d "api_key=YOUR_API_KEY" "https://leanpub.com/YOUR_BOOK_SLUG/preview.json"
Response:
{
"success": true
}
Preview Subset
Generates a preview using only the files listed in Subset.txt. This creates only a PDF for faster iteration.
POST https://leanpub.com/{slug}/preview/subset.json
Example:
curl -X POST -d "api_key=YOUR_API_KEY" "https://leanpub.com/YOUR_BOOK_SLUG/preview/subset.json"
Response:
{
"success": true
}
Preview Single File
Generates a preview from raw Markdown content. Useful for previewing a single chapter without modifying Subset.txt. The output is saved as {slug}-single-file.pdf in your Dropbox previews folder.
POST https://leanpub.com/{slug}/preview/single.json
Content-Type: text/plain
# Chapter Title
Your markdown content here...
Example with curl:
curl -X POST \
-H "Content-Type: text/plain" \
--data-binary "# Test Chapter
This is a test chapter for the single file preview API.
## Section 1
Some content here." \
"https://leanpub.com/YOUR_BOOK_SLUG/preview/single.json?api_key=YOUR_API_KEY"
Ruby Script Example:
#!/usr/bin/env ruby
require "httpclient"
slug = ARGV[0]
filename = ARGV[1]
api_key = ENV["LEANPUB_API_KEY"]
content = File.read(filename)
headers = { "Content-Type" => "text/plain"}
url = "https://leanpub.com/#{slug}/preview/single.json?api_key=#{api_key}"
HTTPClient.new.post(url, :body => content, :header => headers)
Response:
{
"success": true
}
Publish Book
Publishes your book, making the latest version available to readers.
POST https://leanpub.com/{slug}/publish.json
Parameters:
publish[email_readers](boolean): Send release notification to readers (default: false)publish[release_notes](string): Release notes included in the notification email (URL-encode if needed)
Examples:
Publish without notifying readers:
curl -d "api_key=YOUR_API_KEY" \
https://leanpub.com/YOUR_BOOK_SLUG/publish.json
Publish and notify readers:
curl -d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=true" \
-d "publish[release_notes]=Fixed typos in chapter 3" \
https://leanpub.com/YOUR_BOOK_SLUG/publish.json
Publish with multi-line release notes (URL-encoded):
curl -d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=true" \
-d "publish[release_notes]=New+in+this+release%3A%0A%0A-+Fixed+typos%0A-+Added+chapter+4" \
https://leanpub.com/YOUR_BOOK_SLUG/publish.json
Response:
{
"success": true
}
Unpublish Book
Unpublishes a book, making it no longer available for purchase. Only the primary author can unpublish a book.
POST https://leanpub.com/{slug}/unpublish.json
Example:
curl -X POST -d "api_key=YOUR_API_KEY" \
https://leanpub.com/YOUR_BOOK_SLUG/unpublish.json
Response (success):
{
"success": true,
"book": {
"slug": "YOUR_BOOK_SLUG",
"state": "unpublished"
}
}
Response (not primary author):
{
"success": false,
"error": "Only the primary author can unpublish a book"
}
Retire Book
Retires a published book. Retired books are no longer available for new purchases, but existing readers retain access. Only the primary author can retire a book.
POST https://leanpub.com/{slug}/retire.json
Example:
curl -X POST -d "api_key=YOUR_API_KEY" \
https://leanpub.com/YOUR_BOOK_SLUG/retire.json
Response (success):
{
"success": true,
"book": {
"slug": "YOUR_BOOK_SLUG",
"state": "retired"
}
}
Response (not primary author):
{
"success": false,
"error": "Only the primary author can retire a book"
}
Close Book
Closes a book completely. Closed books are hidden from the store and cannot be purchased. Only the primary author can close a book.
POST https://leanpub.com/{slug}/close.json
Example:
curl -X POST -d "api_key=YOUR_API_KEY" \
https://leanpub.com/YOUR_BOOK_SLUG/close.json
Response (success):
{
"success": true,
"book": {
"slug": "YOUR_BOOK_SLUG",
"state": "closed"
}
}
Response (not primary author):
{
"success": false,
"error": "Only the primary author can close a book"
}
Get Job Status
Check the status of a running preview or publish job.
GET https://leanpub.com/{slug}/job_status.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG/job_status.json?api_key=YOUR_API_KEY"
Response (job in progress):
{
"num": 8,
"total": 28,
"job_type": "GenerateBookJob#preview",
"message": "Generating PDF...",
"status": "working",
"name": "Preview YOUR_BOOK_SLUG",
"time": 1376073552,
"options": {
"requested_by": "you@example.com",
"slug": "YOUR_BOOK_SLUG",
"action": "preview"
}
}
Response fields:
num: Current step numbertotal: Total number of stepsjob_type: The type of job running (e.g.,GenerateBookJob#preview,GenerateBookJob#publish)message: Human-readable status messagestatus: Job status (working, etc.)time: Unix timestamp when the job started
The num and total fields can be used to display progress like "Step 8 of 28".
Response (job complete):
Returns an empty object {} when no job is running.
Poll this endpoint to track progress, but limit requests to once every 5 seconds.
Utility Script: Poll Until Complete
while true; do
status=$(curl -s "https://leanpub.com/YOUR_BOOK_SLUG/job_status.json?api_key=YOUR_API_KEY")
echo "$status" | jq .
if [ "$status" = "{}" ]; then
echo "Job complete!"
break
fi
sleep 5
done
Sales & Royalties
Get Royalties Summary
Returns a summary of your book's sales and royalties. Available in both JSON and XML formats.
JSON:
GET https://leanpub.com/{slug}/royalties.json
XML:
GET https://leanpub.com/{slug}/royalties.xml
Here's an example with curl:
# JSON
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.json?api_key=YOUR_API_KEY"
# XML
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.xml?api_key=YOUR_API_KEY"
Response (JSON):
{
"total_royalties": 12500.0,
"royalties_bundled": 500.0,
"royalties_unbundled": 12000.0,
"last_week_royalties": 250.0,
"royalties_to_revenue_ratio": 0.8,
"total_revenue": 15625.0,
"revenue_bundled": 625.0,
"revenue_unbundled": 15000.0,
"total_copies_sold": 1234,
"num_copies_sold_bundled": 50,
"num_copies_sold_unbundled": 1184
}
Get Individual Purchases
Returns a paginated list of individual purchases for your book. Available in both JSON and XML formats.
JSON:
GET https://leanpub.com/{slug}/individual_purchases.json
XML:
GET https://leanpub.com/{slug}/individual_purchases.xml
Parameters:
-
page(integer): Page number (default: 1, 50 items per page) -
email(string, optional): Filter purchases by reader email address. Only returns results when the reader opted to share their email with the author at purchase time. The match is case-insensitive. The filter returns an empty list when:- No reader with that email has purchased the book
- The reader purchased the book but did not opt to share their email with the author
This means the
emailfilter cannot be used to determine whether someone purchased your book — it only finds purchases where the reader chose to share their contact information with you.
Examples:
# JSON
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.json?api_key=YOUR_API_KEY"
# XML
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.xml?api_key=YOUR_API_KEY"
# JSON with pagination
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.json?api_key=YOUR_API_KEY&page=2"
# XML with pagination
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.xml?api_key=YOUR_API_KEY&page=2"
# Filter by reader email (only returns results if the reader shared their email)
curl "https://leanpub.com/YOUR_BOOK_SLUG/individual_purchases.json?api_key=YOUR_API_KEY&email=reader@example.com"
Response (JSON):
[
{
"id": "123",
"author_royalties": 8.0,
"state": "paid",
"payable_at": "2024-02-01T00:00:00Z",
"purchased_package_id": "456",
"free": false,
"user_email": "reader@example.com"
}
]
Coupons
List Coupons
Returns all coupons for a book. Available in both JSON and XML formats.
JSON:
GET https://leanpub.com/{slug}/coupons.json
XML:
GET https://leanpub.com/{slug}/coupons.xml
Examples:
# JSON
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons.json?api_key=YOUR_API_KEY"
# XML
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons.xml?api_key=YOUR_API_KEY"
Response (JSON):
[
{
"coupon_code": "LAUNCH50",
"created_at": "2024-01-01T00:00:00Z",
"package_discounts": [
{
"package_slug": "book",
"discounted_price": 4.99
}
],
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"max_uses": 100,
"num_uses": 25,
"note": "Launch promotion",
"suspended": false,
"book_slug": "YOUR_BOOK_SLUG"
}
]
Get Single Coupon
Returns details for a specific coupon.
GET https://leanpub.com/{slug}/coupons/{coupon_code}.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG/coupons/LAUNCH50.json?api_key=YOUR_API_KEY"
Response:
{
"coupon_code": "LAUNCH50",
"created_at": "2024-01-01T00:00:00Z",
"package_discounts": [
{
"package_slug": "book",
"discounted_price": 4.99
}
],
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"max_uses": 100,
"num_uses": 25,
"note": "Launch promotion",
"suspended": false,
"book_slug": "YOUR_BOOK_SLUG"
}
Create Coupon
Creates a new coupon for your book.
POST https://leanpub.com/{slug}/coupons.json
Parameters:
coupon_code(required): Unique code for this couponpackage_discounts_attributes(required): Array of package discountsstart_date(required): Start date (YYYY-MM-DD)end_date(optional): End date (YYYY-MM-DD)max_uses(optional): Maximum number of uses (null = unlimited)note(optional): Internal note about this couponsuspended(optional): Whether the coupon is suspended (default: false)
Example (JSON):
curl -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"coupon": {
"coupon_code": "SAVE50",
"package_discounts_attributes": [
{"package_slug": "book", "discounted_price": 4.99}
],
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"max_uses": 100,
"note": "Half price promotion"
}
}' \
https://leanpub.com/YOUR_BOOK_SLUG/coupons.json
Example (form data):
curl -d "api_key=YOUR_API_KEY" \
-d "coupon[coupon_code]=SAVE50" \
-d "coupon[package_discounts_attributes][][package_slug]=book" \
-d "coupon[package_discounts_attributes][][discounted_price]=4.99" \
-d "coupon[start_date]=2024-01-01" \
-d "coupon[end_date]=2024-12-31" \
https://leanpub.com/YOUR_BOOK_SLUG/coupons.json
Response: Returns the created coupon (see Get Single Coupon).
Update Coupon
Updates an existing coupon. Only include fields you want to change.
PUT https://leanpub.com/{slug}/coupons/{coupon_code}.json
Example (JSON):
curl -X PUT \
-H "Content-Type: application/json" \
-d '{"api_key": "YOUR_API_KEY", "suspended": true, "note": "Updated via API"}' \
https://leanpub.com/YOUR_BOOK_SLUG/coupons/SAVE50.json
Example (form data):
curl -X PUT \
-d "api_key=YOUR_API_KEY" \
-d "suspended=false" \
-d "max_uses=200" \
https://leanpub.com/YOUR_BOOK_SLUG/coupons/SAVE50.json
Response: Returns the updated coupon (see Get Single Coupon).
Account
Verify API Key
Validates your API key and returns information about the authenticated user. Useful for testing your integration.
GET https://leanpub.com/current_user.json
Example:
curl "https://leanpub.com/current_user.json?api_key=YOUR_API_KEY"
Response:
{
"username": "YOUR_USERNAME",
"email": "you@example.com"
}
Get Reader Emails
Returns email addresses of readers who have opted to share their email with you.
GET https://leanpub.com/u/{username}/reader_emails.json
Parameters:
type(string): Filter by purchase type:all,book, orcourse(default:all)purchase_type(string): Alias fortype(for backwards compatibility)
Examples:
# All readers
curl "https://leanpub.com/u/YOUR_USERNAME/reader_emails.json?api_key=YOUR_API_KEY"
# Books only (new parameter)
curl "https://leanpub.com/u/YOUR_USERNAME/reader_emails.json?api_key=YOUR_API_KEY&type=book"
# Courses only (new parameter)
curl "https://leanpub.com/u/YOUR_USERNAME/reader_emails.json?api_key=YOUR_API_KEY&type=course"
# Books only (legacy parameter - still works)
curl "https://leanpub.com/u/YOUR_USERNAME/reader_emails.json?api_key=YOUR_API_KEY&purchase_type=book"
Response:
["reader1@example.com", "reader2@example.com"]
Get Book Reader Emails
Returns email addresses of readers who have purchased a specific book and opted to share their email with you.
GET https://leanpub.com/{slug}/reader_emails.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG/reader_emails.json?api_key=YOUR_API_KEY"
Response:
["reader1@example.com", "reader2@example.com"]
Register Interest
Registers an email address to be notified when a book is published. This is useful for unpublished books where readers want to know when it becomes available.
POST https://leanpub.com/{slug}/interested.json
Parameters:
name(required): The name of the interested readeremail(required): Email address to notifyshare_email_with_author(boolean): Whether to share the email with the author (default: false)
Example:
curl -X POST -H "Content-Type: application/json" \
-d '{
"api_key": "YOUR_API_KEY",
"name": "Test Reader",
"email": "reader@example.com",
"share_email_with_author": true
}' \
https://leanpub.com/YOUR_BOOK_SLUG/interested.json
Response (success):
{
"success": true,
"message": "You will be notified when this book is published"
}
Response (error):
{
"success": false,
"errors": ["Email has already registered interest"]
}
Get Interested Readers
Returns a list of people who registered interest in a book and opted to share their email with the author.
GET https://leanpub.com/{slug}/interested_readers.json
Example:
curl "https://leanpub.com/YOUR_BOOK_SLUG/interested_readers.json?api_key=YOUR_API_KEY"
Response:
[
{
"name": "Test Reader",
"email": "reader@example.com"
}
]
Only readers who set share_email_with_author: true when registering interest will appear in this list.
Downloading Book Files
The book summary endpoint returns secret URLs for downloading your book files:
pdf_preview_url/epub_preview_url- Latest previewpdf_published_url/epub_published_url- Latest published version
Get the download URLs first:
curl "https://leanpub.com/YOUR_BOOK_SLUG.json?api_key=YOUR_API_KEY" | \
jq '{pdf_preview_url, epub_preview_url, pdf_published_url, epub_published_url}'
Download using the secret URLs:
These URLs redirect to S3. Use the -L flag with curl to follow redirects:
# Download using the secret URLs (use actual URLs from above)
curl -L "https://leanpub.com/s/YOUR-SECRET-ID.pdf" > book.pdf
curl -L "https://leanpub.com/s/YOUR-SECRET-ID.epub" > book.epub
Error Handling
HTTP Status Codes:
- 200: Success
- 201: Created (for POST requests)
- 401: Unauthorized - Invalid or missing API key
- 403: Forbidden - Insufficient permissions (e.g., not the primary author)
- 404: Not found - Book or resource doesn't exist, or you're not an author
- 405: Method not allowed
- 422: Validation error - Check the error response for details
- 500: Internal server error
Error Response Formats:
GET requests return null with the appropriate status code for backwards compatibility:
null
POST/PUT requests return a structured error response:
{
"success": false,
"error": "Unauthorized"
}
For validation errors with multiple messages:
{
"success": false,
"errors": ["Title is required", "Slug is required"]
}
Rate Limits
The API enforces per-key rate limits to ensure fair usage. Both REST and MCP endpoints share the same rate limit pool.
| Tier | Limit | Endpoints | |------|-------|-----------| | READ | 30/min | Book summary, check exists, job status, royalties, get coupon | | LIST | 15/min | Coupons list, purchases, reader emails, interested readers | | WRITE | 15/min | Create/update coupon, register interest | | HEAVY | 3/min | Preview, publish (books & courses) | | STATE_CHANGE | 5/min | Unpublish, retire, close | | CREATE | 2/min | Create book, bundle, course, track |
These limits are designed so authors never hit them during regular use. If you exceed a limit, you'll receive a 429 Too Many Requests response with a Retry-After header indicating how many seconds to wait.
Example 429 response:
{
"error": "Rate limit exceeded for preview/publish operations. Limit: 3 requests per 1 minutes. Try again in 45 seconds."
}
Tips:
- Poll
job_statusno more than once every 5 seconds - Add small delays between calls if making many requests
- The
Retry-Afterheader tells you exactly how long to wait
MCP Server (AI Assistant Integration)
⚠️ Early Beta Warning: The Leanpub MCP Server is in very early beta. Expect things to not work. We are actively developing this feature and it may be unstable, have missing functionality, or behave unexpectedly. If you run into issues, please email hello@leanpub.com.
The Leanpub MCP Server lets AI assistants like Claude interact with your Leanpub account through the Model Context Protocol. This enables AI-powered book management, publishing automation, and more.
Endpoint
The MCP server is hosted at:
https://leanpub.com/api/mcp
No installation required—just configure your AI client to connect to this URL with your API key.
Claude Desktop Configuration
Add to your Claude Desktop config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows:
%APPDATA%\Claude\claude_desktop_config.json
Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"leanpub": {
"url": "https://leanpub.com/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
Replace YOUR_API_KEY with your Leanpub API key from your settings.
Available Tools
The MCP server provides 29 tools organized by function:
- Authentication:
verify_api_key - Book Information:
get_book,check_book_exists - Product Creation:
create_book,create_bundle,create_course,create_track(all work for regular self-publishing authors—no publisher ID needed) - Preview & Publish:
preview_book,preview_subset,preview_single,publish_book - Book State:
unpublish_book,retire_book,close_book - Job Status:
get_job_status - Interested Readers:
register_interest,get_interested_readers - Reader Emails:
get_user_reader_emails,get_book_reader_emails - Royalties:
get_royalties - Purchases:
get_individual_purchases - Coupons:
list_coupons,get_coupon,create_coupon,update_coupon - Courses:
preview_course,publish_course,preview_org_course,publish_org_course
Rate Limits
The MCP server enforces rate limits to ensure fair usage:
- READ (30/min): Book info, royalties, job status
- LIST (15/min): Coupons, purchases, reader emails
- WRITE (15/min): Create/update coupons, register interest
- HEAVY (3/min): Preview, publish (books & courses)
- STATE_CHANGE (5/min): Unpublish, retire, close
- CREATE (2/min): Create book, bundle, course, track
These limits are designed so authors never hit limits during regular use.
Example Conversation
Once configured, you can ask Claude to help with your Leanpub books:
You: Check the status of my book "lean-publishing"
Claude: uses get_book tool
Your book "Lean Publishing" has:
- 45,000 words across 150 pages
- 500 copies sold with $5,000 total revenue
- Last published on January 15, 2024
You: Create a 50% off coupon for the holidays
Claude: uses create_coupon tool
Done! I created coupon "HOLIDAY50" that gives 50% off ($9.99 instead of $19.99). It's valid through December 31st.
You: Create a new bundle called "The Complete Web Dev Collection" with slug "web-dev-collection"
Claude: uses create_bundle tool
Done! I created your bundle "The Complete Web Dev Collection" at https://leanpub.com/b/web-dev-collection. It's currently unpublished — you can add books to it and publish when ready.
You: Preview my latest changes and let me know when it's done
Claude: uses preview_book and get_job_status tools
Preview generation started. I'll check on the progress... The preview is complete! You can find the PDF in your Dropbox previews folder.
Your Book's Slug
Your book's slug is the URL-friendly identifier in your book's URL:
https://leanpub.com/YOUR_BOOK_SLUG
^^^^^^^^^^^^^^ this is the slug
Course API
Leanpub courses can be previewed and published via the API, similar to books.
Your Course's Slug
Courses have different URL patterns depending on how they're published:
- Self-published:
https://leanpub.com/c/YOUR_COURSE_SLUG— needsYOUR_COURSE_SLUGslug - Organization:
https://leanpub.com/courses/YOUR_ORG_SLUG/YOUR_COURSE_SLUG— needs bothYOUR_ORG_SLUGandYOUR_COURSE_SLUG - University:
https://leanpub.com/universities/courses/YOUR_UNIVERSITY_SLUG/YOUR_COURSE_SLUG— needs bothYOUR_UNIVERSITY_SLUGandYOUR_COURSE_SLUG
Preview Course
Starts a preview generation of your course.
Self-published course:
POST https://leanpub.com/c/{slug}/preview.json
Organization course:
POST https://leanpub.com/c/{organization_slug}/{slug}/preview.json
University course:
POST https://leanpub.com/c/{university_slug}/{slug}/preview.json
Examples:
# Self-published course
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/c/YOUR_COURSE_SLUG/preview.json
# Organization course
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/c/YOUR_ORG_SLUG/YOUR_COURSE_SLUG/preview.json
# University course
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/c/YOUR_UNIVERSITY_SLUG/YOUR_COURSE_SLUG/preview.json
Response (success):
{
"success": true
}
Response (course not found):
{
"success": false
}
Publish Course
Publishes your course, making the latest version available to learners.
Self-published course:
POST https://leanpub.com/c/{slug}/publish.json
Organization course:
POST https://leanpub.com/c/{organization_slug}/{slug}/publish.json
University course:
POST https://leanpub.com/c/{university_slug}/{slug}/publish.json
Parameters:
publish[email_readers](Optional) - Whether to notify learners (default: true)publish[release_notes](Optional) - Release notes to include in notificationpublish[percent_complete](Optional) - Set course percent complete (integer)
Examples:
# Self-published course - publish without notifying learners
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/c/YOUR_COURSE_SLUG/publish.json
# Self-published course - publish with release notes and notification
curl -d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=true" \
-d "publish[release_notes]=Updated lesson content" \
https://leanpub.com/c/YOUR_COURSE_SLUG/publish.json
# Organization course
curl -d "api_key=YOUR_API_KEY" \
-d "publish[email_readers]=true" \
-d "publish[release_notes]=New lessons added" \
https://leanpub.com/c/YOUR_ORG_SLUG/YOUR_COURSE_SLUG/publish.json
# University course
curl -d "api_key=YOUR_API_KEY" https://leanpub.com/c/YOUR_UNIVERSITY_SLUG/YOUR_COURSE_SLUG/publish.json
Response (success):
{
"success": true
}
Response (course not found):
{
"success": false
}
XML Endpoints
XML endpoints are supported for backwards compatibility with the legacy API. The following endpoints support both JSON and XML formats:
/{slug}/royalties.xml- Royalties summary/{slug}/individual_purchases.xml- Individual purchases (supports pagination)/{slug}/coupons.xml- List coupons
Simply use the .xml extension instead of .json to get XML responses:
# JSON
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.json?api_key=YOUR_API_KEY"
# XML
curl "https://leanpub.com/YOUR_BOOK_SLUG/royalties.xml?api_key=YOUR_API_KEY"
Migration Notes from Legacy API
If you have an existing integration with the Leanpub API, here's what you need to know:
What's the Same
- All JSON endpoints work identically
- All XML endpoints work identically (royalties, individual_purchases, coupons)
- Authentication via
api_keyquery param or POST body - Request/response formats are compatible
- Both
typeandpurchase_typework for reader_emails - Course API (
/c/...) endpoints work identically
What's Different
Full compatibility has been achieved with the legacy API. Your existing integrations should continue to work without any changes.
New endpoints added:
POST /books.json- Create a new book (Browser or GitHub mode)POST /bundles.json- Create a new bundlePOST /courses.json- Create a new coursePOST /tracks.json- Create a new trackPOST /{slug}/interested.json- Register interest in an unpublished bookGET /{slug}/interested_readers.json- Get list of interested readers (for authors)POST /api/mcp- MCP server endpoint for AI assistant integration
API Test Script
The following Ruby script demonstrates the complete API workflow, including creating a book, previewing, publishing, managing lifecycle states, and testing various endpoints. You can use this as a reference for your own integrations.
Usage:
./test_api.rb --api-key YOUR_API_KEY --user YOUR_USERNAME
# With existing book tests
./test_api.rb --api-key YOUR_API_KEY --user YOUR_USERNAME --existing-book YOUR_BOOK_SLUG
Script:
#!/usr/bin/env ruby
# frozen_string_literal: true
require "net/http"
require "uri"
require "json"
require "optparse"
require "securerandom"
options = {}
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on("--api-key KEY", "Your Leanpub API key (required)") do |v|
options[:api_key] = v
end
opts.on("--user USERNAME", "Username for existing book tests (required)") do |v|
options[:user] = v
end
opts.on("--existing-book SLUG", "Test existing book endpoints with this slug (optional)") do |v|
options[:existing_book] = v
end
opts.on("-h", "--help", "Show this help") do
puts opts
puts
puts "Examples:"
puts " #{$0} --api-key YOUR_API_KEY --user YOUR_USERNAME"
puts " #{$0} --api-key YOUR_API_KEY --user YOUR_USERNAME --existing-book YOUR_BOOK_SLUG"
exit
end
end.parse!
if options[:api_key].nil? || options[:user].nil?
puts "Error: --api-key and --user are required."
puts
puts "Usage: #{$0} --api-key KEY --user USERNAME"
puts
puts "Examples:"
puts " #{$0} --api-key YOUR_API_KEY --user YOUR_USERNAME"
puts " #{$0} --api-key YOUR_API_KEY --user YOUR_USERNAME --existing-book YOUR_BOOK_SLUG"
exit 1
end
API_KEY = options[:api_key]
BASE_URL = "https://leanpub.com"
TEST_USER = options[:user]
EXISTING_BOOK = options[:existing_book]
# Generate a unique test book slug
TEST_BOOK_SLUG = "api-test-#{SecureRandom.uuid}"
TEST_BOOK_TITLE = "API Test Book #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
TEST_INTERESTED_EMAIL = "#{SecureRandom.uuid}@test.com"
def create_http(uri)
http = Net::HTTP.new(uri.host, uri.port)
if uri.scheme == "https"
http.use_ssl = true
end
http
end
def prompt_continue(step_name)
print "\n\033[33mPress Enter to continue to: #{step_name} (or 'q' to quit)\033[0m "
input = gets.chomp
exit(0) if input.downcase == "q"
end
def build_curl_command(method, path, body: nil, content_type: nil)
uri = URI("#{BASE_URL}#{path}")
case method
when :get
query_params = { api_key: API_KEY }.merge(URI.decode_www_form(uri.query || "").to_h)
uri.query = URI.encode_www_form(query_params)
"curl -s \"#{uri}\""
when :post
if content_type == "application/json"
json_body = body.to_json
"curl -s -X POST -H \"Content-Type: application/json\" \\\n -d '#{json_body}' \\\n \"#{uri}\""
else
form_data = (body || {}).merge(api_key: API_KEY)
data_args = form_data.map { |k, v| "-d \"#{k}=#{v}\"" }.join(" \\\n ")
"curl -s -X POST \\\n #{data_args} \\\n \"#{uri}\""
end
when :put
form_data = (body || {}).merge(api_key: API_KEY)
data_args = form_data.map { |k, v| "-d \"#{k}=#{v}\"" }.join(" \\\n ")
"curl -s -X PUT \\\n #{data_args} \\\n \"#{uri}\""
end
end
def run_request(method, path, body: nil, content_type: nil, format: :json)
# Show curl command
curl_cmd = build_curl_command(method, path, body: body, content_type: content_type)
puts "\033[90m$ #{curl_cmd}\033[0m"
puts
uri = URI("#{BASE_URL}#{path}")
http = create_http(uri)
case method
when :get
uri.query = URI.encode_www_form({ api_key: API_KEY }.merge(URI.decode_www_form(uri.query || "").to_h))
request = Net::HTTP::Get.new(uri)
when :post
request = Net::HTTP::Post.new(uri)
if content_type == "application/json"
request["Content-Type"] = "application/json"
request.body = body.to_json
else
request.set_form_data((body || {}).merge(api_key: API_KEY))
end
when :put
request = Net::HTTP::Put.new(uri)
request.set_form_data((body || {}).merge(api_key: API_KEY))
end
response = http.request(request)
puts "\033[36mStatus: #{response.code}\033[0m"
if format == :json && response.body && !response.body.empty?
begin
parsed = JSON.parse(response.body)
puts JSON.pretty_generate(parsed)
rescue JSON::ParserError
puts response.body
end
else
puts response.body
end
response
end
def test_step(number, name, &block)
puts "\n" + "=" * 60
puts "\033[32m#{number}. #{name}\033[0m"
puts "=" * 60
block.call
end
def wait_for_job(slug, max_wait: 120)
puts "\033[90mWaiting for job to complete (max #{max_wait}s)...\033[0m"
start_time = Time.now
loop do
uri = URI("#{BASE_URL}/#{slug}/job_status.json")
uri.query = URI.encode_www_form({ api_key: API_KEY })
http = create_http(uri)
request = Net::HTTP::Get.new(uri)
response = http.request(request)
if response.body == "{}" || response.body.nil? || response.body.empty?
puts "\033[32mJob complete!\033[0m"
return true
end
begin
status = JSON.parse(response.body)
if status["message"]
print "\r\033[90m #{status['message']} (#{status['num']}/#{status['total']})\033[0m"
print " " * 20 # Clear any remaining characters
end
rescue JSON::ParserError
# Ignore parse errors
end
if Time.now - start_time > max_wait
puts "\n\033[31mTimeout waiting for job!\033[0m"
return false
end
sleep 3
end
end
def fetch_book_info(slug)
uri = URI("#{BASE_URL}/#{slug}.json")
uri.query = URI.encode_www_form({ api_key: API_KEY })
http = create_http(uri)
request = Net::HTTP::Get.new(uri)
response = http.request(request)
return nil unless response.code == "200"
JSON.parse(response.body)
rescue JSON::ParserError
nil
end
def prompt_open_url(url, description)
return unless url && !url.empty?
puts "\n\033[36m#{description}:\033[0m #{url}"
print "\033[33mPress Enter to open in browser (or 's' to skip)\033[0m "
input = gets.chomp
return if input.downcase == "s"
# Open URL in default browser (macOS)
system("open", url)
end
# Start testing
puts "\033[1m\033[35m"
puts "=" * 60
puts " LEANPUB API TEST SCRIPT"
puts "=" * 60
puts "\033[0m"
puts
puts "\033[1m\033[41m\033[37m WARNING \033[0m\033[1m\033[31m Job completion detection is not working correctly!\033[0m"
puts "\033[31m Please manually verify that preview/publish jobs complete before proceeding.\033[0m"
puts
puts "Base URL: #{BASE_URL}"
puts "API Key: #{API_KEY[0..10]}..."
puts "Test User: #{TEST_USER}"
puts
puts "\033[1mBook Lifecycle Test:\033[0m"
puts " Slug: #{TEST_BOOK_SLUG}"
puts " Title: #{TEST_BOOK_TITLE}"
puts " Interested Email: #{TEST_INTERESTED_EMAIL}"
step = 0
# ============================================================
# PART 1: Verify API Key
# ============================================================
prompt_continue("Verify API Key")
test_step(step += 1, "Verify API Key") do
run_request(:get, "/current_user.json")
end
# ============================================================
# PART 2: Book Lifecycle (Create -> Preview -> Publish -> Unpublish -> Retire -> Close)
# ============================================================
prompt_continue("Check if test book exists (should return exists: false)")
test_step(step += 1, "Check Book Exists (expect exists: false)") do
run_request(:get, "/#{TEST_BOOK_SLUG}/exists.json")
end
prompt_continue("Create Book (Browser mode)")
test_step(step += 1, "Create Book") do
response = run_request(:post, "/books.json",
body: {
api_key: API_KEY,
title: TEST_BOOK_TITLE,
slug: TEST_BOOK_SLUG,
sync_mode: "monaco"
},
content_type: "application/json"
)
if response.code == "200" || response.code == "201"
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page")
end
end
prompt_continue("Verify book now exists")
test_step(step += 1, "Check Book Exists (expect 200)") do
run_request(:get, "/#{TEST_BOOK_SLUG}/exists.json")
end
prompt_continue("Get Book Summary")
test_step(step += 1, "Get Book Summary") do
run_request(:get, "/#{TEST_BOOK_SLUG}.json")
end
prompt_continue("Preview Book")
test_step(step += 1, "Preview Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/preview.json")
if response.code == "200"
puts
if wait_for_job(TEST_BOOK_SLUG)
book_info = fetch_book_info(TEST_BOOK_SLUG)
if book_info
prompt_open_url(book_info["pdf_preview_url"], "PDF Preview")
prompt_open_url(book_info["epub_preview_url"], "EPUB Preview")
end
end
end
end
prompt_continue("Register interest in the unpublished book")
test_step(step += 1, "Register Interest (#{TEST_INTERESTED_EMAIL})") do
puts "\033[90mThis email will be notified when the book is published:\033[0m"
puts " #{TEST_INTERESTED_EMAIL}"
puts
run_request(:post, "/#{TEST_BOOK_SLUG}/interested.json",
body: {
api_key: API_KEY,
name: "API Test User",
email: TEST_INTERESTED_EMAIL,
share_email_with_author: true
},
content_type: "application/json"
)
end
prompt_continue("Get Interested Readers (before publish)")
test_step(step += 1, "Get Interested Readers") do
puts "\033[90mChecking if #{TEST_INTERESTED_EMAIL} appears (they opted to share email):\033[0m"
puts
run_request(:get, "/#{TEST_BOOK_SLUG}/interested_readers.json")
end
prompt_continue("Publish Book")
test_step(step += 1, "Publish Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/publish.json",
body: {
"publish[email_readers]" => false,
"publish[release_notes]" => "Initial publish via API test"
}
)
if response.code == "200"
puts
if wait_for_job(TEST_BOOK_SLUG)
book_info = fetch_book_info(TEST_BOOK_SLUG)
if book_info
prompt_open_url(book_info["pdf_published_url"], "PDF Published")
prompt_open_url(book_info["epub_published_url"], "EPUB Published")
end
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page (Published)")
end
end
end
prompt_continue("Unpublish Book")
test_step(step += 1, "Unpublish Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/unpublish.json")
if response.code == "200"
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page (Unpublished)")
end
end
prompt_continue("Check book state after unpublish")
test_step(step += 1, "Check Book State (should be unpublished)") do
run_request(:get, "/#{TEST_BOOK_SLUG}/exists.json")
end
# Note: To retire, book must be published first. Let's re-publish then retire.
prompt_continue("Re-publish Book (required before retire)")
test_step(step += 1, "Re-publish Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/publish.json",
body: {
"publish[email_readers]" => false
}
)
if response.code == "200"
puts
if wait_for_job(TEST_BOOK_SLUG)
book_info = fetch_book_info(TEST_BOOK_SLUG)
if book_info
prompt_open_url(book_info["pdf_published_url"], "PDF Published")
end
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page (Re-published)")
end
end
end
prompt_continue("Retire Book")
test_step(step += 1, "Retire Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/retire.json")
if response.code == "200"
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page (Retired)")
end
end
prompt_continue("Check book state after retire")
test_step(step += 1, "Check Book State (should be retired)") do
run_request(:get, "/#{TEST_BOOK_SLUG}/exists.json")
end
prompt_continue("Close Book")
test_step(step += 1, "Close Book") do
response = run_request(:post, "/#{TEST_BOOK_SLUG}/close.json")
if response.code == "200"
prompt_open_url("#{BASE_URL}/#{TEST_BOOK_SLUG}", "Book Landing Page (Closed)")
end
end
prompt_continue("Check book state after close")
test_step(step += 1, "Check Book State (should be closed)") do
run_request(:get, "/#{TEST_BOOK_SLUG}/exists.json")
end
# ============================================================
# PART 3: Existing Book Tests (if --existing-book provided)
# ============================================================
if EXISTING_BOOK
puts "\n" + "=" * 60
puts "\033[1m\033[35m EXISTING BOOK TESTS (#{EXISTING_BOOK})\033[0m"
puts "=" * 60
prompt_continue("Royalties (JSON)")
test_step(step += 1, "Royalties (JSON)") do
run_request(:get, "/#{EXISTING_BOOK}/royalties.json")
end
prompt_continue("Royalties (XML)")
test_step(step += 1, "Royalties (XML)") do
run_request(:get, "/#{EXISTING_BOOK}/royalties.xml", format: :xml)
end
prompt_continue("Book Reader Emails")
test_step(step += 1, "Book Reader Emails") do
run_request(:get, "/#{EXISTING_BOOK}/reader_emails.json")
end
prompt_continue("Individual Purchases (JSON)")
test_step(step += 1, "Individual Purchases (JSON)") do
run_request(:get, "/#{EXISTING_BOOK}/individual_purchases.json")
end
prompt_continue("Individual Purchases (XML)")
test_step(step += 1, "Individual Purchases (XML)") do
run_request(:get, "/#{EXISTING_BOOK}/individual_purchases.xml", format: :xml)
end
prompt_continue("List Coupons (JSON)")
test_step(step += 1, "List Coupons (JSON)") do
run_request(:get, "/#{EXISTING_BOOK}/coupons.json")
end
prompt_continue("List Coupons (XML)")
test_step(step += 1, "List Coupons (XML)") do
run_request(:get, "/#{EXISTING_BOOK}/coupons.xml", format: :xml)
end
prompt_continue("Create Coupon")
coupon_code = "TEST#{Time.now.to_i}"
test_step(step += 1, "Create Coupon (#{coupon_code})") do
run_request(:post, "/#{EXISTING_BOOK}/coupons.json",
body: {
api_key: API_KEY,
coupon: {
coupon_code: coupon_code,
package_discounts_attributes: [
{ package_slug: "book", discounted_price: 4.99 }
],
start_date: "2026-01-01",
end_date: "2026-12-31",
max_uses: 100,
note: "Test coupon from API script"
}
},
content_type: "application/json"
)
end
prompt_continue("Get Single Coupon")
test_step(step += 1, "Get Single Coupon (#{coupon_code})") do
run_request(:get, "/#{EXISTING_BOOK}/coupons/#{coupon_code}.json")
end
prompt_continue("Update Coupon")
test_step(step += 1, "Update Coupon (suspend #{coupon_code})") do
run_request(:put, "/#{EXISTING_BOOK}/coupons/#{coupon_code}.json",
body: { suspended: true, note: "Suspended via API test" }
)
end
end
# ============================================================
# Summary
# ============================================================
puts "\n" + "=" * 60
puts "\033[1m\033[32m ALL TESTS COMPLETE!\033[0m"
puts "=" * 60
puts
puts "Test book created: #{TEST_BOOK_SLUG}"
puts "Final state: closed"
puts
if EXISTING_BOOK
puts "Coupon '#{coupon_code}' was created and suspended on '#{EXISTING_BOOK}'."
end
puts