Sync a user's library

Recipes for specific tasks against the API.

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's id can change when a user wipes and reimports their library.
  • Apply items in order. The last state per key wins.
  • current is 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_ids may name a list you have not received yet. Treat it as not loaded yet.
  • Ignore type and action values you don't know and keep going.
  • Deleting a list also sends an entry upsert for each entry that was on it, with the list gone from its list_ids.
  • revision only 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_at for 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_date and finish_date are 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.cursor and more, same as the feed. A bad cursor is 400: run a full listing.
  • For each user, call the feed with that user's OAuth token. user_id is the sub claim 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.