mullvad-api
v1.0.0
Published
TypeScript client library for the Mullvad VPN API
Downloads
91
Readme
mullvad-api
TypeScript/Node.js client library for the Mullvad VPN API. Provides account management, device management, relay list fetching, WireGuard key generation, and config file generation.
Built by reverse-engineering the Mullvad VPN app source code.
Requirements
- Node.js >= 18 or Bun
- No external runtime dependencies — uses native
fetchandnode:crypto(X25519)
Install
npm install mullvad-apiQuick Start
import { MullvadClient } from "mullvad-api";
const client = new MullvadClient();
client.setAccount("1234567890123456");
// Register a new device with an auto-generated WireGuard keypair
const { keyPair, device } = await client.registerDevice();
// Generate a full WireGuard config targeting a Swedish server
const { configString, relay } = await client.generateFullConfig({
privateKey: keyPair.privateKey,
device,
country: "se",
});
console.log(`Connecting to ${relay.hostname}`);
console.log(configString);
// [Interface]
// PrivateKey = ...
// Address = 10.x.x.x/32, fc00::.../128
// DNS = 10.64.0.1
//
// [Peer]
// PublicKey = ...
// AllowedIPs = 0.0.0.0/0, ::/0
// Endpoint = 185.x.x.x:51820API Reference
MullvadClient
The main class. All methods that hit the API are async.
Constructor
new MullvadClient(options?: MullvadApiOptions)| Option | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | https://api.mullvad.net | API base URL |
| userAgent | string | mullvad-api-ts/1.0.0 | User-Agent header |
| timeout | number | 10000 | Request timeout in ms |
Authentication
setAccount(accountNumber: string): void
Set the Mullvad account number for authenticated requests. Clears any cached access token if the account changes.
getAccessToken(): Promise<string>
Obtain a bearer token for the configured account. Tokens are cached and reused until expiry. Automatically refreshes expired tokens.
clearAccessToken(): void
Force-clear the cached token, so the next authenticated request fetches a new one.
Account Management
createAccount(): Promise<string>
Create a new Mullvad account. Returns the account number.
const accountNumber = await client.createAccount();getAccountData(): Promise<AccountData>
Get account info (ID and subscription expiry).
const { id, expiry } = await client.getAccountData();
console.log(`Account expires: ${expiry}`);deleteAccount(): Promise<void>
Permanently delete the authenticated account.
submitVoucher(voucherCode: string): Promise<VoucherSubmission>
Redeem a voucher code. Returns the time added (in seconds) and the new expiry date.
const { time_added, new_expiry } = await client.submitVoucher("XXXX-XXXX-XXXX-XXXX");getWwwAuthToken(): Promise<string>
Get a one-time token for authenticating on the Mullvad website.
Device Management
Mullvad uses "devices" to represent WireGuard peers. Each device has a public key and is assigned tunnel IP addresses.
listDevices(): Promise<Device[]>
List all devices on the account.
getDevice(deviceId: string): Promise<Device>
Get a specific device by its ID.
createDevice(pubkey: string, hijackDns?: boolean): Promise<Device>
Register a new device with a WireGuard public key (base64). Returns the device with its assigned IPv4/IPv6 tunnel addresses.
const device = await client.createDevice(keyPair.publicKey);
console.log(device.ipv4_address); // "10.x.x.x/32"removeDevice(deviceId: string): Promise<void>
Remove a device from the account.
rotateDeviceKey(deviceId: string, newPubkey: string): Promise<Device>
Replace the WireGuard public key for an existing device.
registerDevice(hijackDns?: boolean): Promise<{ keyPair: WireGuardKeyPair; device: Device }>
Convenience method: generates a new X25519 keypair and registers it as a device in one call.
rotateDeviceKeyPair(deviceId: string): Promise<{ keyPair: WireGuardKeyPair; device: Device }>
Convenience method: generates a new keypair and rotates the key for an existing device.
Relay List
getRelayList(): Promise<RelayList>
Fetch the full server/relay list. Uses HTTP ETag caching — subsequent calls return the cached list if the server responds with 304 Not Modified.
const relays = await client.getRelayList();
console.log(`${relays.wireguard.relays.length} WireGuard relays`);
console.log(`${relays.openvpn.relays.length} OpenVPN relays`);
console.log(`${relays.bridge.relays.length} bridge relays`);pickRelay(relayList: RelayList, filter?: { country?: string; city?: string; hostname?: string }): WireguardRelay
Pick a WireGuard relay using weighted random selection. Filters by country code, city code, or exact hostname.
// Pick any relay in Sweden
const relay = client.pickRelay(relays, { country: "se" });
// Pick a relay in Gothenburg
const relay = client.pickRelay(relays, { city: "se-got" });
// Pick a specific server
const relay = client.pickRelay(relays, { hostname: "se-got-wg-001" });WireGuard Configuration
generateKeyPair(): WireGuardKeyPair
Generate a new WireGuard X25519 keypair. Returns { privateKey, publicKey } as base64 strings.
const { privateKey, publicKey } = client.generateKeyPair();publicKeyFromPrivate(privateKeyBase64: string): string
Derive the public key from a base64-encoded private key.
buildWireGuardConfig(options): WireGuardConfig
Build a structured WireGuard configuration object.
const config = client.buildWireGuardConfig({
privateKey: keyPair.privateKey,
device,
relay,
port: 51820, // optional, random from port_ranges if omitted
dns: ["10.64.0.1"], // optional, defaults to Mullvad DNS
useIpv6: false, // optional, use IPv6 endpoint
});toWireGuardConfigString(config: WireGuardConfig): string
Render a config object as a wg-quick compatible .conf string.
generateFullConfig(options): Promise<{ config, configString, relay }>
All-in-one: fetches relays, picks a server, and builds a complete WireGuard configuration.
const { configString, relay } = await client.generateFullConfig({
privateKey: keyPair.privateKey,
device,
country: "de", // optional filter
city: "de-ber", // optional filter
hostname: undefined, // optional exact match
port: 51820, // optional
dns: ["10.64.0.1"], // optional
useIpv6: false, // optional
});Standalone Functions
These are also exported for use without instantiating MullvadClient:
import {
generateKeyPair,
publicKeyFromPrivate,
buildWireGuardConfig,
toWireGuardConfigString,
} from "mullvad-api";Error Handling
All API errors throw MullvadApiError with structured error information:
import { MullvadApiError, MullvadTimeoutError } from "mullvad-api";
try {
await client.getAccountData();
} catch (err) {
if (err instanceof MullvadApiError) {
console.log(err.statusCode); // 401
console.log(err.code); // "INVALID_ACCOUNT"
console.log(err.detail); // Human-readable message
if (err.is("MAX_DEVICES_REACHED")) {
// Handle specific error
}
}
if (err instanceof MullvadTimeoutError) {
// Request timed out
}
}Known API error codes: INVALID_ACCOUNT, INVALID_ACCESS_TOKEN, VOUCHER_USED, INVALID_VOUCHER, DEVICE_NOT_FOUND, MAX_DEVICES_REACHED, PUBKEY_IN_USE
Types
All types are exported for use in your own code:
import type {
AccountData,
Device,
RelayList,
RelayLocation,
WireguardRelay,
WireguardEndpointData,
WireGuardConfig,
WireGuardKeyPair,
BridgeRelay,
ShadowsocksEndpoint,
OpenVpnRelay,
OpenVpnPort,
} from "mullvad-api";API Endpoints
This library covers the following Mullvad API endpoints:
| Method | Endpoint | Library Method |
|---|---|---|
| POST | /auth/v1/token | getAccessToken() |
| POST | /accounts/v1/accounts | createAccount() |
| GET | /accounts/v1/accounts/me | getAccountData() |
| DELETE | /accounts/v1/accounts/me | deleteAccount() |
| GET | /accounts/v1/devices | listDevices() |
| POST | /accounts/v1/devices | createDevice() |
| GET | /accounts/v1/devices/{id} | getDevice() |
| DELETE | /accounts/v1/devices/{id} | removeDevice() |
| PUT | /accounts/v1/devices/{id}/pubkey | rotateDeviceKey() |
| GET | /app/v1/relays | getRelayList() |
| POST | /app/v1/submit-voucher | submitVoucher() |
| POST | /app/v1/www-auth-token | getWwwAuthToken() |
License
MIT
