Skip to content
墨 AniLink

AniList authentication ​

Constructor token ​

Pass the token as the first argument or in the anilist credential slot:

typescript
import { AniLink } from "anilink-api-wrapper";

const aniLink = new AniLink("anilist-token");
// or
const aniLink2 = new AniLink({ anilist: { authToken: "anilist-token" } });

Multiple instances can hold different tokens, and each exposes its own anilist API. Create a separate instance for each user or bot that needs a different token.

Public versus authenticated operations ​

Read-only operations (public media, character, staff queries) work without any token:

typescript
const aniLink = new AniLink();
const anime = await aniLink.anilist.query.media({ id: 1, type: "ANIME" });

Mutations and viewer-scoped queries (viewer, notification, list mutations) require a token. Calling them without one throws AniLinkAuthError.

OAuth2 authorization-code flow ​

AniLink provides helpers for the OAuth2 authorization-code flow. First, register an application on the AniList developer settings page to get a client ID and secret.

1. Send the user to the authorization URL ​

typescript
import { buildAuthorizationUrl } from "anilink-api-wrapper";

const state = crypto.randomUUID(); // a fresh random value per login attempt
const authorizeUrl = buildAuthorizationUrl("your-client-id", "https://example.com/callback", state);
// Redirect the user to `authorizeUrl`.

The third state parameter is optional, but use it to help prevent CSRF attacks. Bind it to the user's session and validate that the returned state matches before exchanging the code.

2. Exchange the code for a token ​

typescript
import { getAccessToken, AniLink } from "anilink-api-wrapper";

const { access_token, refresh_token } = await getAccessToken(
    "your-client-id",
    "your-client-secret",
    code, // the `code` query parameter from the redirect
    "https://example.com/callback"
);

const aniLink = new AniLink(access_token);

3. Refresh when the token expires ​

The refresh response may not include a new refresh_token. When it does not, keep using the one you stored:

typescript
import { refreshAccessToken } from "anilink-api-wrapper";

const { access_token, refresh_token: rotated } = await refreshAccessToken(
    "your-client-id",
    "your-client-secret",
    refresh_token
);

const nextRefreshToken = rotated ?? refresh_token;

4. Refresh proactively ​

AniList reports the token lifetime as expires_in seconds. Use getTokenExpiry to refresh before the token expires instead of waiting for a 401:

typescript
import { getTokenExpiry, refreshAccessToken } from "anilink-api-wrapper";

if (Date.now() >= getTokenExpiry(tokenResponse).getTime() - 60_000) {
    tokenResponse = await refreshAccessToken(
        "your-client-id",
        "your-client-secret",
        nextRefreshToken
    );
}

5. Automatic refresh ​

Steps 1 through 4 leave token refresh to your code. Set refreshToken, clientId, and clientSecret to let the client refresh automatically. When any AniList request fails with a 401 (an HTTP-level 401, or a GraphQL envelope whose errors entry carries status: 401), the client automatically exchanges the stored refresh token for a fresh access token. It updates its credentials and replays the original request once.

typescript
const aniLink = new AniLink({
    anilist: {
        authToken: token.access_token,
        refreshToken: token.refresh_token,
        clientId: "your-client-id",
        clientSecret: "your-client-secret",
        onTokenRefresh: (response) => saveToken(response),
    },
});
  • Opt-in. The client attempts a refresh only when you set all three fields (refreshToken, clientId, and clientSecret). Otherwise, the call fails with a 401. AniList requires the client secret for its refresh grant, but MAL treats it as optional.
  • Bootstrappable. A client with refreshToken, clientId, and clientSecret, but no authToken, refreshes on its first auth-required call. You can construct one from a stored refresh token without an access token.
  • One replay, no loop. A replay that fails again rejects with that error. There is no retry loop. Concurrent 401s share one refresh grant.
  • Failure event. onTokenRefreshError fires exactly once per failed grant with the sanitized token-request error. The failing call rejects with the same error. If the callback throws, the client reports the callback error through onHookError. The callback error does not replace the token-request error.
  • Per-call, not per-traversal. Refresh applies per wrapped operation call. Pages already in flight under paginate with concurrency > 1 fail independently if they dispatched with the expired token. Only the failing call itself triggers the grant and replay.

Persist synchronously in onTokenRefresh. The client starts using the new token before your callback returns. If the process exits or the callback throws between the refresh and your persistence write, the in-memory client works but your stored credentials are stale. When AniList rotates the refresh token, the stored one becomes permanently invalid. Automatic refresh cannot recover after a restart. Re-authorize manually to resume refresh. The callback receives the effective token response. When AniList omits refresh_token, the stored one stays valid and the response carries it.

Constants and types ​

ANILIST_AUTHORIZE_URL and ANILIST_TOKEN_URL expose the OAuth endpoints. AniListTokenResponse types the token payload (access_token, token_type, expires_in, refresh_token), handy for your own storage layer.

Next steps ​

AniLink, typed AniList & MyAnimeList client for TypeScript.