Medium REST API Example Calls with Python Requests

Jess Schalz
10 min readFeb 11, 2022

A tutorial showing how to get an integration token, set headers, and retrieve and post data from the Medium REST API.

Author’s Note

Up front, I’m the reason this repo was archived. I’d almost finished this post when I emailed the Medium customer support team and asked for clarification on the repo.

An email from Jessica Schalz (me) to the Medium support team. I ask if they can enable issues in the repo, or update it if it isn’t formally maintained.
Include links/info for your support teams and make their lives easier.

And, uh…

An email reply from Lucius on Medium’s User Services team: “Thanks for pointing this out. That repo has been archived and is no longer maintained.”
…shit.

The repo was archived before I finished the last available endpoint in the API, /images. I don’t know if these endpoints are still available, and I don’t know if that last endpoint would even work. (I do remember being able to upload images and search for them based on file name.) Regardless, I didn’t include it in the post here.

I’m still posting this because I feel like it’s a good resource for folks looking to do simple Medium automation, but be warned. Thar be unmaintained dragons.

Medium has a REST API! Documentation exists online on GitHub here.

The documentation hasn’t been updated since September 2020, though, so it may have been abandoned. In the meantime, I was curious about what endpoints still work. Let’s explore!

Prerequisites:

Auth with Integration/Access Token

According to the documentation, all calls to their API resources require OAuth2 authentication. There are two ways to accomplish this: create an integration token for your account, or retrieve an access token for existing integrations. The first option is significantly easier, especially since I don’t have any third parties associated with my account, so I’ll walk through how to do that.

First, navigate to https://www.medium.com/me/settings. You can do this either through entering that directly in your search bar, or when logged in you can click on your icon in the top right corner of the screen. Click the Settings link in the drop down menu.

On the Settings page, navigate to the Integration Tokens section. You can either scroll down, or click the Integration Token subtitle on the left panel.

If you haven’t set up an integration token yet, this section should be empty. Enter a name for your token in the description box, and then click the “Get integration token” button.

This gives you a unique, hex-encoded string as an integration token.

Important note about integration tokens on Medium! They do not expire! Users can revoke them at any time, so if you think there’s been a compromise you should revoke it immediately and create a new one. (Don’t worry, that token has already been revoked by the time this post is published.)

Once you have your token, you can auth to API resources!

Accessible Endpoints

The documentation indicates there’s four major endpoints, but there’s actually five! Unfortunately, there’s no OpenAPI documentation available, but the GitHub goes through all the info we need! Let’s learn!

The base URL for all of these endpoints is https://api.medium.com/v1. I’ll explain what this means for our requests in the next sections. Also note that all endpoints provide JSON responses, which some exceptions I’ll explain below.

Before I show any code, I wanted to include my pre-set variables and headers for these requests.

In the first line I imported the Python requests library and aliased it to “r”, so I didn’t have to write out “requests” over and over.

You can see I’ve removed my access token from the access token string, but to fix this simply replace the “XXXXX” with your token value.

Another header I like to include when API-diving, especially in unmaintained APIs, is the “ATTN-Security” header. This is a custom header where I add a fun little note for the security/infrastructure/SREs that might maintain the server I’m hitting. This way, they know I don’t have malicious intent, AND they can filter my requests so they aren’t getting alerted constantly. (I’ve been on-call before, I know the deal.) In this case, I used “friendly-dev-bug-hunter”.

/me

The first endpoint available is included in the documentation, but isn’t included as an official endpoint: the /me endpoint! The /me endpoint retrieves information about the currently authenticated user, and supports GET requests.

The documentation provides the following definition and endpoint URL:

Using this endpoint means you gather information about yourself, and yourself only, as a User object. This includes your user ID, your username, your human name, your profile URL, and your icon’s image URL.

If we were to write this in Python, using the requests library and my previously defined headers and token, the following function returns JSON-encoded data about me:

When pretty printed with the Python JSON library, the results look like:

/users

You can also gather information with the /users endpoint.

This time however, you need to know the user’s ID. An ID is an alphanumeric string representing the user. In the previous function, the returned data shows my user ID as “1e3adabb57ab4613a002046c3f838f3a51a5771dda670b99adbc2197030fc0f58”.

Unfortunately, despite the name, you cannot get information about users from this endpoint. You can only get information about what publications the user is associated with (as a follower, a publisher, etc.)

A Publication object is defined in the documentation as:

To use this endpoint, provide the user ID, followed by “publications”.

I don’t author any publications, but I do follow the Medium Engineering blog, so that shows up in my results.

The /users endpoint seems…undeveloped. There clearly isn’t much power in this API from a development standpoint. However, this does give you publication IDs, and that’s necessary for the next section.

/publications

To get information about publications on Medium, you can hit the /publications endpoint. Just like the /users section however, I hope you only want to know exactly one thing about publications: their contributors.

“Contributors” means anyone who affects the publication, specifically “writers” and “editors”. The Contributor object is defined as:

The following code demonstrates this endpoint.

I input the publication ID for the Medium Engineering Blog (“2817475205d3”) and received these results.

(The results are truncated.)

Remember: there’s no efficient way to get user information from the /users endpoint, so even if I provided the user IDs from these results, I don’t have an approved way of getting data. There is an UNAPPROVED way of getting data about a user, but I’ll go over that in another post.

/posts

There are two ways you can create a post through the /posts endpoint. A user can create a post on their own account, or under a publication in which they’re an editor or writer.

Creating a post requires a significantly higher amount of effort than all the previous endpoint. The API docs show a simple request as an example:

Every post request requires the following fields:

  • title (string)
    REQUIRED
    Note: the title here isn’t represented in the actual post. This is only used for SEO listing. You should also include the title in the content field.
    Another note: the title cannot be longer than 100 characters or it will be ignored, and a title will be derived from the first 100 characters of the content.
  • contentFormat (string) (either “HTML” or “markdown”)
    REQUIRED
    Either “HTML” or “markdown”
    You’re telling me we can use markdown here? Nice. Medium posted acceptable tags and markdown formatting here in 2015.
  • content (string)
    REQUIRED
    Must be valid, semantic HTML or markdown blocks
  • canonicalUrl
    OPTIONAL
    Original URL if the content was published elsewhere first
  • tags (string array)
    OPTIONAL
    Note: Only three tags will be counted.
    Another note: Tags must not be longer than 25 characters.
  • publishStatus (string)
    OPTIONAL
    Must be “public”, “draft”, or “unlisted”
    Default: “public”
  • license (string)
    OPTIONAL
    Valid values are “all-rights-reserved”, “cc-40-by”, “cc-40-by-sa”, “cc-40-by-nd”, “cc-40-by-nc”, “cc-40-by-nc-nd”, “cc-40-by-nc-sa”, “cc-40-zero”, “public-domain”
    Default: “all-rights-reserved”
  • notifyFollowers (boolean)
    OPTIONAL
    Set this as “true” to notify a user’s followers that the post was created.
    Note: There’s no listed default value, and no field for it in a successful response body.

The response body of a successful post request is defined as a Post object:

The endpoint to create a post under the authenticated user’s account is /users/{{AUTHOR ID}}/posts. Replace the {{AUTHOR ID}} with your user ID (retrieved from the /me endpoint) to use it.

The following code successfully creates a test post under my account:

(I called this function with my ID, under the variable “gremID”.)

The HTTP response I get back looks like:

Does that reflect on my account? I went to the URL listed in that response, https://medium.com/@grem/c9131e57ba6b, to check if the post exists.

Sure enough, I had a draft listed there!

I did want to check if my title was persisted through the SEO settings, so I moved to the “More Settings” section for my post and found…

It did! And the title didn’t show up in the actual content of the post. Looks like that behavior was consistent with the docs.

Now, I regret to inform you that I don’t have a paid subscription to Medium, and can’t test the endpoint for posting under publications. That being said, I CAN show you what happens if I try to post under something that I don’t have the permissions to.

I borrowed the publication ID of the Medium Engineering blog for this test, and used the example request data in the docs. Its endpoint is /publications/{{PUBLICATIONID}}/posts.

As expected, I got an error in return.

It’s weird that the error code wasn’t a 403, but regardless, since we know I don’t have editor or writer permissions in that publication, I was “not allowed to publish in it”.

The most important change between these two use cases is that the user ID is replaced with the publication ID.

There’s also an important relationship between editors and writers here. The docs give us three rules:

  • If the authenticated user is an ‘editor’ for the publication, they can create posts with any publish status. Posts published as ‘public’ or ‘unlisted’ will appear in collection immediately, while posts created as ‘draft’ will remain in pending state under publication.
  • If the authenticated user is a ‘writer’ for the chosen publication, they can only create a post as a ‘draft’. That post will remain in pending state under publication until an editor for the publication approves it.
  • If the authenticated user is neither a ‘writer’ nor an ‘editor’, they are not allowed to create any posts in a publication.

To summarize, only an editor can post directly to a publication. Writers can only post drafts.

Summary

The total summary of information you can get from the Medium API is as follows:

  • Your user ID
  • Your username and human name
  • Your blog’s URL
  • Your icon’s URL
  • The publications you interact with
  • The contributors of any publication
  • Confirmation and metadata of a post created (through a request)
  • The URL and MD5 hash of an image you upload (through a request)

The functionality here seems just enough to do very simple automation for posting under your account, or a publication in which you are an editor or writer.

About the Author

Jess Schalz (she/they) is a software engineer and oddity collector. She has a terrible cat named Sudo, and the two of them live in Minneapolis, MN. She’s never successfully grown a garden, but tries every year nonetheless.

--

--

Jess Schalz

Software engineer turned technical writer and autistic as heck. (she/they)