Help us shape the future of Go! Take the 2026 Go Developer Survey

nexusmods

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 21 Imported by: 0

README

go-nexusmods

Go


A Go client for the Nexus Mods API: REST and GraphQL calls, the rate-limit budget, nxm:// links and the two sign-in flows. Package nexusmods, module github.com/Rethunk-Tech/go-nexusmods.

Nexus asks every app to say who it is and to stay inside its hourly and daily budget. This client does both by construction: it sends the name and version you give it on every request, reads the X-Rl-* headers, and refuses to send once a window is nearly spent. It never fetches a mod file: the free path needs an nxm:// link from Nexus's own Mod Manager Download button, and the premium paths are named Premium... so grep Premium finds each. The caller sets the User-Agent (an httpkit.Transport does it) and owns token storage; WithKey and WithBearer copies share one budget.

Quick start

go get github.com/Rethunk-Tech/go-nexusmods

A worked example and the contracts: HUMANS.md.

Features

  • REST and GraphQL reads: account, mod pages, files, changelogs, categories, scan status, tracked mods, collections.
  • Batched lookups of many mods or files, one request per 50.
  • nxm:// and mod-page link parsing that refuses anything but the exact forms.
  • A live rate-limit budget and typed errors (httpkit.StatusError, httpkit.RateLimitError).
  • Sign-in in package sso: legacy SSO and OAuth 2 PKCE with a loopback callback.

Documentation

Doc Audience
HUMANS.md Running and using the package
AGENTS.md Internals, invariants, and why
CONTRIBUTING.md Process — commits, tests, docs
SECURITY.md Vulnerability disclosure
CHANGELOG.md Release notes

License

MIT — see LICENSE. © 2026 Rethunk.Tech, LLC.

Documentation

Overview

Package nexusmods is a client for the Nexus Mods API: the v1 REST calls and v2 GraphQL queries a mod manager makes, the rate-limit budget Nexus reports in its X-Rl-* headers, nxm:// link parsing and mod-page URLs.

Every request carries the Application-Name and Application-Version the caller gave New, and none is sent once a window is at LimitFloor until it resets. No function returns a mod file's bytes: DownloadLinks redeems an nxm:// link from Nexus's Mod Manager Download button, PremiumDownloadLinks asks as a premium account, and fetching a mirror is the caller's code. The package sets no User-Agent and opens no browser; the sign-in flows live in the sso subpackage.

Index

Constants

View Source
const (
	// BaseURL is the public API.
	BaseURL = "https://api.nexusmods.com"
	// LimitFloor is the remaining-call count at or under which requests are refused until the window resets.
	LimitFloor = 5
)
View Source
const (
	ReasonForm     = "form"
	ReasonGame     = "game"
	ReasonExpired  = "expired"
	ReasonUser     = "user"
	ReasonSignedIn = "signedIn"
)

Why a link was refused. They are stable strings a UI can translate.

View Source
const MaxCollectionArchive = 64 << 20

MaxCollectionArchive caps the curator's collection archive PremiumCollectionArchive reads into memory.

Variables

View Source
var (
	// ErrUnauthorized means the API key was rejected.
	ErrUnauthorized = errors.New("nexus rejected the API key")
	// ErrPremiumRequired means a download link needs the key and expires from an nxm:// link, or a premium account.
	ErrPremiumRequired = errors.New("nexus requires a premium account or an nxm:// link for this download")
)

Functions

func ModURL

func ModURL(domain string, modID int) string

ModURL is a mod's page on Nexus.

func NXMDomain

func NXMDomain(raw string) (string, error)

NXMDomain is the nxm URL's game segment (the host), or a *RejectError when raw is not an nxm link.

func NewerFile

func NewerFile(a string, aID int64, b string, bID int64) bool

NewerFile reports whether file a (version a, file id aID) is newer than file b of the same mod: by version where both parse and differ, else the later upload, which has the higher file id. Nexus file ids grow with each upload, so the id breaks a tie when an author re-uploads a fix under the same version.

func ParseModURL

func ParseModURL(text string) (domain string, modID int, ok bool)

ParseModURL reads a Nexus mod page link, in the old (/<domain>/mods/<id>) or new (/games/<domain>/mods/<id>) form; tabs, queries and fragments are ignored.

func Requests

func Requests(n int) int

Requests is how many GraphQL requests ModsByDomain makes for n mods.

Types

type ArchiveName added in v0.2.0

type ArchiveName struct {
	// Name is the mod's name as written in the file name, where Nexus has turned spaces into underscores.
	Name  string
	ModID int
	// Version is the file's version with its dots written as '-', as in the file name ("2-8-2").
	Version string
	// Timestamp is the Unix time Nexus stamped on the download.
	Timestamp int64
	// Ext is the archive extension, lower case, without the dot.
	Ext string
}

ArchiveName is what a Nexus download's file name carries.

func ParseArchiveName added in v0.2.0

func ParseArchiveName(file string) (ArchiveName, bool)

ParseArchiveName reads the name Nexus gives a downloaded archive, "<name>-<mod id>-<version>-<timestamp>.<ext>" with ext zip, 7z or rar. A directory part is ignored. ok is false for any other name or a mod id of 0. A name that itself holds "-<digits>-" is read at its first such span, since the pattern cannot tell them apart.

type BatchFile

type BatchFile struct {
	FileID   int    `json:"fileId"`
	Name     string `json:"name"`
	Version  string `json:"version"`
	Category string `json:"category"`
	// ReplacedBy is the author's file_updates successor. The batched query does not carry it, so it comes from a
	// mod's cached full file list when there is one.
	ReplacedBy int `json:"replacedBy,omitempty"`
}

BatchFile is one file of a mod as Nexus's v2 API lists it.

type Changelog

type Changelog struct {
	Version string   `json:"version"`
	Notes   []string `json:"notes"`
}

Changelog is one version's notes, as plain text.

type Client

type Client struct {
	// HTTP sends every request; nil means http.DefaultClient. Its redirect policy is chained with
	// httpkit.SameOrigin, so the credential never leaves the API origin. Give it an httpkit.Transport to set the
	// User-Agent and to observe every round trip through OnResult.
	HTTP *http.Client
	// BaseURL is the API origin; empty means BaseURL.
	BaseURL string
	// Now is the clock; nil means time.Now in UTC.
	Now func() time.Time
	// contains filtered or unexported fields
}

Client talks to the API with one user's key or token. Copies made by WithKey and WithBearer share the rate-limit state and the caches.

func New

func New(name, version string) *Client

New returns a client that sends Application-Name: name and Application-Version: version on every request, as Nexus asks of every app. It has no credential until WithKey or WithBearer.

func (*Client) Abstain

func (c *Client) Abstain(ctx context.Context, t Title, modID int, version string) (EndorseStatus, error)

Abstain withdraws an endorsement of modID at version for the signed-in user.

func (*Client) Categories

func (c *Client) Categories(ctx context.Context, t Title) (map[int]string, error)

Categories maps the game's category IDs to their names.

func (*Client) Changelogs

func (c *Client) Changelogs(ctx context.Context, t Title, modID, limit int) ([]Changelog, error)

Changelogs returns a mod's limit newest changelog versions. The versions are object keys, which a map would lose along with duplicates, so the object is read token by token; a mod with none answers [].

func (*Client) Collection

func (c *Client) Collection(ctx context.Context, domain, slug string, revision int) (Collection, error)

Collection reads a collection revision, the latest when revision is 0. A collection Nexus does not know wraps httpkit.ErrNotFound.

func (c *Client) DownloadLinks(ctx context.Context, t Title, l NXMLink) ([]Link, error)

DownloadLinks asks for a file's download mirrors in Nexus's order, redeeming the key and expiry of an nxm:// link that ParseNXM accepted. A link with no key or no expiry is refused before any request, so a hand-built literal cannot fall through to the keyless form.

func (*Client) Endorse

func (c *Client) Endorse(ctx context.Context, t Title, modID int, version string) (EndorseStatus, error)

Endorse records an endorsement of modID at version for the signed-in user.

func (*Client) Files

func (c *Client) Files(ctx context.Context, t Title, modID int) ([]File, error)

Files lists every file of a mod.

func (*Client) FilesOf

func (c *Client) FilesOf(ctx context.Context, t Title, modIDs []int) (map[int][]BatchFile, error)

FilesOf lists the files of many mods with one GraphQL request per 50 mods (aliased modFiles queries), instead of one request per mod.

func (*Client) ForgetTracked

func (c *Client) ForgetTracked()

ForgetTracked drops the remembered tracked-mods list, which belongs to the account that fetched it.

func (*Client) Limits

func (c *Client) Limits() Limits

Limits returns the budget from the latest response.

func (*Client) ModsByDomain

func (c *Client) ModsByDomain(ctx context.Context, domain string, modIDs []int) (map[int]ModInfo, error)

ModsByDomain looks up many of one game's mods with one GraphQL request per 50 ids, instead of one request per mod. A mod Nexus does not return is absent from the map. A refused or failed request stops further requests and the lookup returns the mods found and the error; a rate limit is returned as such, never retried.

func (*Client) Page

func (c *Client) Page(ctx context.Context, t Title, modID int) (Page, error)

Page fetches a mod page.

func (*Client) PremiumCollectionArchive

func (c *Client) PremiumCollectionArchive(ctx context.Context, downloadLink string) ([]byte, error)

PremiumCollectionArchive downloads a collection revision's curator archive (collection.json plus bundled files). downloadLink is Collection's DownloadLink, the API path Nexus reports for the revision. It asks with no key, which Nexus allows only premium accounts, and a free account gets ErrPremiumRequired. The archive is the curator's manifest, not a mod file, and it is the one body this package downloads from outside Nexus's API.

func (c *Client) PremiumDownloadLinks(ctx context.Context, t Title, modID, fileID int) ([]Link, error)

PremiumDownloadLinks asks for a file's download mirrors with no key, which Nexus allows only premium accounts; a free account gets ErrPremiumRequired.

func (*Client) ScanStatuses

func (c *Client) ScanStatuses(ctx context.Context, t Title, modID int) (map[int]string, error)

ScanStatuses returns Nexus's v2 virus-scan status for each file. Results are cached for this client.

func (*Client) SetLimitsHook

func (c *Client) SetLimitsHook(fn func(Limits))

SetLimitsHook sets fn, called after a response updates the rate-limit budget and with no lock held, so it may call Limits. Set it before sharing the client between goroutines.

func (*Client) Track

func (c *Client) Track(ctx context.Context, t Title, modID int) error

Track starts tracking modID for the signed-in user.

func (*Client) TrackedMods

func (c *Client) TrackedMods(ctx context.Context) ([]TrackedMod, error)

TrackedMods lists mods the signed-in user tracks. The list is reused for trackedCacheTTL.

func (*Client) Untrack

func (c *Client) Untrack(ctx context.Context, t Title, modID int) error

Untrack stops tracking modID for the signed-in user.

func (*Client) Validate

func (c *Client) Validate(ctx context.Context) (User, error)

Validate identifies the account behind the client's key.

func (*Client) WithBearer

func (c *Client) WithBearer(token TokenSource) *Client

WithBearer returns a client that sends an OAuth access token, which Nexus accepts in the Authorization header on both the REST and GraphQL APIs.

func (*Client) WithKey

func (c *Client) WithKey(key string) *Client

WithKey returns a client that authenticates with a personal API key.

type Collection

type Collection struct {
	Name     string
	Author   string
	Slug     string
	Revision int
	Files    []CollectionFile
	// Instructions is the curator's installationInfo text, shown before importing.
	Instructions string
	// External lists what the curator points to outside Nexus; each URL is an http or https address or empty.
	External []CollectionExternal
	// DownloadLink is the Nexus API path that resolves to the curator's collection archive.
	DownloadLink string
}

Collection is one revision of a Nexus collection. It has no JSON tags: the Go field names are its stored form.

type CollectionExternal

type CollectionExternal struct {
	Name     string
	Type     string
	URL      string
	Version  string
	Author   string
	Optional bool
}

CollectionExternal is one off-Nexus resource of a collection. Type is Nexus's resourceType ("direct", "browse" or "manual").

type CollectionFile

type CollectionFile struct {
	ModID    int
	FileID   int
	Optional bool
}

CollectionFile is one mod file a collection installs.

type EndorseStatus

type EndorseStatus string

EndorseStatus is the signed-in user's endorsement of one mod.

const (
	EndorseUndecided EndorseStatus = "Undecided"
	EndorseEndorsed  EndorseStatus = "Endorsed"
	EndorseAbstained EndorseStatus = "Abstained"
)

type File

type File struct {
	FileID      int       `json:"fileId"`
	FileName    string    `json:"fileName"`
	Name        string    `json:"name"`
	Description string    `json:"description"`
	Version     string    `json:"version"`
	ModVersion  string    `json:"modVersion"`
	Category    string    `json:"category"`
	SizeKB      int64     `json:"sizeKb"`
	IsPrimary   bool      `json:"isPrimary"`
	Uploaded    time.Time `json:"uploaded"`
	MD5         string    `json:"md5,omitempty"`
	// ReplacedBy is the file the author uploaded as this one's update (Nexus file_updates), or 0.
	ReplacedBy int `json:"replacedBy"`
}

File is one downloadable file of a mod; Category is empty when Nexus reports null. Name is its title on the page and Description its BBCode.

type GameOf

type GameOf func(domain string) (gameID string, ok bool)

GameOf maps an nxm link's Nexus domain to the caller's game id; ok is false for a game the caller does not serve.

type Limits

type Limits struct {
	Known  bool   `json:"known"`
	Daily  Window `json:"daily"`
	Hourly Window `json:"hourly"`
}

Limits is the daily and hourly budget; Known is false until a response has carried the headers.

type Link struct {
	Name      string `json:"name"`
	ShortName string `json:"shortName"`
	URI       string `json:"uri"`
}

Link is one download mirror.

type MessageError

type MessageError struct {
	Code    int
	Status  string
	Message string
}

MessageError is a non-200 API answer that carried a JSON message (endorse-without-download uses this).

func (*MessageError) Error

func (e *MessageError) Error() string

type ModInfo

type ModInfo struct {
	ModID        int
	Name         string
	Version      string
	Status       string
	Summary      string
	Author       string
	PictureURL   string
	Category     string
	Endorsements int
	Downloads    int
	Adult        bool
	// Endorsed is whether the signed-in user endorsed the mod.
	Endorsed bool
	Created  time.Time
	Updated  time.Time
	// Requirements are what the mod page lists under "Nexus requirements".
	Requirements []Requirement
}

ModInfo is one mod page as the batched lookup reports it.

func (ModInfo) Page

func (m ModInfo) Page() Page

Page is the mod page data the batched lookup carries; what only a page request gives (the description, the uploader's link, the unique download count) stays empty.

type NXMLink struct {
	// Game is the caller's id for the game the link's Nexus domain belongs to.
	Game    string `json:"game"`
	ModID   int    `json:"modId"`
	FileID  int    `json:"fileId"`
	Key     string `json:"key"`
	Expires int64  `json:"expires"`
	UserID  int    `json:"userId"`
}

NXMLink is an accepted nxm:// download link, the value Nexus's Mod Manager Download button produces.

func ParseNXM

func ParseNXM(raw string, userID int, now time.Time, gameOf GameOf) (NXMLink, error)

ParseNXM accepts nxm://<domain>/mods/<mod id>/files/<file id>?key=&expires=&user_id= only for a domain gameOf maps to a game: numeric ids, a key, not yet expired at now, and user_id equal to userID, the signed-in account. Anything else is a *RejectError.

type Page

type Page struct {
	ModID           int       `json:"modId"`
	Name            string    `json:"name"`
	Summary         string    `json:"summary"`
	Description     string    `json:"description"`
	PictureURL      string    `json:"pictureUrl"`
	Version         string    `json:"version"`
	Author          string    `json:"author"`
	UploadedBy      string    `json:"uploadedBy"`
	UploaderURL     string    `json:"uploaderUrl"`
	CategoryID      int       `json:"categoryId"`
	Endorsements    int       `json:"endorsements"`
	Downloads       int       `json:"downloads"`
	UniqueDownloads int       `json:"uniqueDownloads"`
	Created         time.Time `json:"created"`
	Updated         time.Time `json:"updated"`
	Adult           bool      `json:"adult"`
	Status          string    `json:"status"`
	Available       bool      `json:"available"`
	// Endorsement is the signed-in user's endorse_status (Endorsed, Abstained, Undecided); empty when Nexus omitted it.
	Endorsement string `json:"endorsement"`
	// Requirements come from the batched GraphQL lookup; the REST page has none.
	Requirements []Requirement `json:"requirements"`
}

Page is everything a mod page reports. Description is Nexus's BBCode with <br /> line breaks, unrendered. Status is published, under_moderation, not_published, publish_with_game, removed, wastebinned or hidden; an unavailable page comes without its text.

type RejectError

type RejectError struct{ Reason string }

RejectError says why a link was refused.

func (*RejectError) Error

func (e *RejectError) Error() string

type Requirement

type Requirement struct {
	ModID    int    `json:"modId"`
	Name     string `json:"name"`
	URL      string `json:"url"`
	External bool   `json:"external"`
	Notes    string `json:"notes"`
}

Requirement is one mod or outside download a mod page says is needed. A Nexus mod of the same game has ModID and its page as URL; anything else (another game's mod, an outside site) is External, with whatever URL Nexus gave.

type Title

type Title struct {
	Domain string
	ID     int
}

Title names a game to Nexus: its domain in v1 URLs and its numeric id in the v2 API.

type TokenSource

type TokenSource func(ctx context.Context) (string, error)

TokenSource returns the current OAuth access token, refreshing it when it is about to expire.

type TrackedMod

type TrackedMod struct {
	ModID      int    `json:"modId"`
	DomainName string `json:"domainName"`
}

TrackedMod is one entry from GET /v1/user/tracked_mods.json.

type User

type User struct {
	ID        int    `json:"userId"`
	Name      string `json:"name"`
	IsPremium bool   `json:"isPremium"`
}

User is the account a key belongs to.

type Window

type Window struct {
	Remaining int       `json:"remaining"`
	Limit     int       `json:"limit"`
	Reset     time.Time `json:"reset"`
}

Window is one rate-limit window as last reported by the API.

Directories

Path Synopsis
Package sso signs in to Nexus Mods without a pasted key.
Package sso signs in to Nexus Mods without a pasted key.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL