Skip to content

Master server API ​

The WFX.re master server is master-server/server.js — Node.js, no dependencies. It serves the server list, network statistics, WFX.re accounts, the launcher linking flow, the Portal, published server images and the wfx.re landing page. The project's own (French) reference is docs/MASTER_API.md.

SettingDefaultMeaning
PORT7777Listening port
PUBLIC_URLthe request's HostBase for public URLs: iconUrl, bannerUrl. https: also turns on Secure cookies.
LINK_URLPUBLIC_URLBase of the account-linking page given to the launcher (<LINK_URL>/link?code=…). On WFX.re: https://auth.wfx.re, while PUBLIC_URL is https://servers.wfx.re
DEMOonDEMO=0 disables the four demo servers shown in development
CLIENT_VERSION0.1.0Returned by /version
DOCS_DIST../docs-site/docs/.vitepress/distFolder served under /docs/

Data lives in master-server/data/: accounts.json (users with scrypt + salt; sessions keyed by sha256(token), never the token itself), portal.json (registered servers; sha256(sv_key) plus its last 4 characters), media/<serverId>/icon.png and banner.(png|jpg).

All JSON responses carry Cache-Control: no-store. Error bodies are { "error": "English message" } (heartbeat errors: { "ok": false, "error": … }).

Public list (no authentication, CORS *) ​

GET /servers ​

Servers with a valid heartbeat in the last 90 s (plus demo servers when DEMO is on).

json
[{
  "id": "192.168.2.3:30120", "host": "192.168.2.3", "port": 30120,
  "name": "WildM LAN", "desc": "…", "tags": ["roleplay", "dev"], "locale": "FR",
  "players": 0, "maxPlayers": 32, "peak": 0,
  "serverId": "017992545b6d", "owner": "wildm-admin",
  "iconUrl": "http://127.0.0.1:7777/media/servers/017992545b6d/icon.png?v=1794651208650",
  "bannerUrl": null,
  "verified": true
}]
FieldSourceNotes
idheartbeatAlways host:port; stable key for launcher favourites and history
host, portheartbeatport is the game server's HTTP port
name, desc, tags, localePortal
maxPlayersPortal (else heartbeat)
players, peakheartbeatpeak = 24-hour rolling peak for this server
serverId, ownerPortalnull for demo servers
iconUrl, bannerUrlPortalAbsolute URLs or null; ?v= changes on each upload
verified—true = registered on the Portal

GET /stats ​

json
{ "players": 67, "peak24": 117, "servers": 4 }

Total players now, network-wide 24-hour peak (sampled every 30 s), number of listed servers.

GET /version ​

json
{ "client": "0.1.0" }

The launcher compares this with its own version at boot and warns if a newer one is published.

GET /media/servers/<serverId>/icon.png · …/banner.png · …/banner.jpg ​

Public images. Access-Control-Allow-Origin: *, Content-Security-Policy: default-src 'none', cached 5 minutes. 404 if absent.

Heartbeat (game server → master) ​

POST /heartbeat ​

JSON body, max 4 KB, sent every 30 s by the game server.

json
{ "key": "wfx_<40 hex>", "host": "192.168.2.3", "port": 30120, "players": 0, "maxPlayers": 32 }
AnswerWhen
200 {"ok":true,"id":"host:port","serverId":"…"}Valid key — the server is listed
403 {"ok":false,"error":"Missing sv_key — create one on the WFX.re Portal"}No key
403 {"ok":false,"error":"Invalid sv_key — create one on the WFX.re Portal"}Unknown key (format ^wfx_[0-9a-f]{40}$)
429More than 30 invalid keys in 5 minutes from the same IP
400Invalid JSON, or port not an integer 1–65535

host missing → the source IP is used. players is clamped to 0–4096. Other fields (name, desc…) are ignored: the Portal is the source of truth. One key = one list entry (the latest heartbeat wins).

Accounts (launcher, Bearer token) ​

RouteBody / headerResponse
POST /auth/register{username, password}{token, username}
POST /auth/login{username, password}{token, username}
GET /auth/meAuthorization: Bearer <token>{username} or 401 {"error":"Invalid session"}
POST /auth/logoutAuthorization: Bearer <token>{ok:true}
POST /auth/ticketAuthorization: Bearer <token>, {serverId} (12 hex, from GET /servers){ticket: "wft_…", expires} · 401 · 404 unknown server · 429 (60 / 5 min / IP)
POST /auth/ticket/verify{key: <sv_key>, ticket} (game server){ok:true, username, license} · 403 bad key · 404 unknown, expired or other server's ticket

A ticket lets a game server know who a player is without ever seeing the account token. It is valid 2 hours, reusable for reconnects, and only on the server it was issued for (checked through that server's sv_key). license is a 40-hex HMAC of the account, stable across tickets and servers, that does not reveal the username. The launcher puts the ticket in wildm_connect.json → token; the client sends it in hello.

Username: 3–20 characters [A-Za-z0-9_.-], unique case-insensitively. Password: 8–200 characters. Rate limit: 10 attempts per 5 minutes per IP (429 beyond).

Launcher linking through the browser ​

text
launcher                      master                         browser
   | POST /auth/link/start  ->  |                                |
   | <- {code, poll, url, expiresIn}                             |
   | opens url  ------------------------------------------------>| GET /link?code=XXXX-XXXX
   |                            | <- POST /auth/link/approve ----| {code, mode, username, password}
   | GET /auth/link/poll?code&poll (every 2 s)                   |
   | <- {status:"approved", token, username}                     |
RouteDetails
POST /auth/link/start{code, poll, url, expiresIn}. code is 8 characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789 shown as XXXX-XXXX; poll is a 48-hex secret only the launcher knows; expiresIn = 600 s.
GET /link?code=…The HTML sign-in page (CSP, X-Frame-Options: DENY). Without ?code= it points to the Portal.
POST /auth/link/approve{code, mode: "login" or "register", username, password} → {username}. Errors: Code expired…, This code has already been used, validation errors. Rate-limited like login.
GET /auth/link/poll?code=…&poll=…{status:"pending"}, {status:"approved", token, username} (delivered once, then the code is deleted), {status:"expired"} or {status:"invalid"} (wrong poll).

The Portal web app (GET /portal) uses a cookie session: wfx_session, HttpOnly; SameSite=Lax; Path=/; Max-Age 30 days (plus Secure when PUBLIC_URL is https). Sessions expire after 30 days.

CSRF protection: every non-GET request must carry an Origin equal to the host (http(s)://<Host> or the PUBLIC_URL origin) and, if present, Sec-Fetch-Site: same-origin — otherwise 403. JSON bodies require Content-Type: application/json (415), image uploads image/png or image/jpeg.

RouteBodyResponse
GET /portal/api/me—{username} or 401
POST /portal/api/register · /login{username, password}{username} + Set-Cookie (the token is never in the body)
POST /portal/api/logout—{ok:true}, cookie cleared
GET /portal/api/servers—The account's servers (owner view), oldest first
POST /portal/api/servers{name, desc?, tags?, locale?, maxPlayers?}201 {server, key} — the only time the key is returned
GET /portal/api/servers/<id>—Owner view
PATCH /portal/api/servers/<id>any of {name, desc, tags, locale, maxPlayers} (≤ 4 KB)Owner view
DELETE /portal/api/servers/<id>—{ok:true}; registration, key and images removed, delisted
POST /portal/api/servers/<id>/key—{server, key}; the old key stops working at once and the server leaves the list until a valid heartbeat
PUT /portal/api/servers/<id>/iconraw PNG, ≤ 256 KB{server, warning?} (warning if not 96×96)
PUT /portal/api/servers/<id>/bannerraw PNG or JPEG, ≤ 1 MB{server}
DELETE /portal/api/servers/<id>/icon · /banner—{server}

Owner view: {id, name, desc, tags, locale, maxPlayers, keyHint, iconUrl, bannerUrl, created, updated, online, live}, where live is {host, port, players, maxPlayers, seen} or null. The key hash is never returned.

Validation: name 3–64 characters, description ≤ 140, up to 5 tags of 16 characters [a-z0-9_-], locale XX or XX-YY, maxPlayers 1–1024, 10 servers per account. A server owned by another account answers 404. Image types are checked against magic bytes (PNG signature + IHDR, JPEG FF D8 FF) and must match the Content-Type; dimensions up to 4096×4096; size is checked on Content-Length and while reading (413).

Error codes: 400 validation, 401 not signed in, 403 origin refused, 404 unknown or not yours, 413 too large, 415 wrong type, 429 too many attempts.

Website routes ​

RouteContent
GET /wfx.re landing page (live stats and server list)
GET /docs/…This documentation: the static VitePress build from docs-site/docs/.vitepress/dist (override with DOCS_DIST), with its own CSP; /docs redirects to /docs/, extension-less links resolve to .html, unknown pages get the site's 404.html
GET /portalPortal web app
GET /linkLauncher linking page
GET /assets/<file>Site assets
GET /download/<file>Downloads from public/downloads/ (the landing page links WildM-Setup.exe; not published yet)
GET /wfx-logo.svg, /wfx-mark.svgLogos

Unknown routes answer 404 {"error":"not found"}.

WildM is an independent community project. It is not affiliated with or endorsed by Ubisoft. Ghost Recon Wildlands is a trademark of Ubisoft Entertainment.