Miniflux Google Reader API
This document describes the Google Reader compatible API implemented by the internal/googlereader package in this repository.
Miniflux implements a compatibility subset intended for existing Google Reader clients. It is not a full reimplementation of the historical Google Reader API, and several behaviors are intentionally narrower or implementation-specific.
Endpoint
- Client login path:
BASE_URL/accounts/ClientLogin - API prefix:
BASE_URL/reader/api/0 BASE_URLincludes the Miniflux root URL and any configuredBasePath- Response format:
ClientLogin: plain text by default, JSON whenoutput=json- most API reads: JSON
- most API writes: plain text
OK
Enabling the API
Google Reader compatibility is configured per user from the Miniflux integrations page.
Google Reader APImust be enabledGoogle Reader Usernamemust be unique across all Miniflux usersGoogle Reader Passwordis stored as a bcrypt hash
The Google Reader username and password are separate integration credentials. They are not the Miniflux account password.
Authentication
POST /accounts/ClientLogin
This endpoint exchanges the configured Google Reader username and password for an auth token.
Form parameters:
Email: Google Reader usernamePasswd: Google Reader passwordoutput: optional, set tojsonfor a JSON response
Successful responses:
- default: plain text
- with
output=json: JSON
Example plain-text response:
SID=readeruser/0123456789abcdef...
LSID=readeruser/0123456789abcdef...
Auth=readeruser/0123456789abcdef...
Example JSON response:
{
"SID": "readeruser/0123456789abcdef...",
"LSID": "readeruser/0123456789abcdef...",
"Auth": "readeruser/0123456789abcdef..."
}
On authentication failure, ClientLogin returns HTTP 401 with the normal JSON error body:
{
"error_message": "access unauthorized"
}
Auth token format
The token format is:
<googlereader_username>/<hex_digest>
The digest is generated server-side from:
- the Google Reader username
- the stored bcrypt hash of the Google Reader password
Specifically, the code computes an HMAC-SHA256 digest of an empty message using the key:
googlereader_username + bcrypt_hash
Because the bcrypt hash is only known to the server, clients should not try to precompute the token. Use ClientLogin or GET /reader/api/0/token.
Authenticating API calls
Miniflux uses different auth mechanisms for GET and POST requests:
GETrequests must send the headerAuthorization: GoogleLogin auth=<token>POSTrequests are authenticated withT=<token>read from the parsed form values
Notes:
- the auth scheme must be exactly
GoogleLogin - the auth field name must be exactly lowercase
auth - for
POST,Tmay come from the URL query or the form body because the server reads merged form values POSTrequests do not accept the token from theAuthorizationheaderGETrequests do not accept the token from the query string
GET /reader/api/0/token
This endpoint requires normal GET authentication and returns the same token as plain text.
Many Google Reader clients use this as the edit token for subsequent write requests. In Miniflux, the edit token and auth token are the same value.
Authentication failure on /reader/api/0/*
When API authentication fails under /reader/api/0, Miniflux returns:
- HTTP
401 - header
X-Reader-Google-Bad-Token: true - content type
text/plain; charset=utf-8 - body
Unauthorized
This is different from ClientLogin, which returns a JSON 401.
Identifier formats
Stream IDs
The implementation recognizes these stream forms:
- built-in streams:
user/-/state/com.google/readuser/-/state/com.google/starreduser/-/state/com.google/reading-listuser/-/state/com.google/kept-unreaduser/-/state/com.google/broadcastuser/-/state/com.google/broadcast-friendsuser/-/state/com.google/like
- user-specific equivalents:
user/<user_id>/state/com.google/...
- label streams:
user/-/label/<name>user/<user_id>/label/<name>
- feed streams:
feed/<value>
Important feed stream difference:
- read APIs usually emit
feed/<numeric_feed_id> subscription/editwithac=subscribeexpectsfeed/<absolute_feed_url>subscription/editwithac=editorac=unsubscribeexpectsfeed/<numeric_feed_id>
So feed/<...> is not a single stable identifier format across all endpoints.
Item IDs
edit-tag and stream/items/contents accept repeated i parameters in all of these formats:
- long Google Reader form:
tag:google.com,2005:reader/item/00000000148b9369 - short prefixed hexadecimal form:
tag:google.com,2005:reader/item/2f2 - bare 16-character hexadecimal form:
000000000000048c - decimal entry ID:
12345
Responses use different forms depending on endpoint:
stream/items/idsreturns decimal IDs as stringsstream/items/contentsreturns long-form Google Reader item IDs
Common response conventions
JSON errors use this shape:
{
"error_message": "..."
}
Plain-text success responses from write endpoints are usually:
OK
POST parameter parsing
Most POST handlers call ParseForm() and read from r.Form, so parameters may be supplied either in the query string or in a standard form body.
Important exception:
POST /reader/api/0/edit-tagreadsaandrfromr.PostForm, so those tag lists must come from the request body
Because GET auth comes only from the Authorization header, query parameters never authenticate GET requests even when other parameters are read from the query string.
Endpoint reference
GET /reader/api/0/user-info
Returns JSON only. No output=json parameter is required.
Response fields:
userId: Miniflux user ID as a stringuserName: Miniflux usernameuserProfileId: same value asuserIduserEmail: same value asuserName
Example:
{
"userId": "1",
"userName": "demo",
"userProfileId": "1",
"userEmail": "demo"
}
GET /reader/api/0/tag/list?output=json
Returns the starred state and user labels.
Notes:
output=jsonis required- only labels and the starred state are returned
- built-in states such as
readandreading-listare not listed here
Response shape:
{
"tags": [
{
"id": "user/1/state/com.google/starred"
},
{
"id": "user/1/label/Tech",
"label": "Tech",
"type": "folder"
}
]
}
GET /reader/api/0/subscription/list?output=json
Returns the user's feeds.
Notes:
output=jsonis required- each feed is reported with a numeric feed stream ID such as
feed/42 categoriesalways contains the Miniflux category as a Google Reader folder
Response shape:
{
"subscriptions": [
{
"id": "feed/42",
"title": "Example Feed",
"categories": [
{
"id": "user/1/label/Tech",
"label": "Tech",
"type": "folder"
}
],
"url": "https://example.org/feed.xml",
"htmlUrl": "https://example.org/",
"iconUrl": "https://miniflux.example.com/icon/..."
}
]
}
POST /reader/api/0/subscription/quickadd
Subscribes to the first discovered feed for the given absolute URL.
Form parameters:
T: auth tokenquickadd: absolute URL
Response shape when a feed is found:
{
"numResults": 1,
"query": "https://example.org/feed.xml",
"streamId": "feed/42",
"streamName": "Example Feed"
}
Response shape when no feed is found:
{
"numResults": 0
}
Notes:
- the request URL must be absolute
- the created subscription is assigned to the user's first category when no explicit category is provided
POST /reader/api/0/subscription/edit
Edits subscriptions. Successful requests return plain text OK.
Form parameters:
T: auth tokenac: actions: repeated stream IDa: optional destination label streamt: optional title
Supported actions:
ac=subscribeac=unsubscribeac=edit
Behavior by action:
subscribe- only the first
svalue is used smust befeed/<absolute_feed_url>a, when present, must be a label streamt, when present, becomes the feed title after creation
- only the first
unsubscribe- every
smust befeed/<numeric_feed_id>
- every
edit- only the first
svalue is used smust befeed/<numeric_feed_id>trenames the feedamoves the feed to a label, and must be a label stream
- only the first
Notable limitations:
- removing a label is not implemented here
subscribe,edit, andunsubscribedo not share the same feed ID format
POST /reader/api/0/rename-tag
Renames a label. Successful requests return plain text OK.
Form parameters:
T: auth tokens: source label streamdest: destination label stream
Rules:
- both
sanddestmust be label streams - the destination label name must not be empty
- if the source label does not exist, the endpoint returns HTTP
404
POST /reader/api/0/disable-tag
Deletes one or more labels and reassigns affected feeds to the user's first remaining category.
Form parameters:
T: auth tokens: repeated label stream
Rules:
- only label streams are supported
- at least one category must remain after deletion, otherwise the operation fails
Successful requests return plain text OK.
POST /reader/api/0/edit-tag
Marks entries read or unread and starred or unstarred.
Form parameters:
T: auth tokeni: repeated item IDa: repeated tag stream to addr: repeated tag stream to remove
Supported tag semantics:
- add
user/.../state/com.google/read: mark read - remove
user/.../state/com.google/read: mark unread - add
user/.../state/com.google/kept-unread: mark unread - remove
user/.../state/com.google/kept-unread: mark read - add
user/.../state/com.google/starred: star - remove
user/.../state/com.google/starred: unstar
Special cases:
readandkept-unreadcannot be combined in conflicting ways in the same requeststarredcannot be present in both add and removebroadcastandlikeare recognized but ignored- unsupported tag types cause an error
Successful requests return plain text OK.
GET /reader/api/0/stream/items/ids?output=json
Returns item IDs for one stream.
Required query parameters:
output=jsons=<stream_id>
Optional query parameters:
n: maximum number of items to returnc: numeric offset continuation tokenr: sort direction,ofor ascending, anything else for descendingot: only items published after this Unix timestamp in secondsnt: only items published before this Unix timestamp in secondsxt: repeated exclude target streamit: repeated filter target stream, parsed but currently ignored
Supported s values:
user/.../state/com.google/reading-listuser/.../state/com.google/starreduser/.../state/com.google/readfeed/<numeric_feed_id>
Notes:
- exactly one
svalue is expected - label streams are not supported here
- when
xtcontains thereadstream,reading-listandfeed/<id>behave as unread-only queries - if
nis omitted, the query is effectively unbounded continuationis a numeric offset encoded as a JSON string, not an opaque token
Response shape:
{
"itemRefs": [
{
"id": "12345"
},
{
"id": "12344"
}
],
"continuation": "2"
}
POST /reader/api/0/stream/items/contents
Returns content for specific items.
Required parameters:
T: auth tokenoutput=jsoni: repeated item ID
Optional query parameters:
r: sort direction,ofor ascending, anything else for descending
Implementation notes:
- the route is
POSTonly T,output, andiare read from merged form values, so they may be supplied in the query string or the form body- the handler parses stream filter query parameters, but in practice only the sort direction affects the result
Response shape:
{
"direction": "ltr",
"id": "user/-/state/com.google/reading-list",
"title": "Reading List",
"self": [
{
"href": "https://miniflux.example.com/reader/api/0/stream/items/contents"
}
],
"updated": 1710000000,
"author": "demo",
"items": [
{
"id": "tag:google.com,2005:reader/item/00000000148b9369",
"categories": [
"user/1/state/com.google/reading-list",
"user/1/label/Tech",
"user/1/state/com.google/starred"
],
"title": "Example entry",
"crawlTimeMsec": "1710000000123",
"timestampUsec": "1710000000123456",
"published": 1710000000,
"updated": 1710000300,
"author": "Author",
"alternate": [
{
"href": "https://example.org/post",
"type": "text/html"
}
],
"summary": {
"direction": "ltr",
"content": "<p>Content</p>"
},
"content": {
"direction": "ltr",
"content": "<p>Content</p>"
},
"origin": {
"streamId": "feed/42",
"title": "Example Feed",
"htmlUrl": "https://example.org/"
},
"enclosure": [],
"canonical": [
{
"href": "https://example.org/post"
}
]
}
]
}
Notes:
- top-level
idandtitleare hard-coded as the reading list summary.contentandcontent.contentboth contain the rewritten entry content- enclosure URLs and embedded media may be rewritten through the Miniflux media proxy
POST /reader/api/0/mark-all-as-read
Marks items as read before a timestamp. Successful requests return plain text OK.
Form parameters:
T: auth tokens: stream IDts: optional timestamp
Supported s values:
feed/<numeric_feed_id>user/.../label/<name>user/.../state/com.google/reading-list
Timestamp handling:
- if
tshas at least 16 digits, it is interpreted as microseconds since the Unix epoch - otherwise it is interpreted as seconds since the Unix epoch
- if
tsis omitted, Miniflux uses the current server time
Notes:
- only unread entries published before
tsare marked as read - unsupported stream types are effectively a no-op and still return
OK
Catch-all unimplemented endpoints
Any other GET or POST path under /reader/api/0/ is caught by the fallback handler and returns:
[]
with HTTP 200.
Compatibility notes and deviations
These differences are important for client authors:
- only a subset of Google Reader endpoints is implemented
- feed stream IDs are numeric in read responses, but
ac=subscribeexpectsfeed/<absolute_feed_url> stream/items/idsreturns decimal entry IDs, whilestream/items/contentsreturns long-form Google Reader item IDs- pagination uses
cas a numeric SQL offset, not an opaque continuation token itfilter targets are parsed but currently ignoredtag/listreturns onlystarredand user labels- API auth failures under
/reader/api/0/*return plain text401 Unauthorized, not JSON - unknown
/reader/api/0/*endpoints return[]with200, not404