You keep a page of the books you read on your own site, and you want it to stay current. A public token lets the page read your books and your totals, so your account credential never reaches the page source.
Prompt for a coding agent
Build a books page for my own site from the Readwise Books API. The docs are at
https://readwise.io/books_api.
Read GET /public/{profile_id}/books and GET /public/{profile_id}/stats with
the header Books-Public-Token: YOUR_PUBLIC_TOKEN. Show the books I am reading now, the ones I
finished most recently, and my totals. One HTML file, CSS inline, no build step.
Page with the next URL until it is null, and cache the responses for five minutes,
which is what the API asks for.
Mint a public token on Your public tokens. Name it for the page it serves.
The token is shown once. Store it, and send it from the page in the Books-Public-Token header. The panel also shows your profile_id. It is your account id, and it goes in the feed URLs below.
The same call with status=currently_reading gives the shelf you are reading now. product is null for a book the catalog did not match, so read title and author from the book itself and treat the cover as optional. Leave year off here: it filters on finished_reading_at, which a book you are still reading does not have.
Your e-reader, plugin, or script records a reading session and posts it as it happens. A session posted here earns XP and counts in your stats, like a session read in the app, and reaches your feed when you send publish_to_feed: true.
Prompt for a coding agent
Record my reading sessions in Readwise Books from APP_OR_DEVICE. The docs are at
https://readwise.io/books_api.
Authenticate with the header Authorization: Token YOUR_TOKEN. For each session, send
POST /books first with the ISBN and status currently_reading, which answers existing
when the book is already in my library, then POST /sessions with the book_id it
returns, the start and end times, the format, and the start and end positions for that
format. Add publish_to_feed: true when the session belongs on my feed.
The book is often not in the library yet, so add it first. A book already in your library answers existing with the same book_id, so you can send this every time.
The book_id of the result is the book_id of the session. If you already hold the id, read the book back with GET /books and ids instead.
A request carries 1 to 10 sessions, so a device that reads while offline can post the sessions it held back in batches of 10. A session stays off your feed unless you send publish_to_feed: true; it counts in your stats either way.
You move a history out of a spreadsheet, a notebook, or a service with no importer. POST /books matches each book to a catalog product and puts it on a shelf with its dates and rating.
Prompt for a coding agent
Import my reading history from HISTORY_FILE into Readwise Books. The docs are at
https://readwise.io/books_api.
Authenticate with the header Authorization: Token YOUR_TOKEN. Send POST /books in
batches of 10, one item per row, carrying the ISBN, the shelf, the dates, and the
rating. A book the catalog does not carry is still added, with product null. Repeating a
book is safe, it answers existing. Skip POST /sessions unless the file carries session
times.
A request takes at most 10 items, so send a longer history in batches of 10. A book the catalog has no product for is added with product: null, and its metadata fills in later if we find it.
Skip this step when the export carries no session times. Most exports carry the books and the dates only, and the books step alone brings the history in.
Send the same books request twice and the matched item answers existing with the same book_id: the endpoint applies the dates, the rating, and the review to the book you already have, instead of adding a second copy. Sessions have no such match: a repeated POST /sessions records a second session, so keep track of the sessions you have written and replay only the books step after a stopped import.
You want the numbers behind a year: the totals, a day-by-day grid, and the books in the order you finished them.
Prompt for a coding agent
Make a year in review page for my 2026 reading from the Readwise Books API. The docs
are at https://readwise.io/books_api.
Authenticate with the header Authorization: Token YOUR_TOKEN. Read
GET /stats/summary?period=year&year=2026 for the totals,
GET /stats/sessions-by-day?from=2026-01-01&to=2026-12-31 for a day-by-day grid, and
GET /books?status=finished&order=-finished_reading_at for the books, keeping the ones
finished in 2026. One HTML file, CSS inline.
A recommendation arrives and you want the book on a shelf without buying it now.
Prompt for a coding agent
Add a book to my Readwise Books Want to Read shelf from the command line. The docs
are at https://readwise.io/books_api.
Authenticate with the header Authorization: Token YOUR_TOKEN. Take an ISBN as the
argument and send POST /books with status want_to_read. Print the book_id and whether
the result is created or existing.
Without an ISBN, send asin, the catalog id, or title with author. A new book lands on want_to_read whether or not the item sends status. When the book is already in your library, the result is existing with the id of the book you have, and that book moves to the shelf the item sends.
There are no endpoints for one record. To read specific records, pass their ids to the list endpoint: GET /books?ids=4815162,4790033.
ids takes 1 to 100 comma-separated ids. It combines with the other filters. An id that is not yours is left out of the results, as if it did not exist.
GET /books and GET /sessions can return only the records that changed after a time. Use this to keep a copy of your data current.
Request the full list with order=updated. For sessions, add include_deleted=true. Follow next to the last page.
Store the latest change time you received. For a deleted session, the change time is deleted_at. For every other record, it is updated.
Next time, add updated_after with the stored time, and keep the same parameters.
A book you delete stays in the list with status: "deleted", so a books sync needs nothing beyond updated_after.
A session you delete leaves the list. Send include_deleted=true to keep receiving it, with is_deleted: true and a deleted_at time. Without it, your copy never learns about deleted sessions.
order=updated sorts by the change time, so a deleted session sorts by its deleted_at. order=updated sorts ascending only, so a record that changes while you page still reaches you.
For books, updated_after covers the book record only: the shelf, the reading dates, manual_total_pages and the other length fields, and the catalog product. A change to rating, review, or progress does not change updated. To pick those changes up, request the full list again.
A Kindle or Goodreads purge removes books outright, and the removed book appears in no list. To detect those removals, compare the ids in a full list with your copy.
For sessions, updated_after covers the session record only: the times, the positions, the format, the time zone, the origin, the device, the feed publication, and deletion. A change to caption does not change updated, because the caption is stored on the feed post, not on the session. To pick caption changes up, request the full list again.
Every POST that creates records takes a list, even for one record: {"items": [...]} with 1 to 10 items.
The response has one result per item, in the same order. Each result has a status: created, existing, or error. A result with error carries an Error object.
The request returns 200 when the server processed it, even if some items failed. It returns 400 only when the whole request is not valid, for example with 11 items.
An unknown query parameter or body field is ignored. Check the spelling of a filter: a misspelled filter returns unfiltered results.
Fields that only the server sets, such as id or created, are unknown in a request body and are ignored there. You can copy an object from a GET response into a POST without removing them.
Every request counts against two limits: a ceiling of 240/minute across the whole API, and the endpoint's own limit. Each endpoint block states its own limit.
A request over either limit returns 429 with code rate_limited and a Retry-After header. Retry-After gives the seconds to wait.
The public feed endpoints carry no account credential, so they count for each public token instead.
A book is one entry in your library. Each book is on one shelf, given by status. A book can link to a catalog product.
A book carries product when the catalog matched it, and null when it did not. A matched product can still carry cover_url: null or store_url: null. Read a book's own title and author, which are always present.
Deleting a book moves it to the deleted shelf. The book stays in your library with status: "deleted", and PATCH /books puts it back on another shelf.
Books on every shelf are returned, including the deleted shelf. Use status to select one shelf. Use product_ids, isbns, or asins to select the books matched to specific catalog products.
Input
Query parameters ListBooksQuery
Name
Type
Required
Limits and choices
Description
ids
array of integer
No
1 to 100 items
Return only these books
product_ids
array of string
No
1 to 100 items
Return only books matched to these catalog products
isbns
array of string
No
1 to 100 items
Return only books whose catalog product has one of these ISBNs. ISBN-10 or ISBN-13, hyphens allowed.
asins
array of string
No
1 to 100 items
Return only books whose catalog product has one of these ASINs
Adds 1 to 10 books to your library, each on a shelf.
Rate limit:50/minute.
The endpoint matches each item to a catalog product. An id, ISBN, or ASIN of any edition resolves to the ebook edition of that work. An item with no match is still added, with matched_by and productnull: we look the metadata up in the background and fill product in if we find it. An item carrying only an id we cannot resolve is an error with code not_found.
When the matched product is already in your library, the endpoint updates that book and returns existing. It does not add a second copy. The dates, rating, review, and page count you send are applied to the existing book. A matched book on the unsorted or deleted shelf moves to the shelf you send, or to want_to_read when you send none. A matched book on any other shelf keeps its shelf unless you send one.
Total pages of your paper copy. Progress of a paper session is its page divided by this total, so set it before you log paper sessions: sessions logged earlier keep the progress they were logged with.
Catalog product identity ProductIdentity
Send one identity: id, isbn, asin, or title with author. The endpoint tries them in that order.
Name
Type
Required
Limits and choices
Description
id
string
No
Pattern ^\d{1,20}$
Catalog product id of any edition
isbn
string
No
10 to 17 characters
ISBN-10 or ISBN-13 of any edition. Hyphens allowed.
Field of product that found the catalog product. null on an error result, or when no catalog product matched. An unmatched book is still added without a product, and a background lookup adds the product if it finds the book.
book_id
integer
Yes
Id of the created or updated book. null when status is error.
Total pages of your paper copy. Progress of a paper session is its page divided by this total, so set it before you log paper sessions: sessions logged earlier keep the progress they were logged with. Omit it to leave the total unchanged.
The books stay in GET /books with status: "deleted". Their sessions stay.
To bring a book back, set another shelf with PATCH /books.
The ids go in the query string. A book already on the deleted shelf returns deleted again. An id that is not yours results in an error with code not_found.
Sessions from every origin are returned: the Readwise Books app, Kindle, the manual tracker, and this API. format is set on a manual session and on a session recorded through this API. A session from the Readwise Books app or from Kindle carries null.
caption is your earliest comment on the session's feed post. A session without a feed post, and a session you left no comment on, carries null.
Input
Query parameters ListSessionsQuery
Name
Type
Required
Limits and choices
Description
ids
array of integer
No
1 to 100 items
Return only these sessions
book_ids
array of integer
No
1 to 100 items
Return only sessions of these books
started_after
datetime
No
Return only sessions that started at or after this time
started_before
datetime
No
Return only sessions that started before this time. Must be after started_after.
updated_after
datetime
No
Return only records changed or deleted after this time. See Syncing changes.
include_deleted
boolean
No
Default false
Also return deleted records, with is_deleted: true
Only a session that has ended can be recorded. You cannot record a session that is still in progress.
Every item is a session that has ended. The server computes the XP and the reading time. Reading time is ended_at - started_at - paused_seconds. For audiobook, it is seconds_at_end - seconds_at_start instead.
The book has to be in your library already. A book_id that is not in your library results in an error with code not_found.
You cannot record sessions for a sample. The Readwise Books app records them while you read it. A book_id of a sample results in a validation error with code invalid_value.
format decides which pair of positions the item carries. ebook takes percent_at_start and percent_at_end, paper takes page_at_start and page_at_end, audiobook takes seconds_at_start and seconds_at_end. A missing one results in a validation error with code missing. A page_at_end above the book's manual_total_pages, or a seconds_at_end above the audiobook length set in the app, results in a validation error with code invalid_value.
A paper session gets its progress from the book's manual_total_pages. Set it with PATCH /books before you record paper sessions. A paper session recorded without it has no progress, and setting the total later does not change it.
A recorded session earns XP and counts in your stats and on your leaderboards, like a session the Readwise Books app records. It stays off your feed unless you send publish_to_feed: true.
The API does not detect duplicates. Sending the same request twice records two sessions.
A result carries the session id, not the session. Read the sessions back with GET /sessions and ids.
Times and positions cannot change. To correct them, delete the session and record it again.
A caption needs a feed post. A session that is not in your feed results in a validation error with code invalid_value.
A session that has not ended cannot be captioned. Captioning one results in a validation error with code invalid_value; end it in the Readwise Books app first.
An id that is not yours, and an id of a deleted session, results in an error with code not_found, and nothing is changed for it.
A caption change does not change updated: the caption is a comment on the feed post, not a field of the session record.
A result carries the session id, not the session. Read the changed sessions back with GET /sessions and ids.
A deleted session appears in GET /sessions only with include_deleted=true, with is_deleted: true. Its feed post goes with it.
The ids go in the query string. An id that is not yours, and an id you have already deleted, results in an error with code not_found.
A session that has not ended cannot be deleted. Deleting one results in a validation error with code invalid_value; end it in the Readwise Books app first.
Deleting a session removes its XP. A session from this week lowers your weekly XP and your leaderboard standing. An older session lowers your lifetime XP.
Stats are totals computed from your sessions and books.
Stats count the same sessions as the Readwise Books app. An app session counts only if it is long enough and records progress. A manual or API session counts once it ends.
Returns your reading time, XP, and sessions for each day in a date range.
Rate limit:20/minute.
Every day in the range appears, including days without sessions. Each session counts on the local date it was recorded with, in the zone of the device that recorded it.
to must not be before from, and at most 366 days after it.
The response has no next. One request covers the whole range.
Input
Query parameters SessionsByDayQuery
Name
Type
Required
Limits and choices
Description
from
date
Yes
First day
to
date
Yes
Last day, included. Not before from, and at most 366 days after it.
Ordering
date, ascending. This endpoint takes no order parameter.
A book counts as started in the year its started_reading_at falls in, and as finished in the year its finished_reading_at falls in. Both dates use your time zone.
Books on the deleted shelf are left out of both counts.
Input
Query parameters StatsSummaryQuery
Name
Type
Required
Limits and choices
Description
period
enum
Yes
year, all_time
Period to total
year
integer
No
2000 or more
Year to total. Required when period is year, and not after the current year.
Output
Response StatsSummary
Name
Type
Nullable
Limits and choices
Description
period
enum
No
year, all_time
Period you asked for
year
integer
Yes
Year you asked for. null for all_time.
books_started
integer
No
0 or more
Books started in the period
books_finished
integer
No
0 or more
Books finished in the period
session_count
integer
No
0 or more
Sessions in the period
total_time_seconds
integer
No
0 or more
Reading time, in seconds
total_xp
integer
No
0 or more
XP earned
reading_days
integer
No
0 or more
Days with at least one session
target_books_read
integer
Yes
1 to 10000
Your reading goal for the year. null without a goal or for all_time.
The public feed returns the books and totals of the account whose public token you send. A page sends the token in the Books-Public-Token header, so a static books page can read the data without an account credential.
Each account has its own feed URL, with the account id in the path.
The feed carries no account credential, so the rate limit counts for each public token instead of for the account. Both endpoints answer Cache-Control: public, max-age=300, so a change can take five minutes to reach a reader. A revoked token stops working at once, but a response cached before the revocation can still be served until it expires.
Returns the published books of the account whose token you send.
Rate limit:120/minute.
Send a public token in the Books-Public-Token header. No other credential works here, and an Authorization header is ignored. A missing, unknown, or revoked token returns 401 with code unauthenticated.
The path carries your account id. It is the profile_id of your public token, shown on Your public tokens and returned by POST /public-tokens. An id that the token does not publish returns 403 with code forbidden.
Four shelves are published: currently_reading, finished, want_to_read, and did_not_finish. Books on the unsorted and deleted shelves are never returned, and neither are deleted records.
Any website can call this endpoint from a browser, from any origin. Because the token travels in the Books-Public-Token header, browsers send an OPTIONS preflight first.
product is null for a book the catalog did not match, and a matched product can carry cover_url: null or store_url: null. A page that reads product has to handle product: null, cover_url: null, and store_url: null.
year filters on finished_reading_at. A book you are reading now has no finish date, so year and status=currently_reading together return nothing.
Returns the published totals of the account whose token you send.
Rate limit:120/minute.
Send a public token in the Books-Public-Token header. No other credential works here. A missing, unknown, or revoked token returns 401 with code unauthenticated.
Without year, the totals cover all time. With year, a session counts in the year of its local date, and a book counts in the year its finished_reading_at falls in, in the reader's time zone.