Medium API & Scraper
The Medium API returns author and article data as clean JSON.
🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.
The primary user endpoint returns a user's id, username, name, bio and follower, following and post counts, and you can resolve a user_id, pull user_articles, user_top_articles, an article, its content and its HTML. It is built for content research and author intelligence that need Medium data without a scraper. One ReefAPI key, one shared credit pool, the standard envelope.
Medium ids, timestamps and the paywall you cannot see
Medium has no paywall flag in this response, two different date formats depending on which action you call, and two different pagination cursor shapes. Measured 2026-08-27 against @dhh, the Signal v. Noise publication and the python and machine-learning tag feeds.
| Field | What it holds | Measured 2026-08-27 |
|---|---|---|
| Article id | 12 lowercase hex characters, the tail of the URL slug | 41adf356857f, from .../reconsider-41adf356857f |
| user_id and collection_id | The same 12-hex shape as an article id | dhh is 54bcbf647830; Signal v. Noise is 668e14b18fb1 |
| published_at and created_at | Unix epoch MILLISECONDS, not seconds | 1446644507775 is 2015-11-04 |
| published_at from the tag action | An RFC-822 date string instead, because that surface is the topic feed | "Wed, 26 Aug 2026 22:02:12 GMT" |
| reading_time | Fractional minutes, not a rounded integer | 11.599056603773585 on a 2,822-word post |
| word_count against the returned body | word_count is Medium's full count; a member-only post returns only a free preview | One tagged post: word_count 1,338 against 160 body words. Five siblings returned 98 to 101% |
| next_cursor on user_articles and publication_articles | An epoch-milliseconds string | "1545257107680" |
| next_cursor on article_responses | offset:id, a different shape entirely | "45:ade9442ee53d" |
| collection_id | Non-null means the post was published in a publication, and its URL sits under the publication path | RECONSIDER lives at /signal-v-noise/..., not /@dhh/... |
| Publication slug | Follows renames, so the returned slug can differ from the one you sent | slug "towards-data-science" came back as slug "data-science", name "TDS Archive" |
| Body formats | article_content, article_html and article_markdown return the same paragraphs in three encodings | Same post, 67 paragraphs: 16,272 chars plain, 16,689 HTML, 16,306 markdown |
publication.created_at came back null on all three publications measured, and publication.url is inconsistent: a custom-domain publication returned "betterprogramming.pub" with no scheme while medium.com-hosted ones returned a full https URL. Normalize it before using it as a link.
Real request and response JSON
Captured from the indexed primary action, user, on .
{
"method": "POST",
"url": "https://api.reefapi.com/medium/v1/user",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"user": "dhh"
}
}{
"ok": true,
"meta": {
"api": "medium",
"endpoint": "user",
"mode": "live",
"latency_ms": 2111.4,
"record_count": 1,
"bytes": 38389,
"cache_hit": false,
"method": "format_json_profile"
},
"data": {
"user": {
"user_id": "54bcbf647830",
"username": "dhh",
"name": "DHH",
"bio": "Creator of Ruby on Rails, Founder & CTO at Basecamp (formerly 37signals), NYT Best-selling author of REWORK and REMOTE, and Le Mans class-winning racing driver.",
"created_at": 1391048788962,
"twitter_screen_name": "dhh",
"is_writer_program_enrolled": true,
"posts_published_count": 191,
"followers_count": 124738,
"following_count": 100,
"author_tags": [
{
"slug": "startup",
"name": "Startup",
"post_count": 495076
},
{
"slug": "artificial-intelligence",
"name": "[redacted-name]",
"post_count": 489834
},
{
"slug": "entrepreneurship",
"name": "Entrepreneurship",
"post_count": 401173
}
],
"publication_ids": [
"668e14b18fb1"
]
}
}
}What the Medium API does
| Action | Description | Concrete use case | Key params |
|---|---|---|---|
| user | Public Medium user profile by username. | Social-listening tools call user to get public Medium user profile by username.. | user |
| user_id | Resolve username to Medium userId. | Creator and influencer platforms call user_id to resolve username to Medium userId.. | user |
| user_articles | An author's published articles, newest first, with deep pagination. | Brand-monitoring teams call user_articles to get an author's published articles, newest first, with deep pagination.. | user, limit, cursor |
| user_top_articles | User articles sorted by clap count from profile references. | Audience analysts call user_top_articles to get user articles sorted by clap count from profile references.. | user, limit |
| article | Article metadata by url or id. | Social-listening tools call article to get article metadata by url or id.. | url, id |
| article_content | Article plain-text body from bodyModel paragraphs. | Creator and influencer platforms call article_content to get article plain-text body from bodyModel paragraphs.. | url, id |
| article_html | Article HTML body from bodyModel paragraphs. | Brand-monitoring teams call article_html to get article HTML body from bodyModel paragraphs.. | url, id |
| article_markdown | Article markdown body from bodyModel paragraphs. | Audience analysts call article_markdown to get article markdown body from bodyModel paragraphs.. | url, id |
| article_responses | Public responses/comments for an article. | Social-listening tools call article_responses to get public responses/comments for an article.. | url, id, limit, cursor |
| publication | Publication/collection metadata by slug. | Creator and influencer platforms call publication to get publication/collection metadata by slug.. | slug |
| publication_id | Resolve publication slug to collection id. | Brand-monitoring teams call publication_id to resolve publication slug to collection id.. | slug |
| publication_articles | A publication's articles, newest first, with deep pagination. | Audience analysts call publication_articles to get a publication's articles, newest first, with deep pagination.. | slug, limit, cursor |
| tag | Latest posts for a topic tag (the tag/topic feed). | Social-listening tools call tag to get latest posts for a topic tag (the tag/topic feed).. | tag, limit |
| search_users | Look up an author by exact username. (Medium gates fuzzy people-search behind a logged-in JS app, so this resolves q as a username and returns that profile.) | Creator and influencer platforms call search_users to look up an author by exact username. | q |
| search_publications | Look up a publication by exact slug. (Fuzzy publication search is login/JS-gated on Medium, so this resolves q as a publication slug and returns that publication.) | Brand-monitoring teams call search_publications to look up a publication by exact slug. | q |
| search_tags | Check whether a topic tag exists and is active on Medium (resolves q as a tag slug). | Audience analysts call search_tags to check whether a topic tag exists and is active on Medium (resolves q as a tag slug).. | q |
| followers | An author's followers (paginated). Identify the author by 'user' or 'user_id'. | Social-listening tools call followers to get an author's followers (paginated). | user, user_id, limit, cursor |
| following | The authors a user follows (paginated). Identify the user by 'user' or 'user_id'. | Creator and influencer platforms call following to get the authors a user follows (paginated). | user, user_id, limit, cursor |
| list | Reading list metadata and articles from public list page HTML. Provide the full list 'url', or 'id' together with 'user'. | Brand-monitoring teams call list to get reading list metadata and articles from public list page HTML. | id, url, user, username, slug, ... |
| search_articles | Search Medium articles by keyword — returns matching articles with title, URL and publication details from medium.com. | Audience analysts call search_articles to search Medium articles by keyword. | q, limit, cursor |
Call user from your stack
curl -X POST https://api.reefapi.com/medium/v1/user \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"user":"dhh"}'import requests
r = requests.post(
"https://api.reefapi.com/medium/v1/user",
headers={"x-api-key": REEF_KEY},
json={
"user": "dhh"
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/medium/v1/user", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"user": "dhh"
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.medium.user with {"user":"dhh"}.Who uses this API and why
- Content research calls user_articles to study an author's output.
- Author-intelligence uses user and user_top_articles to profile a writer.
- RAG pipelines use article_content to ingest a post as clean text.
Questions developers ask before integrating
What is a Medium post id and where do I find it?
It is the 12-character lowercase hex string at the end of every article URL. In https://medium.com/signal-v-noise/reconsider-41adf356857f the id is 41adf356857f. The article action takes either id or url, and on 2026-08-27 both returned identical objects. User ids and publication (collection) ids use the same 12-hex format, so a bare id on its own does not tell you what kind of object it points at.
Are Medium timestamps in seconds or milliseconds?
Milliseconds, on every action except one. created_at and published_at come back as epoch milliseconds, so 1446644507775 is 2015-11-04, and dividing by 1000 first is the usual fix for a date that lands in 1970. The exception is the tag action, which reads the topic feed and returns published_at as an RFC-822 string like "Wed, 26 Aug 2026 22:02:12 GMT". If you merge tag results with article results, normalize the dates first.
Is there a member-only or paywall flag on an article?
No. The article object has fourteen keys and none of them marks a post as member-only. What you can do is compare word_count against the body you actually got. On 2026-08-27, five of six posts pulled from the machine-learning feed returned 98 to 101% of their stated word_count, and the sixth returned 160 words against a word_count of 1,338. That twelve-percent ratio is the free preview of a locked post. paragraph_count in meta is a useful second signal: the truncated post had 9 paragraphs, the full ones had 54 and more.
Why is reading_time a decimal?
It is Medium's raw estimate in minutes and it is not rounded: a 2,822-word post returned 11.599056603773585 and a 564-word post returned 2.128301886792453. Round it yourself for display. The underlying arithmetic is roughly 265 words a minute plus image time, which is why it never lands on a whole number.
Why did user_articles return four articles when I asked for ten?
Because limit is an upper bound on one page of the profile stream, not a guarantee. Asking for 10 from @dhh returned 4 articles with has_more true and a next_cursor, even though that profile's posts_published_count is 191. Keep passing the previous response's next_cursor until has_more goes false. Ordering on page one is not strictly newest-first either: the first item returned was from 2015 and the second from 2019, because the profile's featured post leads the stream.
Does user_top_articles give me an author's all-time most-clapped posts?
No, and the difference matters. It sorts the same profile-stream page by clap count rather than asking Medium for a global ranking. Measured on @dhh, both actions returned the same four articles, with user_top_articles reordering them to 31,648 / 4,322 / 3,479 / 1,424 claps. For a genuine all-time top you need to page through user_articles with next_cursor and sort the full set yourself.
How do I tell an author's own post from one published in a publication?
Read collection_id. When it is null the post sits on the author's profile at medium.com/@handle. When it is set, the post was published in that publication and its URL uses the publication path: RECONSIDER carries creator_id 54bcbf647830 (dhh) and collection_id 668e14b18fb1, and lives at medium.com/signal-v-noise/reconsider-41adf356857f. The publication action turns a slug into that same id, so you can join the two.
What does a Medium response look like in the data?
A response is itself a post, not a comment record. Each one has its own 12-hex id, its own URL under the responder's handle, a clap count and a created_at in milliseconds. The title field is an auto-generated truncation of the opening line, so it often repeats the start of text. Responses page with a cursor shaped offset:id, for example "45:ade9442ee53d", which is a different format from the epoch cursor used by user_articles.
What is the Medium API?
Medium API is a ReefAPI endpoint group for medium It returns live JSON through POST requests under /medium/v1.
Is the Medium API free to try?
Yes. ReefAPI starts with 1,000 free credits, no card required. Medium calls use the same shared credit balance as every other ReefAPI engine.
Do I need a Medium login or account?
No login to Medium is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.
How fresh is the Medium data?
The page example is captured from a live user call, and production requests fetch live data through ReefAPI rather than a static sample.
How many credits does the Medium API use?
Medium actions currently cost 1 credit per successful call. Failed or blocked calls are free, and all APIs draw from one credit pool.
Can I call Medium from an AI assistant or MCP client?
Yes. Connect ReefAPI once through MCP and your assistant can call medium actions with the same key, credit pool and JSON envelope used by normal REST requests.