Keep a local copy of a MangaBaka library up to date. Read it once, store a cursor, and from then on fetch only what changed, removals included.
The changes feed carries the user's own tracking data: lists, entries, progress, ratings, notes and list membership. It has no series titles or covers. Fetch those separately with batch series lookup and cache them.
Access#↑
Call the feed with an OAuth token that has the library.read scope, sent as Authorization: Bearer, which the examples below use. For your own library, a personal access token works too: send it as x-api-key: mb-your-token instead.
1. Bootstrap#↑
Call without a cursor to read the whole library:
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.mangabaka.org/v2/my/library/changes?limit=250"
limit defaults to 250 and goes up to 1000. During a bootstrap it counts entries only: every list arrives on the first page, so that page can hold more than limit items.
2. Apply each page, then store its cursor#↑
Each item in data is { type, action, revision, current }:
type + action |
current holds |
What to do |
|---|---|---|
entry + upsert |
The full entry, with list_ids |
Insert or replace it |
entry + delete |
Only series_id |
Remove the entry |
list + upsert |
id, name, is_private |
Insert or replace the list |
list + delete |
Only id |
Remove the list |
- Key entries by
series_id. An entry'sidcan change when a user wipes and reimports their library. - Apply items in order. The last state per key wins.
currentis the state when the page was read, not a history. Ten edits arrive as one upsert, and a change made while you page shows up again later.list_idsmay name a list you have not received yet. Treat it as not loaded yet.- Ignore
typeandactionvalues you don't know and keep going. - Deleting a list also sends an
entryupsert for each entry that was on it, with the list gone from itslist_ids. revisiononly orders changes. You don't need to store or compare it, but if you do, it needs a 64-bit integer.- Treat the cursor as opaque: store it exactly as received, URL-encode it when you send it, and never build one yourself.
- Don't watch
updated_atfor changes. List membership can change without touching it. The feed is the only reliable signal. - An entry stays in the feed when its series is merged or removed from the catalog. Keep it until the feed deletes it. A series missing from a batch lookup is not a library deletion.
start_dateandfinish_dateare calendar days. Store them as days, not as timestamps in the user's timezone.
A page after a few changes, entry fields trimmed:
{
"status": 200,
"pagination": {
"limit": 250,
"cursor": "eyJ2IjoxLCJtb2RlIjoic2luY2UiLCJyZXYiOjE3OTMxMTI3MjM5MDc5ODJ9",
"more": false
},
"data": [
{
"type": "list",
"action": "upsert",
"revision": 1793112723591163,
"current": {
"id": 10059,
"name": "Favourites",
"is_private": false
}
},
{
"type": "entry",
"action": "delete",
"revision": 1793112723839525,
"current": {
"series_id": 2
}
},
{
"type": "list",
"action": "delete",
"revision": 1793112723907982,
"current": {
"id": 10060
}
},
{
"type": "entry",
"action": "upsert",
"revision": 1793112723907982,
"current": {
"id": 3477552,
"series_id": 1,
"state": "reading",
"rating": 85,
"progress_chapter": 12,
"start_date": "2026-10-27",
"updated_at": "2026-10-27T14:52:03.229Z",
"list_ids": [10059]
}
}
]
}
The full entry fields are in the reference.
Store pagination.cursor only after the page is applied, ideally in the same local transaction. A crash then resumes from the last stored cursor and repeats at most one page, so make upserts and deletes safe to repeat.
3. Follow more, then poll#↑
While pagination.more is true, call again right away with the new cursor:
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.mangabaka.org/v2/my/library/changes?cursor=STORED_CURSOR&limit=250"
When more is false you are caught up. Keep that last cursor: on your next poll it returns only what changed since. No changes means 200 with empty data and more: false.
Run one sync at a time per user. Parallel page requests can apply pages out of order.
How often to poll#↑
- Apps people open: on launch, on returning to the foreground and on manual refresh. In the background, at most every 15 minutes.
- An empty page still counts against your rate limit. The limit is per client IP, so a server syncing many users shares one budget across all of them. The next section avoids polling users with nothing new.
- After an error, keep your stored cursor and retry on the next poll.
Errors and rebuilds#↑
| Response | What to do |
|---|---|
400 Invalid cursor: bootstrap again without one |
Bootstrap that user again. |
Any other 400, such as a bad limit |
Fix the request. Don't rebuild. |
401 or 403 |
Refresh the OAuth token, or ask the user to authorize again. A rejected token says nothing about the library being empty. |
429 or 5xx |
Keep your cursor and retry later. |
Bootstrap again whenever you lose the local copy or no longer trust it: a cursor without its data can't rebuild a library. Build the new copy beside the old one and swap when the last page is in, since merging a bootstrap into old data keeps entries that are gone. Then poll once right away with the final cursor, to pick up changes made while you were reading.
Many users on a server#↑
Polling every user's feed on a timer is wasteful. Changed users lists which users of your OAuth app have changes you have not fetched:
curl -H "x-api-key: APP_OWNER_TOKEN" \
"https://api.mangabaka.org/v2/my/apps/YOUR_CLIENT_ID/library/changes?limit=250"
- It takes a personal access token of the app's owner. Anyone else gets
404. - It only lists users who granted your app
library.read. - Without a cursor you get a full listing. Page it with
pagination.cursorandmore, same as the feed. A bad cursor is400: run a full listing. - For each user, call the feed with that user's OAuth token.
user_idis thesubclaim from that user's ID token or the userinfo endpoint, so store it when they sign in.
Store this cursor only after every user on the page is synced or saved in your own retry queue. A user you skip past may not show up again until their library changes again.
Each listed user comes with a feed cursor and url. They mark where the last feed run for your app ended, not what you stored. Prefer the feed cursor you stored yourself. cursor: null means bootstrap.
Fetching the last page of a feed with an OAuth token marks that user as synced for your app. Don't fetch pages you won't apply.
A user who revokes your app's access drops out of the listing, and no delete events are sent for your copy. If they grant access again they come back with cursor: null. Resume from your own stored cursor if you kept their data, or bootstrap.
Personal data#↑
The feed holds the user's private tracking data, including removals. Use each user's own token, keep the owner's PAT on your server, and keep responses out of shared caches and public logs. Delete your copy of a user's library as part of your own account and consent handling.