Quick start
Four steps from nothing to reading a reader's library.Open a pairing
Send your app's name. You get back a device code to keep and a user code to show.
curl -X POST https://api.seekquel.app/reading/pair/start \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"device_name": "Shelfbound on Pixel 8", "app": "Shelfbound"}' { "device_code": "…", "user_code": "K7QD-M2XP", "expires_in": 900, "interval": 2 }Show the code to the reader
They enter it in Seekquel under Settings, Integrations, Another reading app, see your app's name, then approve or decline. The code lasts 15 minutes.
Poll for the key
Poll with the device code, waiting interval seconds between tries. Once the reader approves, the answer carries a key. It is sent once, so store it.
curl -X POST https://api.seekquel.app/reading/pair/poll \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"device_code": "…"}' { "key": "…", "api_url": "https://api.seekquel.app/reading", "device_id": "…" }Call the API
Send the key as a bearer token on every request. This one fetches every book that changed since the start of September.
curl "https://api.seekquel.app/reading/library?since=2026-09-01T00:00:00Z" \ -H "Authorization: Bearer <key>" \ -H "Accept: application/json"
How a reader connects
The same exchange used to sign a television into a streaming service.The reader's Seekquel password never reaches your app. Your app appears in their connected devices, and they can disconnect it there at any time. After that the key is refused.
What to send when you open a pairing
device_name- Required. Shown to the reader before they approve, so name your app and the device. Plain text: no angle brackets or line breaks.
app- Optional, and please send it. Your app's name, the same on every device and platform, so we see one app rather than a list of devices. Up to 60 characters of plain text.
platform- Optional. The operating system or hardware, for the reader's list of connected devices.
client- Optional. Your app's listed identifier, once it has one. Leave it out until then.
What polling answers
200- Approved. The body carries key, api_url and device_id. The key is returned once and never again, so store it.
400 authorization_pending- The reader has not answered yet. Wait interval seconds and poll again.
429 slow_down- You polled faster than interval. Back off and keep polling.
403 access_denied- The reader declined. Stop polling.
400 expired_token- The 15 minutes ran out. Start a new pairing.
400 invalid_grant- The device_code is unknown or was already used. Start a new pairing.
Endpoints
Every call is scoped to the one reader who connected you.Paths are relative to https://api.seekquel.app/reading. Every endpoint needs the key except the two pairing calls and the health check. The reference is English only, so it stays in step with the API.
Connecting
POST/pair/startOpen a pairing. Returns device_code, user_code, expires_in and interval with a 201. No key needed. The body is shown in the quick start.
POST/pair/pollPoll with device_code until the reader answers. No key needed. The body is shown in the quick start, and every answer is listed under How a reader connects.
GET/healthcheckAnswers while the door is open. No key needed.
Response{ "status": "ok" }
The reader's library
GET/libraryThe reader's books, oldest change first. Keep the updated_at of the last book you saw and send it back as since next time.
since- Optional. ISO 8601. Only books changed at or after it, so the last book you saw comes back once more. URL-encode it, or end it with Z rather than +00:00.
limit- Optional. Page size, 1 to 100. Default 50.
cursor- Optional. meta.next_cursor from the previous page. null there means you have reached the end.
work_id, private_book_id- Exactly one is set. A private book is one the reader added themselves, and has no slug.
rating- 0.5 to 5, or null.
media_kind- epub, pdf, audio or null.
progress_percent- 0 to 100, unlike percentage on the reading calls, which is 0 to 1.
{ "data": [ { "id": "01J8Q2W4E6R8T0Y2U4I6O8P0A2", "work_id": "01J5ZK8M3QH4R7T2V6W9X0Y1B2", "private_book_id": null, "edition_id": "01J5ZK8N1C3V5B7N9M1Q3W5E7R", "slug": "project-hail-mary", "title": "Project Hail Mary", "authors": [ "Andy Weir" ], "cover_url": "https://…/covers/….jpg", "status": "reading", "rating": null, "media_kind": "epub", "started_at": "2026-09-02", "finished_at": null, "times_read": 0, "current_page": 211, "page_count": 496, "progress_percent": 42.5, "shelves": [ "Science fiction" ], "updated_at": "2026-09-20T18:42:07+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": "…/reading/library?cursor=eyJ1c2VyX2Jvb2tzLnVwZGF0ZWRfYXQiOi…" }, "meta": { "path": "…/reading/library", "per_page": 50, "next_cursor": "eyJ1c2VyX2Jvb2tzLnVwZGF0ZWRfYXQiOi…", "prev_cursor": null } }GET/books/searchFind a catalogue book to link a document to. Send q, 2 to 200 characters, such as the title and author. Answers up to eight books; authors is one comma-separated string here.
Response{ "data": [ { "work_id": "01J5ZK8M3QH4R7T2V6W9X0Y1B2", "title": "Project Hail Mary", "authors": "Andy Weir", "year": 2021, "page_count": 496 } ] }
Documents
A document is created by its first PUT /progress, and until then every call here answers 404. Send the book's title and authors in that first push (or call /metadata straight after it) so it can be matched to a catalogue book; with no title it stays unidentified. Status, rating, sessions and highlights answer 409 document_not_linked until the document is matched, and you can settle that yourself with /link. Every call here answers the document as shown under the first one.
GET/documents/{document}Which book a document is matched to, and the reader's status and rating for it. {document} is your own stable identifier for the book file, up to 64 characters.
match_status- pending (not looked up yet), matched, imported (the reader's own private book), unmatched (nothing in the catalogue cleared the bar, so the reader is asked), unidentified (no title was ever sent), ignored or not_a_book.
book- null until the document is matched. Exactly one of work_id and private_book_id is set.
percentage- The last position you pushed, 0 to 1.
{ "data": { "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "match_status": "matched", "title": "Project Hail Mary", "authors": "Andy Weir", "percentage": 0.4231, "resume": null, "details_stored": true, "book": { "work_id": "01J5ZK8M3QH4R7T2V6W9X0Y1B2", "private_book_id": null, "title": "Project Hail Mary", "status": "reading", "rating": null, "current_page": 211, "page_count": 496 } } }PUT/documents/{document}/linkSay which catalogue book the document is, by work_id (an id from /books/search, or the book's slug). Send null when the book is not in the catalogue, and it becomes the reader's own private book.
Request{ "work_id": "01J5ZK8M3QH4R7T2V6W9X0Y1B2" }PUT/documents/{document}/metadataThe book file's own details, so we can work out which book it is. Every field is optional; send what the file carries.
authors- One string, as the file gives it.
identifiers- Free text. Any ISBN-10 or ISBN-13 in it is found, and an ISBN is the surest match there is.
page_count- The file's own page count. Used to turn your page numbers into the book's.
chapters- Up to 500, in reading order. title is required on each; page and depth are optional.
{ "title": "Project Hail Mary", "authors": "Andy Weir", "series": null, "series_index": null, "language": "en", "description": "Ryland Grace is the sole survivor on a desperate, last-chance mission…", "keywords": "Science fiction, Space", "identifiers": "isbn:9780593135204\nuuid:5b1f6a1e-2c0d-4e8a-9b7f-3d2c1b0a9e8f", "page_count": 496, "format": "epub", "filename": "Project Hail Mary - Andy Weir.epub", "file_size": 1843221, "chapters": [ { "title": "Chapter 1", "page": 1, "depth": 0 }, { "title": "Chapter 2", "page": 14, "depth": 0 } ] }PUT/documents/{document}/statusSet status to want_to_read, reading, paused, read or did_not_finish.
Request{ "status": "read" }PUT/documents/{document}/ratingThe reader's own star rating, 0.5 to 5 in half stars (tenths for a Pro reader who switched precise ratings on), or null to clear it. Rating a want-to-read book marks it read.
Request{ "rating": 4.5 }
Reading
PUT/progressThe reader's place in a book. The first push for a document creates it.
document- Required. Your stable identifier for the book file, up to 64 characters.
progress- Required. Your own position string, up to 2,000 characters. Handed back unchanged by GET /progress.
percentage- Required. How far through, as a fraction from 0 to 1 (0.42, not 42).
chapter, chapter_count- Optional. The chapter the reader is in, counted from 1, and how many there are.
device- Optional. The device's name, shown to the reader beside the position.
metadata- Optional. title, authors and filename, which is enough to match the book. /metadata takes the rest.
Response{ "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "progress": "epubcfi(/6/24!/4/2/8/1:112)", "percentage": 0.4231, "chapter": 12, "chapter_count": 38, "device": "Pixel 8", "metadata": { "title": "Project Hail Mary", "authors": "Andy Weir", "filename": "Project Hail Mary - Andy Weir.epub" } }{ "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "timestamp": 1758393727 }GET/progress/{document}Read the last saved place back, whichever device saved it. timestamp is Unix seconds. Answers 404 with an empty body when nothing is stored yet.
Response{ "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "progress": "epubcfi(/6/24!/4/2/8/1:112)", "percentage": 0.4231, "device": "Pixel 8", "device_id": null, "timestamp": 1758393727 }POST/sessionsReading time for one document, a day at a time, up to 400 days in one call. Accepted with a 202 and credited in the background.
date- Required. YYYY-MM-DD on the reader's clock. Not before 2008, and not after tomorrow.
seconds- Required. The day's total reading time for this book so far, not the time since your last call. Send a day again as it grows; nothing is counted twice.
hours- Optional. The same seconds split by hour of the reader's day, keyed 0 to 23. A map that does not add up is dropped and the day is still credited.
pages- Optional. Pages turned that day, in your own page numbers.
fraction- Optional. The share of the book read that day, 0 to 1. Preferred over pages when both are sent.
chapter- Optional. The chapter reached that day, counted from 1.
Response{ "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "days": [ { "date": "2026-09-19", "seconds": 2460, "pages": 31, "chapter": 11, "hours": { "21": 1800, "22": 660 } }, { "date": "2026-09-20", "seconds": 1320, "fraction": 0.018, "chapter": 12 } ] }{ "days_accepted": 2 }POST/highlightsPassages the reader marked, with their notes, up to 500 in one call. Accepted with a 202 and saved in the background.
external_id- Required. Your stable identifier for the highlight. Sending the same one again updates it rather than adding a second, and one the reader deleted in Seekquel is not brought back.
text- Required. The passage, up to 20,000 characters.
note, chapter, page, created_at, color- Optional. chapter is the chapter's title, created_at is ISO 8601, color is red, orange, yellow, green, olive, cyan, blue or purple, and any other name is kept without a colour.
Response{ "document": "000a5165b016fb6f25284edae4e6bcda6d0e79d879fbfd1e6902665d347eb373", "highlights": [ { "external_id": "hl-2026-09-20-0412", "text": "I penetrated the outer cell membrane with a nanosyringe.", "note": "Where it clicks.", "chapter": "Chapter 12", "page": 204, "created_at": "2026-09-20T21:14:03Z", "color": "yellow" } ] }{ "highlights_accepted": 1 }
Your app
PUT/deviceDescribe your app and its version, so a reader can tell which client sent what. Every field is optional. The answer carries the stored device under device; its other keys belong to the KOReader add-on and can be ignored.
Request{ "device_name": "Shelfbound on Pixel 8", "platform": "Android 16", "app_version": "2.3.0" }GET/requestsRecent requests from your app that did not succeed, newest first: the last 50 over 7 days. Read it before writing to us. A server error carries no message, only its request_id, which is what to quote.
fields- On a 422, each refused field with its reasons, as the error itself said.
request_id- Our reference for that one request. Also sent on every response as the X-Request-ID header.
{ "data": [ { "at": "2026-09-24T14:36:18+00:00", "method": "PUT", "path": "/reading/progress", "status": 422, "type": "validation_error", "code": "VALIDATION_FAILED", "message": "The percentage field must be between 0 and 1.", "fields": { "percentage": [ "The percentage field must be between 0 and 1." ] }, "request_id": "01M39XH24XKE132HY5S0E99DZQ" } ], "meta": { "kept": 50, "kept_days": 7 } }
Errors
Accept: application/json- Send it on every request. Without it, a refused field or an unknown document is answered as a redirect or a web page rather than in the shape below.
401 unauthenticated- The key is missing, wrong, or the reader disconnected your app. Pair again.
404- No document with that identifier yet. Push its position first.
409 document_not_linked- The document is not matched to a book yet. See the note under Documents.
422 validation_error- A field was refused. error.context.fields names each one with its reasons.
429- Over budget. Wait for the Retry-After header.
503 feature_disabled- Reading apps are switched off for now. Try again later; the key stays valid.
5xx- Ours to fix, and we are alerted. GET /requests lists it with its request_id.
{
"error": {
"type": "document_not_linked",
"code": "DOCUMENT_NOT_LINKED",
"message": "Tell us which book this file is before sending reading time or highlights for it."
}
}Limits and boundaries
What a connected app can reach, and how often.- Your app reads and writes that reader's own data and nothing else. No parameter widens it.
- No connected app can change the shared book catalogue. Corrections go through the same review as other readers' corrections.
- Budgets apply per key and per reader, so a reader with two apps connected shares one budget between them. Over budget, you get a 429 with a Retry-After header: wait that long. Use since on the library instead of reading all of it on a timer.
- A reader can have five reading apps connected at once. A sixth is refused until they disconnect one.
- Keep the key on your own server or in the device's secure storage, never in a web page: the API does not answer browsers on other sites. A key unused for 90 days stops working, and your app pairs again with a new code.
- Highlights from an app we have not listed arrive private, and the reader shares them from their notebook. Once your app is listed, its highlights follow the reader's own setting.
Budgets
/library- 60 requests a minute
/progress, /documents, /sessions, /highlights- 60 requests a minute, shared
/books/search- 30 requests a minute
/device, /requests- 10 requests a minute, shared
/pair/start- 5 a minute per network address
Getting your app listed
Readers can connect your app before it is listed.Once it works, write to us. If it holds up when we try it, your app gets its own entry on Seekquel's Integrations screen, with its name and setup steps. Until then readers connect it under Another reading app, which works the same way.
Write to [email protected]