Introduction

Readwise Books API#

Experimental

Read your library and your sessions, record sessions from other apps and devices, and publish a books page.

No book text. The API exposes reading metadata. It never exposes the text of a book.

Experimental. The API can change without notice while v1 is experimental.

Working with agents? Try the Readwise Books MCP.

Authentication#

Most endpoints need your account credential in the Authorization header: a personal access token or an OAuth token.

The public feed is different. It takes a public token in the Books-Public-Token header, and nothing else.

TokenHeaderWhere to get it
Personal access tokenAuthorization: Token YOUR_TOKENhttps://readwise.io/access_token
OAuth access tokenAuthorization: Bearer YOUR_TOKENOAuth 2 flow at https://readwise.io/o/
Public tokenBooks-Public-Token: YOUR_PUBLIC_TOKENYour public tokens
curl
curl "https://readwise.io/books/api/v1/books?limit=1" \
  -H "Authorization: Token YOUR_TOKEN"
OAuth scopes and endpoints

An OAuth token needs the scope for the request method. A personal access token allows every request.

ScopeAllows
readGET requests.
writePOST, PATCH, and DELETE requests.
StepURL
Register a clienthttps://readwise.io/o/register/ (dynamic client registration)
Ask for consenthttps://readwise.io/o/authorize/
Get a tokenhttps://readwise.io/o/token/

Public token#

A public token reads only the books and totals you publish. It is safe to put in the source of a public page.

It works on the public feed and nowhere else. Your personal access token and your OAuth token do not work on the public feed either.

Mint one on Your public tokens, or with POST /public-tokens. The value is shown once.

curl
curl "https://readwise.io/books/api/v1/public/{profile_id}/books?limit=1" \
  -H "Books-Public-Token: YOUR_PUBLIC_TOKEN"

Failed authentication#

StatusCodeWhen
401unauthenticatedThe token is missing or not valid. Browser cookies and app sign-ins do not authenticate API requests.
403insufficient_scopeThe OAuth token does not have the scope for the method.
403forbiddenThe account does not have Readwise Books, or the public token does not publish the account in the path.

Use cases#

Recipes for the common cases. Each one lists the requests in the order you send them.

A books page on your own site#

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.
  1. 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.

    To mint one programmatically instead, use POST /public-tokens.

  2. GET/public/{profile_id}/books?status=finished&order=-finished_reading_at&limit=1

    200 OK
    {
      "results": [
        {
          "id": 4815162,
          "title": "Middlemarch",
          "author": "George Eliot",
          "status": "finished",
          "started_reading_at": "2026-08-02T19:40:00Z",
          "finished_reading_at": "2026-09-11T21:05:00Z",
          "rating": 4.5,
          "review": "Slow start, then impossible to put down. Dorothea and Lydgate stay with you.",
          "product": {
            "id": "576523309002996285",
            "title": "Middlemarch",
            "author": "George Eliot",
            "isbn": "9780141439549",
            "cover_url": "https://bookwise.com/product/cover/576523309002996285/middlemarch-george-eliot",
            "store_url": "https://bookwise.com/9780141439549/"
          }
        }
      ],
      "next": "https://readwise.io/books/api/v1/public/{profile_id}/books?status=finished&order=-finished_reading_at&limit=1&cursor=eyJvIjoiLWZpbmlzaGVkX3JlYWRpbmdfYXQiLCJ2IjoiMjAyNi0wOS0xMVQyMTowNTowMCswMDowMCIsImlkIjo0ODE1MTYyfQ"
    }
  3. GET/public/{profile_id}/books?status=currently_reading

    200 OK
    {
      "results": [
        {
          "id": 4790033,
          "title": "The Mirror and the Light",
          "author": "Hilary Mantel",
          "status": "currently_reading",
          "started_reading_at": "2026-09-01T08:00:00Z",
          "finished_reading_at": null,
          "rating": null,
          "review": null,
          "product": null
        }
      ],
      "next": null
    }

    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.

  4. GET/public/{profile_id}/stats?year=2026

    200 OK
    {
      "username": "ada",
      "year": 2026,
      "books_finished": 17,
      "session_count": 312,
      "total_time_seconds": 612840,
      "total_xp": 2694210
    }

Both feed endpoints answer Cache-Control: public, max-age=300, so a change can take five minutes to reach a reader.

Record sessions from another app or device#

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.
  1. POST/books

    Request body
    {
      "items": [
        {
          "product": {
            "isbn": "9780008366780"
          },
          "status": "currently_reading"
        }
      ]
    }
    200 OK
    {
      "results": [
        {
          "status": "existing",
          "matched_by": "isbn",
          "book_id": 4790033,
          "error": null
        }
      ]
    }

    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.

  2. POST/sessions

    Request body
    {
      "items": [
        {
          "book_id": 4790033,
          "started_at": "2026-09-10T20:14:05Z",
          "ended_at": "2026-09-10T20:58:35Z",
          "paused_seconds": 120,
          "format": "ebook",
          "percent_at_start": 0.412,
          "percent_at_end": 0.447,
          "timezone": "America/New_York",
          "device": {
            "model": "Palma",
            "type": "Handset",
            "vendor": "ONYX"
          }
        }
      ]
    }
    200 OK
    {
      "results": [
        {
          "status": "created",
          "session_id": 90211876,
          "error": null
        }
      ]
    }

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.

Bringing a reading history in#

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.
  1. POST/books

    Request body
    {
      "items": [
        {
          "product": {
            "isbn": "9780141439549"
          },
          "status": "finished",
          "started_reading_at": "2026-08-02T19:40:00Z",
          "finished_reading_at": "2026-09-11T21:05:00Z",
          "rating": 4.5
        },
        {
          "product": {
            "isbn": "9780000000002"
          },
          "status": "finished"
        }
      ]
    }
    200 OK
    {
      "results": [
        {
          "status": "created",
          "matched_by": "isbn",
          "book_id": 4815162,
          "error": null
        },
        {
          "status": "created",
          "matched_by": null,
          "book_id": 4815163,
          "error": null
        }
      ]
    }

    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.

  2. POST/sessions optional

    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.

    Request body
    {
      "items": [
        {
          "book_id": 4815162,
          "started_at": "2026-09-11T20:14:05Z",
          "ended_at": "2026-09-11T21:05:00Z",
          "format": "ebook",
          "percent_at_start": 0.88,
          "percent_at_end": 1
        }
      ]
    }
    200 OK
    {
      "results": [
        {
          "status": "created",
          "session_id": 90211930,
          "error": null
        }
      ]
    }

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.

A year in review#

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.
  1. GET/stats/summary?period=year&year=2026

    200 OK
    {
      "period": "year",
      "year": 2026,
      "books_started": 21,
      "books_finished": 17,
      "session_count": 312,
      "total_time_seconds": 612840,
      "total_xp": 2694210,
      "reading_days": 188,
      "target_books_read": 24
    }
  2. GET/stats/sessions-by-day?from=2026-01-01&to=2026-12-31

    200 OK (first two days)
    {
      "results": [
        {
          "date": "2026-01-01",
          "session_count": 1,
          "total_time_seconds": 2550,
          "total_xp": 11060,
          "book_ids": [
            4790033
          ]
        },
        {
          "date": "2026-01-02",
          "session_count": 0,
          "total_time_seconds": 0,
          "total_xp": 0,
          "book_ids": []
        }
      ]
    }

    The response carries one item per day in the range, including the days with no session. The range is at most 366 days, so ask for one year at a time.

  3. GET/books?status=finished&order=-finished_reading_at

    200 OK (shortened)
    {
      "results": [
        {
          "id": 4815162,
          "title": "Middlemarch",
          "author": "George Eliot",
          "finished_reading_at": "2026-09-11T21:05:00Z",
          "rating": 4.5,
          "...": "..."
        }
      ],
      "next": null
    }

    GET /books has no year filter, so keep the books whose finished_reading_at falls in the year and stop paging at the first book finished before it.

Adding a book to Want to Read#

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.
  1. POST/books

    Request body
    {
      "items": [
        {
          "product": {
            "isbn": "9780141439624"
          },
          "status": "want_to_read"
        }
      ]
    }
    200 OK
    {
      "results": [
        {
          "status": "created",
          "matched_by": "isbn",
          "book_id": 4815201,
          "error": null
        }
      ]
    }

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.

Conventions#

These rules apply to every endpoint unless the endpoint says otherwise.

Base URL: https://readwise.io/books/api/v1/. Requests and responses use JSON.

Query parameters and JSON fields use underscores, for example updated_after. URL paths use dashes, for example /stats/sessions-by-day.

Types#

Field tables use these types only. Every response field is always present. A field without a value is null.

TypeMeaningExample
stringText."Middlemarch"
integerWhole number.4815162
numberNumber that can have a fraction.0.447
booleantrue or false. In a query string, also 1 and 0.true
datetimeISO 8601 time. Responses use UTC with Z. Requests can use Z or an offset."2026-09-10T20:14:05Z"
dateISO 8601 date."2026-09-10"
enumString from a fixed list. The field table lists the values."finished"
array of <type>List of values of that type. In a query string, one comma-separated value.[4815162, 4790033]
objectNested object. Its own table lists its fields.{"name": "Boox Palma"}

Selecting records by id#

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.

Pagination#

List endpoints return one page at a time. limit sets the page size: 1 to 1000, default 40.

Each response has results and next. next is the URL of the next page, or null on the last page. The response has no total count.

The cursor in next is opaque. Pass it back unchanged. Do not build cursors yourself.

next carries no credential. Send the same Authorization or Books-Public-Token header you sent for the first page.

List response (shortened)
{
  "results": [
    {
      "id": 4815162,
      "title": "Middlemarch",
      "...": "..."
    }
  ],
  "next": "https://readwise.io/books/api/v1/books?limit=40&cursor=Y3I6MjAyNS0xMS0xOVQwODowMjo0MVo6NDgxNTE2Mg"
}

Ordering#

order=<field> sorts ascending. order=-<field> sorts descending. A request takes one field.

Each list endpoint has a default order and a table of the values that order accepts. Some fields allow one direction only.

The server adds id, in the same direction, as the last sort key. Records with equal values then keep a fixed order across pages.

Records without a value sort last, in both directions.

An unsupported value returns 400. The error message lists the supported values.

400 Bad Request
{
  "error": {
    "code": "invalid_value",
    "message": "Invalid value for order: Value error, Unsupported order. Supported: created, -created, updated, -finished_reading_at.",
    "details": [
      {
        "location": "query",
        "field": "order",
        "code": "invalid_value",
        "message": "Value error, Unsupported order. Supported: created, -created, updated, -finished_reading_at."
      }
    ]
  }
}

Syncing changes#

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.

  1. Request the full list with order=updated. For sessions, add include_deleted=true. Follow next to the last page.
  2. Store the latest change time you received. For a deleted session, the change time is deleted_at. For every other record, it is updated.
  3. 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.

Batch creates#

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.

Request body (shortened)
{
  "items": [
    {
      "product": {"isbn": "9780141439624"},
      "...": "..."
    },
    {
      "product": {"isbn": "9780141439549"},
      "...": "..."
    },
    {
      "product": {"id": "576523309002996399"},
      "...": "..."
    }
  ]
}
200 OK (shortened)
{
  "results": [
    {
      "status": "created",
      "matched_by": "isbn",
      "book_id": 4815201,
      "error": null
    },
    {
      "status": "existing",
      "matched_by": "isbn",
      "book_id": 4815162,
      "error": null
    },
    {
      "status": "error",
      "matched_by": null,
      "book_id": null,
      "error": {
        "code": "not_found",
        "message": "No catalog product matches this book.",
        "details": [
          {
            "location": "body",
            "field": "items.2.product.id",
            "code": "not_found",
            "message": "No catalog product matches this book."
          }
        ]
      }
    }
  ]
}

Updates and deletes#

PATCH takes {"items": [...]}. Each item has an id and the fields to change. The response has one result per item, as for batch creates.

DELETE takes the ids in the query string, ?ids=, never in a body.

Unknown parameters and fields#

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.

Errors#

A failed request returns an error object. A failed item in a batch carries the same object in its result.

Error messages never repeat the value you sent.

Error
Name Type Nullable Limits and choices Description
code enum No
invalid_request, invalid_value, not_found, unauthenticated, forbidden, insufficient_scope, payload_too_large, rate_limited, server_error
Error type
message string No Explanation for a person. It never repeats the value you sent.
details array of ErrorDetail No One item per problem field
ErrorDetail
Name Type Nullable Limits and choices Description
location enum No
path, query, body
Where the field is
field string No Dotted path of the field, such as items.2.product.isbn
code enum No
missing, invalid_value, not_found, too_many_items
Problem type
message string No Explanation for a person

Status codes

StatusMeaning
200Processed. A batch can still contain item errors.
400The request is not valid. See error.details.
401The token is missing or not valid.
403The token does not have the scope, or the account does not have Readwise Books.
404The path does not exist.
413The body is larger than 1 MB.
429Too many requests. See Rate limits.
500Server error.

Rate limits#

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.

Books#

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.

List books#

GET

Returns the books in your library.

Rate limit: 20/minute.

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
status enum No
unsorted, want_to_read, currently_reading, finished, did_not_finish, deleted
Return only books on this shelf
updated_after datetime No Return only books changed after this time. See Syncing changes.
order enum No
created, -created, updated, -finished_reading_at
Default -created
Sort order
limit integer No
1 to 1000
Default 40
Maximum records on one page
cursor string No
Up to 512 characters
Page to return. Copy it from next.

Ordering

Default: order=-created. The server adds id as the last sort key.

FieldAllowed valuesUse
createdorder=created order=-createdNewest or oldest books. Never changes and is never null.
updatedorder=updatedSync with updated_after.
finished_reading_atorder=-finished_reading_atMost recently finished books. Nullable.

Output

Response BookListResponse
Name Type Nullable Limits and choices Description
results array of Book No The books on this page
next string Yes URL of the next page. null on the last page.
Book

A book is one entry in your library.

Name Type Nullable Limits and choices Description
id integer No Book id
title string Yes Title. From the catalog product when there is one, otherwise from the source the book came from.
author string Yes Author names, as one string
status enum No
unsorted, want_to_read, currently_reading, finished, did_not_finish, deleted
Shelf
creation_origin enum No
bookstore_ebook, uploaded_ebook, physical, kindle, audible, goodreads, bookwise, glose_migration, api
How the book entered your library
progress number Yes
0 to 1
Latest reading position, as a fraction of the book
started_reading_at datetime Yes Time you started the book
finished_reading_at datetime Yes Time you finished the book
status_changed_at datetime Yes Time of the last shelf change
rating number Yes Your rating, 0.25 to 5 in steps of 0.25
review string Yes Your review
manual_total_pages integer Yes Page count you set for a paper book
manual_total_seconds integer Yes Length you set for an audiobook, in seconds
product objectProduct Yes Matched catalog product. null without a match.
created datetime No Time the book was added to your library
updated datetime No Time of the last change
Product

A catalog product is one edition in the Readwise Books catalog.

Name Type Nullable Limits and choices Description
id string No Catalog product id. A string, because the value is too large for a JavaScript number.
title string Yes
Up to 511 characters
Title
author string Yes
Up to 511 characters
Author names, as one string
format string Yes Edition format, as stored in the catalog
isbn string Yes ISBN-13
asin string Yes
Up to 10 characters
Amazon ASIN
language_code string Yes
Up to 16 characters
Language
publishing_date date Yes Publication date of this edition
page_count integer Yes Pages in this edition
word_count integer Yes Words in this edition. A count only.
summary string Yes
Up to 4096 characters
Publisher description. Not text from the book.
cover_url string Yes Cover image
store_url string Yes Page of this edition in the Bookwise store

Examples

200 OK
{
  "results": [
    {
      "id": 4815162,
      "title": "Middlemarch",
      "author": "George Eliot",
      "status": "currently_reading",
      "creation_origin": "goodreads",
      "progress": 0.447,
      "started_reading_at": "2026-08-02T19:40:00Z",
      "finished_reading_at": null,
      "status_changed_at": "2026-08-02T19:40:00Z",
      "rating": null,
      "review": null,
      "manual_total_pages": null,
      "manual_total_seconds": null,
      "product": {
        "id": "576523309002996285",
        "title": "Middlemarch",
        "author": "George Eliot",
        "format": "paperback",
        "isbn": "9780141439549",
        "asin": null,
        "language_code": "en",
        "publishing_date": "2003-04-29",
        "page_count": 880,
        "word_count": 316059,
        "summary": "A novel of provincial life in an English Midlands town in the years before the 1832 Reform Act.",
        "cover_url": "https://bookwise.com/product/cover/576523309002996285/middlemarch-george-eliot",
        "store_url": "https://bookwise.com/9780141439549/"
      },
      "created": "2025-11-19T08:02:41Z",
      "updated": "2026-09-10T20:58:36Z"
    },
    {
      "id": 4790033,
      "title": "A notebook of my own",
      "author": null,
      "status": "currently_reading",
      "creation_origin": "api",
      "progress": null,
      "started_reading_at": "2026-09-01T08:00:00Z",
      "finished_reading_at": null,
      "status_changed_at": "2026-09-01T08:00:00Z",
      "rating": null,
      "review": null,
      "manual_total_pages": 240,
      "manual_total_seconds": null,
      "product": null,
      "created": "2026-09-01T08:00:00Z",
      "updated": "2026-09-01T08:00:00Z"
    }
  ],
  "next": "https://readwise.io/books/api/v1/books?status=currently_reading&limit=1&cursor=Y3I6MjAyNS0xMS0xOVQwODowMjo0MVo6NDgxNTE2Mg"
}

Add books#

POST

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 product null: 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.

Input

Body fields CreateBooksRequest
Name Type Required Limits and choices Description
items array of CreateBookItem Yes
1 to 10 items
Books to add
Book item CreateBookItem
Name Type Required Limits and choices Description
product objectProductIdentity Yes The catalog product. The endpoint tries id, isbn, asin, then title with author.
status enum No
want_to_read, currently_reading, finished, did_not_finish, deleted
Default want_to_read
Shelf
started_reading_at datetime No Time you started the book
finished_reading_at datetime No Time you finished the book
rating number No
0.25 to 5, in steps of 0.25
Your rating
review string No
1 to 10000 characters
Your review
manual_total_pages integer No
1 to 100000
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.
asin string No
10 characters
Amazon ASIN
title string No
1 to 511 characters
Title. Send with author.
author string No
1 to 511 characters
Author names. Send with title.

Output

Response CreateBooksResponse
Name Type Nullable Limits and choices Description
results array of CreateBookResult No One result per item
Book result CreateBookResult
Name Type Nullable Limits and choices Description
status enum No
created, existing, error
Outcome for this item
matched_by enum Yes
id, isbn, asin, title_author
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.
error objectError Yes null unless status is error

Examples

Request body
{
  "items": [
    {
      "product": {
        "isbn": "9780141439624"
      },
      "status": "want_to_read"
    }
  ]
}

Update books#

PATCH

Changes the shelf, dates, rating, review, or page count of 1 to 100 books.

Rate limit: 50/minute.

Fields you leave out stay unchanged. Send null to clear a date, a rating, or a review.

An id that is not yours results in an error with code not_found, and nothing is changed for it.

status takes any shelf except unsorted, including deleted. DELETE /books is the shortcut for the deleted shelf.

Finish any open reading session for the book in the app before you change manual_total_pages. Otherwise the item fails with invalid_value.

A result carries the book id, not the book. Read the changed books back with GET /books and ids.

Input

Body fields UpdateBooksRequest
Name Type Required Limits and choices Description
items array of UpdateBookItem Yes
1 to 100 items
Changes, one item per book
Book change UpdateBookItem
Name Type Required Limits and choices Description
id integer Yes Book to change
status enum No
want_to_read, currently_reading, finished, did_not_finish, deleted
Shelf. Omit it to leave the shelf unchanged.
started_reading_at datetime No Time you started the book. null clears it.
finished_reading_at datetime No Time you finished the book. null clears it.
rating number No
0.25 to 5, in steps of 0.25
Your rating. null clears it.
review string No
1 to 10000 characters
Your review. null clears it.
manual_total_pages integer No
1 to 100000
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.

Output

Response UpdateBooksResponse
Name Type Nullable Limits and choices Description
results array of UpdateBookResult No One result per item
Book change result UpdateBookResult
Name Type Nullable Limits and choices Description
id integer Yes Book id you sent. null when the item carried no usable id.
status enum No
updated, error
Outcome for this item
error objectError Yes null unless status is error

Examples

Request body
{
  "items": [
    {
      "id": 4815162,
      "status": "finished",
      "finished_reading_at": "2026-09-11T21:05:00Z",
      "rating": 4.5,
      "review": "Slow start, then impossible to put down. Dorothea and Lydgate stay with you."
    }
  ]
}

Move books to the deleted shelf#

DELETE

Moves 1 to 100 books to the deleted shelf.

Rate limit: 50/minute.

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.

Input

Query parameters DeleteBooksQuery
Name Type Required Limits and choices Description
ids array of integer Yes
1 to 100 items
Books to move

The endpoint takes no body.

Output

Response DeleteBooksResponse
Name Type Nullable Limits and choices Description
results array of DeleteBookResult No One result per id
Book deletion result DeleteBookResult
Name Type Nullable Limits and choices Description
id integer No Book id you sent
status enum No
deleted, error
Outcome for this id
error objectError Yes null unless status is error

Examples

200 OK
{
  "results": [
    {
      "id": 4790033,
      "status": "deleted",
      "error": null
    }
  ]
}

Sessions#

A session is one period of reading in one book. Sessions earn XP and can appear in your feed.

List sessions#

GET

Returns your sessions.

Rate limit: 20/minute.

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
order enum No
started_at, -started_at, updated, -total_time_seconds
Default -started_at
Sort order
limit integer No
1 to 1000
Default 40
Maximum records on one page
cursor string No
Up to 512 characters
Page to return. Copy it from next.

Ordering

Default: order=-started_at. The server adds id as the last sort key.

FieldAllowed valuesUse
started_atorder=started_at order=-started_atTimelines and calendars. Never null.
updatedorder=updatedSync with updated_after.
total_time_secondsorder=-total_time_secondsLongest sessions. Nullable.

Output

Response SessionListResponse
Name Type Nullable Limits and choices Description
results array of Session No The sessions on this page
next string Yes URL of the next page. null on the last page.
Session

A session is one period of reading in one book.

Name Type Nullable Limits and choices Description
id integer No Session id
book_id integer No Book that you read
started_at datetime No Start time
ended_at datetime Yes End time. null while a session in the Bookwise app is running.
paused_seconds integer No
0 or more
Time paused, in seconds
total_time_seconds integer Yes
0 or more
Reading time without pauses, in seconds
total_xp integer Yes
0 or more
XP the session earned. The server computes it.
total_highlights integer Yes
0 or more
Number of highlights made in the session
format enum Yes
paper, ebook, audiobook
Format read. Decides which position fields are set.
percent_at_start number Yes
0 to 1
Start position as a fraction of the book. For ebook.
percent_at_end number Yes
0 to 1
End position as a fraction of the book. For ebook.
page_at_start integer Yes
0 or more
Start page. For paper.
page_at_end integer Yes
0 or more
End page. For paper.
seconds_at_start integer Yes
0 or more
Start position in seconds. For audiobook.
seconds_at_end integer Yes
0 or more
End position in seconds. For audiobook.
timezone string Yes Time zone of the session
local_date date Yes Day of the session in its time zone
origin enum No
bookwise, kindle, audible, manual, api
Where the session was recorded
device objectDevice Yes Device that recorded the session
caption string Yes Your caption on the feed post
published_at datetime Yes Time the session was posted to your feed. null when it is not in the feed.
created datetime No Time the session was stored
updated datetime No Time of the last change
deleted_at datetime Yes Time of deletion. Set only on deleted records.
is_deleted boolean No true for a deleted record
Device

The device as the recording client stored it. type is not limited to the values a client can send through this API.

Name Type Nullable Limits and choices Description
model string Yes Device model, for example Palma
type string Yes Device type, as the recording client reported it. The Bookwise app reports Handset or Tablet.
vendor string Yes Device maker, for example ONYX

Examples

200 OK
{
  "results": [
    {
      "id": 90211876,
      "book_id": 4815162,
      "started_at": "2026-09-10T20:14:05Z",
      "ended_at": "2026-09-10T20:58:35Z",
      "paused_seconds": 120,
      "total_time_seconds": 2550,
      "total_xp": 11060,
      "total_highlights": null,
      "format": "ebook",
      "percent_at_start": 0.412,
      "percent_at_end": 0.447,
      "page_at_start": null,
      "page_at_end": null,
      "seconds_at_start": null,
      "seconds_at_end": null,
      "timezone": "America/New_York",
      "local_date": "2026-09-10",
      "origin": "api",
      "device": {
        "model": "Palma",
        "type": "Handset",
        "vendor": "ONYX"
      },
      "caption": null,
      "published_at": "2026-09-10T20:58:36Z",
      "created": "2026-09-10T20:58:36Z",
      "updated": "2026-09-10T20:58:36Z",
      "deleted_at": null,
      "is_deleted": false
    }
  ],
  "next": "https://readwise.io/books/api/v1/sessions?book_ids=4815162&started_after=2026-09-10T00:00:00Z&limit=1&cursor=eyJvIjoiLXN0YXJ0ZWRfYXQiLCJ2IjoiMjAyNi0wOS0xMFQyMDoxNDowNSswMDowMCIsImlkIjo5MDIxMTg3Nn0"
}

Record sessions#

POST

Records 1 to 10 finished sessions.

Rate limit: 50/minute.

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.

Input

Body fields CreateSessionsRequest
Name Type Required Limits and choices Description
items array of CreateSessionItem Yes
1 to 10 items
Sessions to record
Session item CreateSessionItem

Send the position pair that matches format: percent for ebook, pages for paper, seconds for audiobook.

Name Type Required Limits and choices Description
book_id integer Yes Book in your library
started_at datetime Yes Start time
ended_at datetime Yes End time
paused_seconds integer No
0 or more
Default 0
Time paused, in seconds
format enum Yes
paper, ebook, audiobook
Format read. Decides which pair of position fields is required.
percent_at_start number No
0 to 1
Start position as a fraction of the book. Required for ebook.
percent_at_end number No
0 to 1
End position as a fraction of the book. Required for ebook.
page_at_start integer No
0 to 100000
Start page. Required for paper.
page_at_end integer No
0 to 100000
End page. Required for paper.
seconds_at_start integer No
0 to 360000
Start position in seconds. Required for audiobook.
seconds_at_end integer No
0 to 360000
End position in seconds. Required for audiobook.
timezone string No
Up to 63 characters
IANA time zone of the session. Default: your profile time zone.
device objectDeviceInfo No Device that recorded the session
publish_to_feed boolean No
Default false
Post the session to your feed
Device DeviceInfo
Name Type Required Limits and choices Description
model string No
1 to 100 characters
Device model, for example Palma
type enum No
Handset, Tablet, E-Reader
Device type
vendor string No
1 to 100 characters
Device maker, for example ONYX

Output

Response CreateSessionsResponse
Name Type Nullable Limits and choices Description
results array of CreateSessionResult No One result per item
Session result CreateSessionResult
Name Type Nullable Limits and choices Description
status enum No
created, existing, error
Outcome for this item
session_id integer Yes Id of the created session. null when status is error.
error objectError Yes null unless status is error

Examples

Request body
{
  "items": [
    {
      "book_id": 4815201,
      "started_at": "2026-09-10T20:14:05Z",
      "ended_at": "2026-09-10T20:58:35Z",
      "paused_seconds": 120,
      "format": "ebook",
      "percent_at_start": 0.412,
      "percent_at_end": 0.447,
      "timezone": "America/New_York",
      "device": {
        "model": "Palma",
        "type": "Handset",
        "vendor": "ONYX"
      }
    }
  ]
}

Update sessions#

PATCH

Changes the caption of 1 to 100 sessions.

Rate limit: 50/minute.

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.

Input

Body fields UpdateSessionsRequest
Name Type Required Limits and choices Description
items array of UpdateSessionItem Yes
1 to 100 items
Changes, one item per session
Session change UpdateSessionItem
Name Type Required Limits and choices Description
id integer Yes Session to change
caption string Yes
Up to 2000 characters
Caption on the feed post. null or "" removes it.

Output

Response UpdateSessionsResponse
Name Type Nullable Limits and choices Description
results array of UpdateSessionResult No One result per item
Session change result UpdateSessionResult
Name Type Nullable Limits and choices Description
id integer Yes Session id you sent. null when the item carried no usable id.
status enum No
updated, error
Outcome for this item
error objectError Yes null unless status is error

Examples

Request body
{
  "items": [
    {
      "id": 90211876,
      "caption": "Book Five. Lydgate and the debts close in."
    }
  ]
}

Delete sessions#

DELETE

Deletes 1 to 100 sessions.

Rate limit: 50/minute.

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.

Input

Query parameters DeleteSessionsQuery
Name Type Required Limits and choices Description
ids array of integer Yes
1 to 100 items
Sessions to delete

The endpoint takes no body.

Output

Response DeleteSessionsResponse
Name Type Nullable Limits and choices Description
results array of DeleteSessionResult No One result per id
Session deletion result DeleteSessionResult
Name Type Nullable Limits and choices Description
id integer No Session id you sent
status enum No
deleted, error
Outcome for this id
error objectError Yes null unless status is error

Examples

200 OK
{
  "results": [
    {
      "id": 90211876,
      "status": "deleted",
      "error": null
    }
  ]
}

Stats#

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.

Sessions by day#

GET

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.

Output

Response SessionsByDayResponse
Name Type Nullable Limits and choices Description
results array of Day No
1 to 367 items
One item per day
Day

A day is one calendar day of your reading.

Name Type Nullable Limits and choices Description
date date No Day
session_count integer No
0 or more
Sessions on this day
total_time_seconds integer No
0 or more
Reading time, in seconds
total_xp integer No
0 or more
XP earned
book_ids array of integer No Books with a session on this day

Examples

200 OK
{
  "results": [
    {
      "date": "2026-09-08",
      "session_count": 2,
      "total_time_seconds": 3310,
      "total_xp": 14120,
      "book_ids": [
        4815162
      ]
    },
    {
      "date": "2026-09-09",
      "session_count": 0,
      "total_time_seconds": 0,
      "total_xp": 0,
      "book_ids": []
    },
    {
      "date": "2026-09-10",
      "session_count": 1,
      "total_time_seconds": 2550,
      "total_xp": 11060,
      "book_ids": [
        4815162
      ]
    }
  ]
}

Summary#

GET

Returns your totals for one year or for all time.

Rate limit: 20/minute.

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.

Examples

200 OK
{
  "period": "year",
  "year": 2026,
  "books_started": 21,
  "books_finished": 17,
  "session_count": 312,
  "total_time_seconds": 612840,
  "total_xp": 2694210,
  "reading_days": 188,
  "target_books_read": 24
}

Public tokens#

A public token lets one site read the books and totals you publish. Minting a token publishes your data. Revoking every token unpublishes it.

A public token is safe to put in the source of a public page. It reads published data only, and every other endpoint rejects it.

Your public tokens#

Sign in with your Readwise account to mint a token here. You can also use POST /public-tokens.

Mint a public token#

POST

Mints one public token. The token publishes your books and totals to one site.

Rate limit: 10/minute.

The response carries the token value once. It is never returned again. Store it before you close the response.

Minting publishes your data. There is no separate setting to turn on.

Mint one token for each site. Revoking one then leaves the others working.

A public token reads published data only. Every other endpoint rejects it.

Input

Body fields CreatePublicTokenRequest
Name Type Required Limits and choices Description
name string No
1 to 100 characters
Label for the site the token is for

Output

Response CreatePublicTokenResponse

The PublicToken fields, plus the token value.

Name Type Nullable Limits and choices Description
id integer No Token id. Revoke the token with this id.
name string Yes
Up to 100 characters
Label you gave the token
token_prefix string No Start of the token, so you can tell your tokens apart
profile_id integer No Your account id. It goes in the public feed URL.
created datetime No Time you minted the token
token string No The token value. Shown once.

Examples

Request body
{
  "name": "My books page"
}

List public tokens#

GET

Returns your public tokens, without their values.

Rate limit: 20/minute.

A token value is shown once, when you mint it. token_prefix tells your tokens apart.

Input

Query parameters ListPublicTokensQuery
Name Type Required Limits and choices Description
ids array of integer No
1 to 100 items
Return only these public tokens
order enum No
created, -created
Default -created
Sort order
limit integer No
1 to 1000
Default 40
Maximum records on one page
cursor string No
Up to 512 characters
Page to return. Copy it from next.

Ordering

Default: order=-created. The server adds id as the last sort key.

FieldAllowed valuesUse
createdorder=created order=-createdNewest or oldest token first. Never null.

Output

Response PublicTokenListResponse
Name Type Nullable Limits and choices Description
results array of PublicToken No The public tokens on this page
next string Yes URL of the next page. null on the last page.
PublicToken

A public token publishes your books and totals to one site.

Name Type Nullable Limits and choices Description
id integer No Token id. Revoke the token with this id.
name string Yes
Up to 100 characters
Label you gave the token
token_prefix string No Start of the token, so you can tell your tokens apart
profile_id integer No Your account id. It goes in the public feed URL.
created datetime No Time you minted the token

Examples

200 OK
{
  "results": [
    {
      "id": 3312,
      "name": "newsletter footer",
      "token_prefix": "bwpub_Lm8dVt",
      "profile_id": 4815,
      "created": "2026-09-13T18:40:05Z"
    },
    {
      "id": 3311,
      "name": "My books page",
      "token_prefix": "bwpub_7Qz3kR",
      "profile_id": 4815,
      "created": "2026-09-12T09:14:22Z"
    }
  ],
  "next": null
}

Revoke a public token#

DELETE

Revokes one public token.

Rate limit: 20/minute.

A revoked token stops working. A page that still sends it gets 401.

Revoking every token unpublishes your data.

A cached response can still reach readers until the cache expires.

The response is the token you revoked, so you can confirm which one it was.

Input

Path parameterTypeDescription
idintegerToken id. Read it from GET /public-tokens.

The endpoint takes no body.

Output

The response is one PublicToken object.

Examples

200 OK
{
  "id": 3312,
  "name": "newsletter footer",
  "token_prefix": "bwpub_Lm8dVt",
  "profile_id": 4815,
  "created": "2026-09-13T18:40:05Z"
}

Public feed#

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.

Public books#

GET

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.

Input

HeaderTypeDescription
Books-Public-TokenstringA token from POST /public-tokens. It names whose published data to return.
Query parameters PublicBooksQuery
Name Type Required Limits and choices Description
status enum No
currently_reading, finished, want_to_read, did_not_finish
Return only books on this shelf
year integer No
2000 or more
Return only books finished in this year, in the reader's time zone. Not after the current year.
order enum No
-finished_reading_at, -started_reading_at
Default -finished_reading_at
Sort order
limit integer No
1 to 1000
Default 40
Maximum records on one page
cursor string No
Up to 512 characters
Page to return. Copy it from next.

Ordering

Default: order=-finished_reading_at. The server adds id as the last sort key.

FieldAllowed valuesUse
finished_reading_atorder=-finished_reading_atBooks page, latest finish first. Nullable.
started_reading_atorder=-started_reading_atCurrent reading, latest start first. Nullable.

Output

Response PublicBookListResponse
Name Type Nullable Limits and choices Description
results array of PublicBook No The books on this page
next string Yes URL of the next page. null on the last page.
PublicBook

A published book is one entry in the reader's library.

Name Type Nullable Limits and choices Description
id integer No Book id
title string Yes Title
author string Yes Author names, as one string
status enum No
currently_reading, finished, want_to_read, did_not_finish
Shelf
started_reading_at datetime Yes Time the reader started the book
finished_reading_at datetime Yes Time the reader finished the book
rating number Yes The reader's rating, 0.25 to 5 in steps of 0.25
review string Yes The reader's review
product objectPublicProduct Yes Matched catalog product. null without a match.
PublicProduct

A catalog product is one edition in the Readwise Books catalog.

Name Type Nullable Limits and choices Description
id string No Catalog product id. A string, because the value is too large for a JavaScript number.
title string Yes
Up to 511 characters
Title
author string Yes
Up to 511 characters
Author names, as one string
isbn string Yes ISBN-13
cover_url string Yes Cover image
store_url string Yes Page of this edition in the Bookwise store

Examples

200 OK
{
  "results": [
    {
      "id": 4815162,
      "title": "Middlemarch",
      "author": "George Eliot",
      "status": "finished",
      "started_reading_at": "2026-08-02T19:40:00Z",
      "finished_reading_at": "2026-09-11T21:05:00Z",
      "rating": 4.5,
      "review": "Slow start, then impossible to put down. Dorothea and Lydgate stay with you.",
      "product": {
        "id": "576523309002996285",
        "title": "Middlemarch",
        "author": "George Eliot",
        "isbn": "9780141439549",
        "cover_url": "https://bookwise.com/product/cover/576523309002996285/middlemarch-george-eliot",
        "store_url": "https://bookwise.com/9780141439549/"
      }
    },
    {
      "id": 4790033,
      "title": "A notebook of my own",
      "author": null,
      "status": "finished",
      "started_reading_at": "2026-07-04T10:00:00Z",
      "finished_reading_at": "2026-07-30T18:22:00Z",
      "rating": null,
      "review": null,
      "product": null
    }
  ],
  "next": "https://readwise.io/books/api/v1/public/{profile_id}/books?status=finished&year=2026&limit=1&cursor=eyJvIjoiLWZpbmlzaGVkX3JlYWRpbmdfYXQiLCJ2IjoiMjAyNi0wOS0xMVQyMTowNTowMCswMDowMCIsImlkIjo0ODE1MTYyfQ"
}

Public stats#

GET

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.

Input

HeaderTypeDescription
Books-Public-TokenstringA token from POST /public-tokens. It names whose published data to return.
Query parameters PublicStatsQuery
Name Type Required Limits and choices Description
year integer No
2000 or more
Year to total. Not after the current year. Omit it for all time.

Output

Response PublicStats
Name Type Nullable Limits and choices Description
username string Yes Bookwise username. null when the account has not chosen one.
year integer Yes Year you asked for. null for all time.
books_finished integer No
0 or more
Books finished
session_count integer No
0 or more
Sessions
total_time_seconds integer No
0 or more
Reading time, in seconds
total_xp integer No
0 or more
XP earned

Examples

200 OK
{
  "username": "ada",
  "year": 2026,
  "books_finished": 17,
  "session_count": 312,
  "total_time_seconds": 612840,
  "total_xp": 2694210
}