Skip to content

Readwise API

Source code in readwise/api.py
class Readwise:
    def __init__(
        self,
        token: str,
    ):
        '''
        Initialize a Readwise API client.

        Documentation for the Readwise API can be found here:
        https://readwise.io/api_deets

        Args:
            token: Readwise API token
        '''
        self._token = token
        self._url = 'https://readwise.io/api/v2'

    @property
    def _session(self) -> requests.Session:
        '''
        Return a requests.Session object with the Readwise API token set as an
        Authorization header.
        '''
        session = requests.Session()
        session.headers.update(
            {
                'Accept': 'application/json',
                'Authorization': f'Token {self._token}',
            }
        )
        return session

    def _request(
        self, method: str, endpoint: str, params: dict = {}, data: dict = {}
    ) -> requests.Response:
        '''
        Make a request to the Readwise API.

        The Readwise API has a rate limit of 240 requests per minute. This
        method will raise a ReadwiseRateLimitException if the rate limit is
        exceeded.

        The Exception will be raised after 8 retries with exponential backoff.

        Args:
            method: HTTP method
            endpoint: API endpoint
            params: Query parameters
            data: Request body

        Returns:
            requests.Response
        '''
        url = self._url + endpoint
        logging.debug(f'Calling "{method}" on "{url}" with params: {params}')
        response = self._session.request(method, url, params=params, json=data)
        while response.status_code == 429:
            seconds = int(response.headers['Retry-After'])
            logging.warning(f'Rate limited by Readwise, retrying in {seconds} seconds')
            sleep(seconds)
            response = self._session.request(method, url, params=params, data=data)
        response.raise_for_status()
        return response

    def get(self, endpoint: str, params: dict = {}) -> requests.Response:
        '''
        Make a GET request to the Readwise API.

        Examples:
            >>> client.get('/highlights/')

        Args:
            endpoint: API endpoint
            params: Query parameters

        Returns:
            requests.Response
        '''
        logging.debug(f'Getting "{endpoint}" with params: {params}')
        return self._request('GET', endpoint, params=params)

    def get_with_limit_20(self, endpoint: str, params: dict = {}) -> requests.Response:
        '''
        Get a response from the Readwise API with a rate limit of 20 requests
        per minute.

        The rate limit of 20 requests per minute needs to be used at the
        endpoints /highlights/ and /books/ because they return a lot of data.

        Args:
            endpoint: API endpoint
            params: Query parameters
        Returns:
            requests.Response
        '''
        return self.get(endpoint, params)

    def post(self, endpoint: str, data: dict = {}) -> requests.Response:
        '''
        Make a POST request to the Readwise API.

        Examples:
            >>> client.post('/highlights/', {'highlights': [{'text': 'foo'}]})

        Args:
            endpoint: API endpoint
            data: Request body

        Returns:
            requests.Response
        '''
        url = self._url + endpoint
        logging.debug(f'Posting "{url}" with data: {data}')
        response = self._request('POST', endpoint, data=data)
        response.raise_for_status()
        return response

    def delete(self, endpoint: str) -> requests.Response:
        '''
        Make a DELETE request to the Readwise API.

        Examples:
            >>> client.delete('/highlights/1234')

        Args:
            endpoint: API endpoint

        Returns:
            requests.Response
        '''
        logging.debug(f'Deleting "{endpoint}"')
        return self._request('DELETE', endpoint)

    def get_pagination(
        self, endpoint: str, params: dict = {}
    ) -> Generator[dict, None, None]:
        '''
        Get a response from the Readwise API with pagination.

        Args:
            endpoint: API endpoint
            params: Query parameters
        Yields:
            Response data
        '''
        yield from self._get_pagination('get', endpoint, params)

    def get_pagination_limit_20(
        self, endpoint: str, params: dict = {}, page_size: int = 1000
    ) -> Generator[dict, None, None]:
        '''
        Get a response from the Readwise API with pagination and a rate limit
        of 20 requests per minute.

        Args:
            endpoint: API endpoint
            params: Query parameters
            page_size: Number of items per page
        Yields:
            Response data
        '''
        yield from self._get_pagination(
            'get_with_limit_20', endpoint, params, page_size
        )

    def _get_pagination(
        self,
        get_method: Literal['get', 'get_with_limit_20'],
        endpoint: str,
        params: dict = {},
        page_size: int = 1000,
    ) -> Generator[dict, None, None]:
        '''
        Get a response from the Readwise API with pagination.

        Args:
            get_method: Method to use for making requests
            endpoint: API endpoint
            params: Query parameters
            page_size: Number of items per page
        Yields:
            dict: Response data
        '''
        page = 1
        while True:
            response = getattr(self, get_method)(
                endpoint, params={'page': page, 'page_size': page_size, **params}
            )
            data = response.json()
            yield data
            if type(data) == list or not data.get('next'):
                break
            page += 1

    def get_books(
        self, category: Literal['articles', 'books', 'tweets', 'podcasts']
    ) -> Generator[ReadwiseBook, None, None]:
        '''
        Get all Readwise books.

        Args:
            category: Book category

        Returns:
            A generator of ReadwiseBook objects
        '''
        for data in self.get_pagination_limit_20(
            '/books/', params={'category': category}
        ):
            for book in data['results']:
                yield ReadwiseBook(
                    id=book['id'],
                    title=book['title'],
                    author=book['author'],
                    category=book['category'],
                    source=book['source'],
                    num_highlights=book['num_highlights'],
                    last_highlight_at=datetime.fromisoformat(book['last_highlight_at'])
                    if book['last_highlight_at']
                    else None,
                    updated=datetime.fromisoformat(book['updated'])
                    if book['updated']
                    else None,
                    cover_image_url=book['cover_image_url'],
                    highlights_url=book['highlights_url'],
                    source_url=book['source_url'],
                    asin=book['asin'],
                    tags=[
                        ReadwiseTag(id=tag['id'], name=tag['name'])
                        for tag in book['tags']
                    ],
                    document_note=book['document_note'],
                )

    def get_book_highlights(
        self, book_id: str
    ) -> Generator[ReadwiseHighlight, None, None]:
        '''
        Get all highlights for a Readwise book.

        Args:
            book_id: Readwise book ID

        Returns:
            A generator of ReadwiseHighlight objects
        '''
        for data in self.get_pagination_limit_20(
            '/highlights/', params={'book_id': book_id}
        ):
            for highlight in data['results']:
                yield ReadwiseHighlight(
                    id=highlight['id'],
                    text=highlight['text'],
                    note=highlight['note'],
                    location=highlight['location'],
                    location_type=highlight['location_type'],
                    url=highlight['url'],
                    color=highlight['color'],
                    updated=datetime.fromisoformat(highlight['updated'])
                    if highlight['updated']
                    else None,
                    book_id=highlight['book_id'],
                    tags=[
                        ReadwiseTag(id=tag['id'], name=tag['name'])
                        for tag in highlight['tags']
                    ],
                )

    def create_highlight(
        self,
        text: str,
        title: str,
        author: str | None = None,
        highlighted_at: datetime | None = None,
        source_url: str | None = None,
        category: str = 'articles',
        note: str | None = None,
    ):
        '''
        Create a Readwise highlight.

        Args:
            text: Highlight text
            title: Book title
            author: Book author
            highlighted_at: Date and time the highlight was created
            source_url: URL of the book
            category: Book category
            note: Highlight note
        '''
        payload = {'text': text, 'title': title, 'category': category}
        if author:
            payload['author'] = author
        if highlighted_at:
            payload['highlighted_at'] = highlighted_at.isoformat()
        if source_url:
            payload['source_url'] = source_url
        if note:
            payload['note'] = note

        self.post('/highlights/', {'highlights': [payload]})

    def get_book_tags(self, book_id: str) -> Generator[ReadwiseTag, None, None]:
        '''
        Get all tags for a Readwise book.

        Args:
            book_id: Readwise book ID

        Returns:
            A generator of ReadwiseTag objects
        '''
        for data in self.get_pagination_limit_20(
            f'/books/{book_id}/tags/', params={'book_id': book_id}
        ):
            for tag in data:
                yield ReadwiseTag(id=tag['id'], name=tag['name'])

    def add_tag(self, book_id: str, tag: str):
        '''
        Add a tag to a Readwise book.

        Args:
            book_id: Readwise book ID
            tag: Tag name

        Returns:
            requests.Response
        '''
        logging.debug(f'Adding tag "{tag}" to book "{book_id}"')
        payload = {'name': tag}
        self.post(f'/books/{book_id}/tags/', payload)

    def delete_tag(self, book_id: str, tag_id: str):
        '''
        Delete a tag from a Readwise book.

        Args:
            book_id: Readwise book ID

        Returns:
            requests.Response
        '''
        logging.debug(f'Deleting tag "{tag_id}"')
        self.delete(f'/books/{book_id}/tags/{tag_id}')

__init__(token)

Initialize a Readwise API client.

Documentation for the Readwise API can be found here: https://readwise.io/api_deets

Parameters:

Name Type Description Default
token str

Readwise API token

required
Source code in readwise/api.py
def __init__(
    self,
    token: str,
):
    '''
    Initialize a Readwise API client.

    Documentation for the Readwise API can be found here:
    https://readwise.io/api_deets

    Args:
        token: Readwise API token
    '''
    self._token = token
    self._url = 'https://readwise.io/api/v2'

add_tag(book_id, tag)

Add a tag to a Readwise book.

Parameters:

Name Type Description Default
book_id str

Readwise book ID

required
tag str

Tag name

required

Returns:

Type Description

requests.Response

Source code in readwise/api.py
def add_tag(self, book_id: str, tag: str):
    '''
    Add a tag to a Readwise book.

    Args:
        book_id: Readwise book ID
        tag: Tag name

    Returns:
        requests.Response
    '''
    logging.debug(f'Adding tag "{tag}" to book "{book_id}"')
    payload = {'name': tag}
    self.post(f'/books/{book_id}/tags/', payload)

create_highlight(text, title, author=None, highlighted_at=None, source_url=None, category='articles', note=None)

Create a Readwise highlight.

Parameters:

Name Type Description Default
text str

Highlight text

required
title str

Book title

required
author str | None

Book author

None
highlighted_at datetime | None

Date and time the highlight was created

None
source_url str | None

URL of the book

None
category str

Book category

'articles'
note str | None

Highlight note

None
Source code in readwise/api.py
def create_highlight(
    self,
    text: str,
    title: str,
    author: str | None = None,
    highlighted_at: datetime | None = None,
    source_url: str | None = None,
    category: str = 'articles',
    note: str | None = None,
):
    '''
    Create a Readwise highlight.

    Args:
        text: Highlight text
        title: Book title
        author: Book author
        highlighted_at: Date and time the highlight was created
        source_url: URL of the book
        category: Book category
        note: Highlight note
    '''
    payload = {'text': text, 'title': title, 'category': category}
    if author:
        payload['author'] = author
    if highlighted_at:
        payload['highlighted_at'] = highlighted_at.isoformat()
    if source_url:
        payload['source_url'] = source_url
    if note:
        payload['note'] = note

    self.post('/highlights/', {'highlights': [payload]})

delete(endpoint)

Make a DELETE request to the Readwise API.

Examples:

>>> client.delete('/highlights/1234')

Parameters:

Name Type Description Default
endpoint str

API endpoint

required

Returns:

Type Description
Response

requests.Response

Source code in readwise/api.py
def delete(self, endpoint: str) -> requests.Response:
    '''
    Make a DELETE request to the Readwise API.

    Examples:
        >>> client.delete('/highlights/1234')

    Args:
        endpoint: API endpoint

    Returns:
        requests.Response
    '''
    logging.debug(f'Deleting "{endpoint}"')
    return self._request('DELETE', endpoint)

delete_tag(book_id, tag_id)

Delete a tag from a Readwise book.

Parameters:

Name Type Description Default
book_id str

Readwise book ID

required

Returns:

Type Description

requests.Response

Source code in readwise/api.py
def delete_tag(self, book_id: str, tag_id: str):
    '''
    Delete a tag from a Readwise book.

    Args:
        book_id: Readwise book ID

    Returns:
        requests.Response
    '''
    logging.debug(f'Deleting tag "{tag_id}"')
    self.delete(f'/books/{book_id}/tags/{tag_id}')

get(endpoint, params={})

Make a GET request to the Readwise API.

Examples:

>>> client.get('/highlights/')

Parameters:

Name Type Description Default
endpoint str

API endpoint

required
params dict

Query parameters

{}

Returns:

Type Description
Response

requests.Response

Source code in readwise/api.py
def get(self, endpoint: str, params: dict = {}) -> requests.Response:
    '''
    Make a GET request to the Readwise API.

    Examples:
        >>> client.get('/highlights/')

    Args:
        endpoint: API endpoint
        params: Query parameters

    Returns:
        requests.Response
    '''
    logging.debug(f'Getting "{endpoint}" with params: {params}')
    return self._request('GET', endpoint, params=params)

get_book_highlights(book_id)

Get all highlights for a Readwise book.

Parameters:

Name Type Description Default
book_id str

Readwise book ID

required

Returns:

Type Description
Generator[ReadwiseHighlight, None, None]

A generator of ReadwiseHighlight objects

Source code in readwise/api.py
def get_book_highlights(
    self, book_id: str
) -> Generator[ReadwiseHighlight, None, None]:
    '''
    Get all highlights for a Readwise book.

    Args:
        book_id: Readwise book ID

    Returns:
        A generator of ReadwiseHighlight objects
    '''
    for data in self.get_pagination_limit_20(
        '/highlights/', params={'book_id': book_id}
    ):
        for highlight in data['results']:
            yield ReadwiseHighlight(
                id=highlight['id'],
                text=highlight['text'],
                note=highlight['note'],
                location=highlight['location'],
                location_type=highlight['location_type'],
                url=highlight['url'],
                color=highlight['color'],
                updated=datetime.fromisoformat(highlight['updated'])
                if highlight['updated']
                else None,
                book_id=highlight['book_id'],
                tags=[
                    ReadwiseTag(id=tag['id'], name=tag['name'])
                    for tag in highlight['tags']
                ],
            )

get_book_tags(book_id)

Get all tags for a Readwise book.

Parameters:

Name Type Description Default
book_id str

Readwise book ID

required

Returns:

Type Description
Generator[ReadwiseTag, None, None]

A generator of ReadwiseTag objects

Source code in readwise/api.py
def get_book_tags(self, book_id: str) -> Generator[ReadwiseTag, None, None]:
    '''
    Get all tags for a Readwise book.

    Args:
        book_id: Readwise book ID

    Returns:
        A generator of ReadwiseTag objects
    '''
    for data in self.get_pagination_limit_20(
        f'/books/{book_id}/tags/', params={'book_id': book_id}
    ):
        for tag in data:
            yield ReadwiseTag(id=tag['id'], name=tag['name'])

get_books(category)

Get all Readwise books.

Parameters:

Name Type Description Default
category Literal['articles', 'books', 'tweets', 'podcasts']

Book category

required

Returns:

Type Description
Generator[ReadwiseBook, None, None]

A generator of ReadwiseBook objects

Source code in readwise/api.py
def get_books(
    self, category: Literal['articles', 'books', 'tweets', 'podcasts']
) -> Generator[ReadwiseBook, None, None]:
    '''
    Get all Readwise books.

    Args:
        category: Book category

    Returns:
        A generator of ReadwiseBook objects
    '''
    for data in self.get_pagination_limit_20(
        '/books/', params={'category': category}
    ):
        for book in data['results']:
            yield ReadwiseBook(
                id=book['id'],
                title=book['title'],
                author=book['author'],
                category=book['category'],
                source=book['source'],
                num_highlights=book['num_highlights'],
                last_highlight_at=datetime.fromisoformat(book['last_highlight_at'])
                if book['last_highlight_at']
                else None,
                updated=datetime.fromisoformat(book['updated'])
                if book['updated']
                else None,
                cover_image_url=book['cover_image_url'],
                highlights_url=book['highlights_url'],
                source_url=book['source_url'],
                asin=book['asin'],
                tags=[
                    ReadwiseTag(id=tag['id'], name=tag['name'])
                    for tag in book['tags']
                ],
                document_note=book['document_note'],
            )

get_pagination(endpoint, params={})

Get a response from the Readwise API with pagination.

Parameters:

Name Type Description Default
endpoint str

API endpoint

required
params dict

Query parameters

{}

Yields: Response data

Source code in readwise/api.py
def get_pagination(
    self, endpoint: str, params: dict = {}
) -> Generator[dict, None, None]:
    '''
    Get a response from the Readwise API with pagination.

    Args:
        endpoint: API endpoint
        params: Query parameters
    Yields:
        Response data
    '''
    yield from self._get_pagination('get', endpoint, params)

get_pagination_limit_20(endpoint, params={}, page_size=1000)

Get a response from the Readwise API with pagination and a rate limit of 20 requests per minute.

Parameters:

Name Type Description Default
endpoint str

API endpoint

required
params dict

Query parameters

{}
page_size int

Number of items per page

1000

Yields: Response data

Source code in readwise/api.py
def get_pagination_limit_20(
    self, endpoint: str, params: dict = {}, page_size: int = 1000
) -> Generator[dict, None, None]:
    '''
    Get a response from the Readwise API with pagination and a rate limit
    of 20 requests per minute.

    Args:
        endpoint: API endpoint
        params: Query parameters
        page_size: Number of items per page
    Yields:
        Response data
    '''
    yield from self._get_pagination(
        'get_with_limit_20', endpoint, params, page_size
    )

get_with_limit_20(endpoint, params={})

Get a response from the Readwise API with a rate limit of 20 requests per minute.

The rate limit of 20 requests per minute needs to be used at the endpoints /highlights/ and /books/ because they return a lot of data.

Parameters:

Name Type Description Default
endpoint str

API endpoint

required
params dict

Query parameters

{}

Returns: requests.Response

Source code in readwise/api.py
def get_with_limit_20(self, endpoint: str, params: dict = {}) -> requests.Response:
    '''
    Get a response from the Readwise API with a rate limit of 20 requests
    per minute.

    The rate limit of 20 requests per minute needs to be used at the
    endpoints /highlights/ and /books/ because they return a lot of data.

    Args:
        endpoint: API endpoint
        params: Query parameters
    Returns:
        requests.Response
    '''
    return self.get(endpoint, params)

post(endpoint, data={})

Make a POST request to the Readwise API.

Examples:

>>> client.post('/highlights/', {'highlights': [{'text': 'foo'}]})

Parameters:

Name Type Description Default
endpoint str

API endpoint

required
data dict

Request body

{}

Returns:

Type Description
Response

requests.Response

Source code in readwise/api.py
def post(self, endpoint: str, data: dict = {}) -> requests.Response:
    '''
    Make a POST request to the Readwise API.

    Examples:
        >>> client.post('/highlights/', {'highlights': [{'text': 'foo'}]})

    Args:
        endpoint: API endpoint
        data: Request body

    Returns:
        requests.Response
    '''
    url = self._url + endpoint
    logging.debug(f'Posting "{url}" with data: {data}')
    response = self._request('POST', endpoint, data=data)
    response.raise_for_status()
    return response