API Reference

NoviList Public API v1

GraphQL access to the catalog, reading lists, and user data. Build extensions, integrations, and custom tools.

Endpoint

GraphQL Endpoint

https://api.novilist.co/api/v1/graphql

An interactive schema explorer is available at https://api.novilist.co/api/v1/explorer. Use it to browse types, run test queries, and inspect the full public schema.

Use HTTPS for production requests. The explorer and live introspection are the authoritative machine-readable v1 contract.

Credentials

Low-volume catalog queries and introspection can be tried without credentials. Use an application key for an attributable catalog quota, or OAuth when acting for a user.

CATALOG

Application Key

For read-only catalog access without user context. Send via the X-Application-Key header.

HTTP Headers
POST /api/v1/graphql HTTP/1.1
Host: api.novilist.co
Content-Type: application/json
X-Application-Key: nvl_app_...

Application keys identify your app for quotas and attribution. They are expected to be discoverable in browser, extension, or mobile code, so never treat one as proof of a user's identity or authorization.

USER

Bearer Token

For user-specific requests (reading lists, saved progress). Send via the standard Authorization header.

Authorization Header
Authorization: Bearer nvl_usr_...

Bearer tokens are reusable for up to one year and are tied to one application and user. Store them securely. The short-lived authorization code—not the bearer token—is single-use.

Do not mix credentials. Sending both an application key and a bearer token in the same request will be rejected. Use one or the other.

Registering an Application

Before using the API, register an application in developer settings. Registration is immediate and returns:

ID

Client ID

A stable, non-secret identifier for your app.

Key

Application Key

Shown once. Used in the X-Application-Key header.

URI

Redirect URIs

Exact callback URIs and browser origins permitted for the application.

You may register up to 20 applications. Key rotation invalidates the previous key. Disabling or deleting an application invalidates all associated user tokens.

User Authorization

NoviList uses OAuth 2.0 with mandatory PKCE S256. There are no client secrets or refresh tokens. Tokens are valid for one year.

Authorization Flow

1 Generate PKCE values
2 User grants consent
3 Exchange code for token
4 Use bearer token
1

Generate PKCE values

Create a cryptographically random state, a 43–128 character code_verifier, and its base64url-encoded SHA-256 hash called code_challenge.

JavaScript
// Generate PKCE values
function generatePKCE() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  const verifier = btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
    .then(buf => {
      const challenge = btoa(String.fromCharCode(...new Uint8Array(buf)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
      return { verifier, challenge };
    });
}
2

Redirect the user

Build the authorization URL and redirect the user to grant consent. The redirect_uri must exactly match a registered URI.

Authorization URL
https://api.novilist.co/api/v1/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=user%3Aread+readinglist%3Aread
  &state=RANDOM_STATE
  &code_challenge=YOUR_CHALLENGE
  &code_challenge_method=S256
3

Verify the callback

After the user consents, they are redirected to your URI with ?code=...&state=.... Verify state matches the value you sent.

4

Exchange for a bearer token

POST the authorization code within two minutes. The code is single-use and expires quickly.

Token Exchange
POST /api/v1/oauth/token HTTP/1.1
Host: api.novilist.co
Content-Type: application/x-www-form-urlencoded

code=AUTH_CODE
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&code_verifier=YOUR_VERIFIER

Response:

JSON
{
  "access_token": "nvl_usr_...",
  "token_type": "Bearer",
  "expires_in": 31536000,
  "scope": "user:read readinglist:read"
}
Authorization fails if the application is disabled, the redirect URI does not match, the code is reused or expired, or PKCE verification fails.

Scopes

Scopes control what operations a bearer token can perform. Request only the scopes you need. Scopes do not imply one another.

ScopeAccess
catalog:readCatalog access when using a user bearer token
submissions:writecreateNovelSubmission — create novel proposals
user:readviewer — your profile and settings
readinglist:readreadingListCollection — all reading lists
readinglist:writesaveReadingListEntry — add/update entries
catalog:lookupsubmissionEntityLookup — entity metadata
submissions:readmySubmissions — your submissions

Catalog-only requests using an application key do not require OAuth. Add user-specific scopes via bearer token when needed.

Catalog

The public catalog includes novels with titles, covers, release dates, staff credits, publishers, tags, genres, relations, and public external links. Disabled links and internal notes are excluded.

SearchNovels
query SearchNovels($query: String!, $first: Int, $after: String) {
  searchNovels(query: $query, first: $first, after: $after) {
    nodes {
      id
      title { english romanized native }
      coverImage { medium }
      status
      tags { id name category }
      novelStaff { role staff { id name { full native } } }
      novelPublishers { isOfficial publisher { id name } }
      externalLinks { platform url purpose language }
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}
StaffAndPublisher
query StaffAndPublisher($staffId: ID!, $publisherId: ID!) {
  staff(id: $staffId) {
    id
    name { full native }
    externalLinks { platform url }
  }
  publisher(id: $publisherId) {
    id
    name
    publisherType
    externalLinks { platform url }
  }
  genreCollection { genre count }
}

Pagination

List queries use cursor-based pagination. This is more efficient than offset-based pagination for large datasets and provides consistent results when items are inserted or deleted between requests.

Arguments

first Number of items to return (search default: 20, max: 100)
after Cursor from the previous page

pageInfo

hasNextPage True if more results exist
endCursor Pass as the next after value

To fetch the next page, take the endCursor from the previous response and pass it as the after argument. Stop when hasNextPage is false.

Pagination Example
query SearchNovels($query: String!, $after: String) {
  searchNovels(query: $query, first: 20, after: $after) {
    nodes {
      id
      title { english romanized }
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}

# First page: omit 'after'
# Variables: { "query": "mushoku" }

# Second page: use endCursor from first response
# Variables: { "query": "mushoku", "after": "cursor_abc123" }

Filtering

The searchNovels query accepts optional filter arguments to narrow results. All filters are combinable.

ArgumentTypeDescription
queryStringFull-text title search; optional when filtering
status[NovelStatus]ONGOING, COMPLETED, HIATUS, CANCELLED
tags[String]Include tag names
genres[String]Include genre names
authorStringFilter by author name
publisherStringFilter by publisher name
countryOfOrigin[NovelCountryOfOrigin!]JP, KR, CN
Filtered Query
query FilteredSearch {
  searchNovels(
    query: "mushoku"
    status: [ONGOING]
    genres: ["Fantasy"]
    first: 10
  ) {
    nodes {
      id
      title { english romanized }
      status
      tags { name category }
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}

Other filters include excludedTags, excludedGenres, isAdult, isLicensed, completelyTranslated, and year. Consult introspection for exact argument types instead of hard-coding a copied schema.

Reading Lists

Authenticated users can read their reading lists and update progress. The public API uses cursor pagination with after and first arguments.

MyReadingList
query MyReadingList {
  readingListCollection {
    lists {
      name
      status
      count
      entries(first: 25) {
        nodes {
          id
          status
          progress
          score
          novel {
            id
            title { english romanized }
          }
        }
        pageInfo { hasNextPage endCursor }
        totalCount
      }
    }
  }
}
UpdateProgress
mutation UpdateProgress($novelId: ID!, $progress: Int!) {
  saveReadingListEntry(novelId: $novelId, progress: $progress) {
    id
    status
    progress
    updatedAt
  }
}

Without userId or userName, the query targets the authenticated user. Another user's public list may be requested explicitly; private entries remain filtered.

Submission Tools

The public API can look up likely catalog matches, read the authenticated user's submission history, and create new-novel or existing-novel edit proposals.

MySubmissions
query MySubmissions($after: Cursor) {
  mySubmissions(after: $after, first: 25) {
    nodes {
      id
      kind
      status
      displayName
      rejectionReason
      liveEntityId
    }
    pageInfo { hasNextPage endCursor }
    totalCount
  }
}
CreateNovelSubmission
mutation CreateNovelSubmission($input: CreateNovelSubmissionInput!) {
  createNovelSubmission(input: $input) {
    id
    submissionStatus
    isNewNovel
    requiresReview
  }
}

Use submissions:read for history and submissions:write for creation. Reuse a client-generated idempotencyKey when retrying the same attempt and set autoSubmit: true to enter the normal review pipeline. Use catalog:lookup for submissionEntityLookup. Submission notes are user-supplied content; do not render them as trusted HTML.

Responses and Errors

Executed operations follow the standard GraphQL format: a data key for successful operations, or an errors array when something goes wrong. Authentication failures that occur before GraphQL execution may instead be an HTTP JSON response containing top-level error and code fields.

Success

JSON
{
  "data": {
    "searchNovels": {
      "nodes": [...],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "cursor_abc123"
      },
      "totalCount": 42
    }
  }
}

Error

JSON
{
  "errors": [{
    "message": "Invalid application key",
    "extensions": {
      "code": "INVALID_APPLICATION_KEY"
    }
  }]
}

Authorization and validation error codes:

INVALID_APPLICATION_KEY
INVALID_ACCESS_TOKEN
AMBIGUOUS_API_CREDENTIALS
PUBLIC_API_SCOPE_REQUIRED
PUBLIC_API_OPERATION_NOT_ALLOWED

Rate Limits

App Key

120 / min

Per application

User Token

300 / min

Per access token

IP Burst

20 / 10s

Short-term burst protection

Response Headers

X-RateLimit-Limit Maximum requests per window
X-RateLimit-Remaining Requests remaining in current window
X-RateLimit-Reset Unix timestamp when the window resets
Retry-After Seconds to wait (only on 429 responses)
Rate-Limited Response
HTTP/1.1 200 OK
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1718430060

{"data": { ... }}
When rate-limited (HTTP 429), wait for the duration specified in Retry-After before retrying. Exponential backoff is recommended for repeated 429 responses.

These are current defaults and may change. Treat the response headers as authoritative. GraphQL complexity limits are enforced separately from request quotas.

Versioning

The API is versioned via the URL path: /api/v1/graphql. Introspection reports only the public v1 schema.

Non-Breaking (within v1)

  • + Adding new fields or types
  • + Adding new enum values
  • + Adding new query arguments

Breaking (requires new version)

  • x Removing or renaming fields
  • x Changing nullability
  • x Removing enum values

Developer application management uses the session-authenticated site schema, not public credentials.

Account security, application management, social features, moderation, administration, maintenance, chapter ingestion, and submission writes are not part of public API v1.

Acceptable Use

  • 1 Identify integrations accurately and do not imply they are official NoviList products without written permission.
  • 2 Cache stable catalog responses where practical and honor rate-limit and retry headers.
  • 3 Do not evade quotas through multiple applications, credentials, accounts, or network addresses.
  • 4 Do not bulk collect, mirror, resell, or train on NoviList catalog data without written permission.
  • 5 Do not expose user tokens or use access outside the scopes and purpose shown on the consent screen.
  • 6 Commercial use requires written approval until a formal commercial policy is published.
  • 7 Preserve source attribution and third-party rights associated with linked metadata and images.
  • 8 NoviList may limit, suspend, or revoke applications that threaten service availability, user privacy, security, or compliance, with notice when operationally reasonable.