npm module: @flitt/api
The official client of the Flitt Public API for TypeScript and JavaScript. It calls the API for you and handles errors, retries and real time, so your code only contains your own logic.
What the module does
@flitt/api wraps the Public API: instead of writing HTTP requests, you call typed methods (flitt.servers.list(), flitt.votes.claim()…). The module handles authentication, timeouts, retries, pagination and errors.
- Zero dependencies, ESM and CommonJS, full TypeScript types.
- Runs on Node.js 20+, Deno, Bun and Cloudflare Workers (any runtime with
fetch). - Safe by default: HTTPS only, key never printed, redirects never followed, refuses to run in a browser.
- Resilient: per-request timeout, retries with exponential backoff,
Retry-Afterhonoured, idempotent vote claims. - Real time: votes, players, chat and status pushed over WebSocket, with automatic reconnection and replay.
Installation
npm install @flitt/api
# pnpm add @flitt/api · yarn add @flitt/api · bun add @flitt/apiFirst call, in ESM or TypeScript:
import { Flitt } from "@flitt/api";
const flitt = new Flitt({ apiKey: process.env.FLITT_API_KEY });
const { data: servers } = await flitt.servers.list();
for (const server of servers) {
console.log(`${server.name}: ${server.playersOnline} players (${server.status})`);
}In CommonJS:
const { Flitt } = require("@flitt/api");Credentials
There are two credentials. Pass one or both to the constructor, or let the module read them from environment variables:
| Account key | Server token | |
|---|---|---|
| Option | apiKey (env FLITT_API_KEY) | serverToken (env FLITT_SERVER_TOKEN) |
| Format | flt_… | flv_… |
| Created in | Dashboard → Profile → API | Dashboard → your server → Votes → API |
| Opens | Every endpoint, on every server you own or are a member of (within your role) | Votes and reviews of one server |
| Quota | Monthly, per plan | 120 requests / minute |
| Use it | Your backend, your Discord bot, your tools | On a game server (Lua, Java, a small script) |
.env
FLITT_API_KEY=flt_...
FLITT_SERVER_TOKEN=flv_...On a game server
const flitt = new Flitt({ serverToken: process.env.FLITT_SERVER_TOKEN });
const claim = await flitt.votes.claim("srv_123", { player: "Alex_RP" });Quotas and limits
| Plan | Account key | Server token |
|---|---|---|
| Freemium | 1,000 requests / month | 120 requests / minute |
| Premium | 20,000 requests / month | 120 requests / minute |
| Enterprise | Unlimited | 120 requests / minute |
An account that owns a Server Premium server (or a partner server) gets the Premium quota. A real-time event stream counts as one request per connection; the events it receives are free.
The quota state after each response is available on flitt.rateLimit ({ limit, remaining, reset }, null = unlimited) or through the onRateLimit callback:
const flitt = new Flitt({
apiKey: process.env.FLITT_API_KEY,
onRateLimit: ({ limit, remaining, reset }) => console.log(remaining, "/", limit, "until", reset),
});
await flitt.servers.list();
console.log(flitt.rateLimit);Available methods
Every method returns a Promise and throws a typed error on failure. Each one takes a last per-request options argument.
Account
flitt.usage.get()Account quota: plan, monthly limit, used, remaining, reset date.
Servers
flitt.servers.list({ page?, limit? })Your servers (owner or member), paginated.
flitt.servers.listAll()Iterator over every page.
flitt.servers.get(serverId)One server, with hold (maintenance, development, closure); IP and port hidden for the VIEWER role.
Players
flitt.players.list(serverId, { status?, page?, limit? })Players: online, offline or all.
flitt.players.listAll(serverId, { status? })Iterator over every player.
flitt.players.get(serverId, playerId)One player, by Flitt id or game id (SteamID, license, UUID).
Firewall (anti-VPN)
flitt.firewall.get(serverId) / update(serverId, changes)Settings: enabled, VPN blocking, kick message (OWNER or ADMIN to change).
flitt.firewall.rules.list / create / delete / deleteManyALLOW / DENY rules by identifier, IP or country; up to 100 rules per call, partial success possible; deleteMany refuses an empty filter.
flitt.firewall.blocks(serverId, { limit? })Latest blocked connections.
await flitt.firewall.update("srv_123", { enabled: true, blockVpn: true, kickMessage: "VPN not allowed" });
const result = await flitt.firewall.rules.create("srv_123", [
{ action: "DENY", kind: "IDENTIFIER", value: "76561198000000000", note: "Cheater" },
{ action: "DENY", kind: "COUNTRY", value: "XX" },
]);
for (const e of result.errors) console.warn(e.index, e.code, e.message);In-game commands
flitt.commands.chat / notify(serverId, …)Message to the whole server or to one player (EDITOR role).
flitt.commands.kick / ban / unban(serverId, externalId, …)Sanctions (ADMIN role). Never retried automatically: a ban must not run twice.
flitt.commands.setGroup / setName(serverId, externalId, …)Group (ULX, SAM, CAMI, ace, LuckPerms…) or name.
flitt.commands.groups(serverId)Current groups of online players.
Commands are queued for the server's addon. Pass waitMs (15,000 at most) to wait for its answer:
await flitt.commands.chat("srv_123", "Restart in 5 minutes!");
const res = await flitt.commands.ban("srv_123", "76561198000000000", { reason: "RDM", durationSeconds: 86_400 }, { waitMs: 5000 });
console.log(res.result?.ok, res.result?.output);Discord linking
flitt.discord.createLinkCode(discordId)Code to type in game (!flitt CODE, or /flitt link CODE on Minecraft).
flitt.discord.findLinks / link / unlinkDiscord accounts linked to a player, manual link and unlink.
flitt.discord.getModule / setModule(serverId, enabled)Addon Discord module, for setups without the Flitt bot.
Votes and reviews
flitt.votes.check(serverId, identity)CLAIMABLE, CLAIMED or NOT_FOUND, with nextVoteInMinutes.
flitt.votes.claim(serverId, identity, { idempotencyKey? })Claims the oldest unclaimed vote (24 h), exactly once across every channel.
flitt.votes.list / listAll(serverId, { since?, limit? })Recent votes, cursor pagination (30 days).
flitt.votes.top(serverId, { period?, limit? })Top voters: current_month, last_month, all_time or YYYY-MM.
flitt.votes.summary / stats(serverId)Counters, ranking, rating; votes, views and clicks per day, week, month.
flitt.reviews.list / listAll(serverId, { sort?, limit? })Published reviews.
Real time
flitt.events.connect(options?)Real-time event stream (WebSocket).
Pagination and iterators
list() methods return one page ({ data, pagination }, or a cursor for votes). listAll() methods walk every page for you, with for await:
const page = await flitt.servers.list({ page: 1, limit: 25 });
console.log(page.data, page.pagination);
for await (const player of flitt.players.listAll("srv_123", { status: "all" })) {
console.log(player.name, player.totalPlaySeconds);
}Rewarding votes
To reward a voter from your own code (for example without the Flitt addon): check, claim the vote exactly once, then give the reward. Works with the account key (role with the votes permissions) or the server token.
const flitt = new Flitt({ serverToken: process.env.FLITT_SERVER_TOKEN });
const claim = await flitt.votes.claim("srv_123", { player: "Alex_RP" });
if (claim.status === "CLAIMED_NOW") {
giveReward("Alex_RP");
for (const bonus of claim.rewards ?? []) giveBonus("Alex_RP", bonus.id);
}Identify the player with exactly one of player, steamId, discordId, license, uuid, ip or voteId. claim() always sends an Idempotency-Key header: an automatic retry after a network error never claims two votes. Pass your own key (for example ${player}:${hour}) to make your own retries safe as well.
Real-time events
flitt.events.connect() opens a WebSocket stream that receives your servers' events as they happen: votes, joins and leaves, chat, status, reviews, affluence. No polling and no public URL: ideal for a Discord bot or a process that runs all the time.
const stream = flitt.events.connect({ events: ["vote_created", "player_joined"] });
stream.on("ready", ({ servers }) => console.log("following", servers));
stream.on("vote_created", (e) => console.log(`${e.data?.player} voted for ${e.server.name}`));
stream.on("player_joined", (e) => console.log("join", e.data));
stream.on("reconnecting", ({ delayMs }) => console.log("reconnecting in", delayMs, "ms"));
stream.on("error", (err) => console.error(err.message));Or with for await:
for await (const event of flitt.events.connect({ servers: ["srv_123"] })) {
if (event.event === "player_chat") relayToDiscord(event);
}| Option | Default | Purpose |
|---|---|---|
servers | all your servers | Servers to follow (200 at most) |
events | every type your role allows | Event types to receive |
resume | true | After a reconnection, replay what was missed (up to 1 hour) |
lastEventIds | none | Positions saved from stream.lastEventIds, to resume after a restart |
WebSocket | global WebSocket | Constructor to use (ws package on Node.js 20) |
signal | none | AbortSignal that closes the stream |
maxReconnectDelayMs | 30000 | Longest wait between two reconnection attempts |
bufferSize | 1000 | Events kept for a slow for await loop (oldest dropped first) |
Available listeners: ready, event and one per type (vote_created…), reconnecting, gap (events too old to be replayed), error and close. stream.close() stops it for good; stream.connected and stream.lastEventIds give its state.
What you receive. With the account key: the servers you own or are a member of, filtered by your role (status, affluence, players, chat, votes: see the event table). With a server token: vote_created and review_created of its server only.
Resuming after a restart of your process:
import { readFile, writeFile } from "node:fs/promises";
const saved = JSON.parse(await readFile("positions.json", "utf8").catch(() => "{}"));
const stream = flitt.events.connect({ lastEventIds: saved });
process.on("SIGTERM", async () => {
await writeFile("positions.json", JSON.stringify(stream.lastEventIds));
stream.close();
});Runtimes. The stream uses the native WebSocket (Node.js 22+, Deno, Bun). On Node.js 20, install ws and pass it:
import WebSocket from "ws";
const stream = flitt.events.connect({ WebSocket });Verifying a webhook
If you prefer to receive events on your own URL, verifyWebhook() checks that the request really comes from Flitt: HMAC-SHA256 signature, age (5 minutes) and replay. Pass the raw body (not re-serialised JSON) and the secret shown when the webhook was created.
Express
import express from "express";
import { verifyWebhook, FlittWebhookError } from "@flitt/api";
const app = express();
app.post("/flitt-webhook", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhook({
payload: req.body,
headers: req.headers,
secret: process.env.FLITT_WEBHOOK_SECRET!,
isReplay: (nonce) => seenBefore(nonce),
});
if (event.event === "server_down") console.log(`${event.server.name} is down`);
res.sendStatus(204);
} catch (err) {
res.sendStatus(err instanceof FlittWebhookError ? 400 : 500);
}
});Fetch API (Next.js, Workers, Deno, Bun)
export async function POST(request: Request) {
const event = await verifyWebhook({
payload: await request.text(),
headers: request.headers,
secret: process.env.FLITT_WEBHOOK_SECRET!,
});
return new Response(null, { status: 204 });
}Every event has the same envelope, typed WebhookEvent: { event, timestamp, server: { id, name }, data }.
Errors
All errors extend FlittError. API errors extend FlittAPIError and carry status, code, message, details, method, path, rateLimit and retryAfterMs. They never contain your key.
| Class | When |
|---|---|
FlittConfigError | Bad option or argument, detected before any request |
FlittValidationError | 400: invalid parameter |
FlittAuthenticationError | 401: missing, invalid or revoked key / token |
FlittPermissionError | 403: plan or role does not allow it |
FlittNotFoundError | 404: not found, or not yours |
FlittConflictError | 409: conflict (duplicate rule, inactive server…) |
FlittRateLimitError | 429: quota reached, see retryAfterMs and rateLimit.reset |
FlittServerError | 5xx: temporary error on Flitt's side |
FlittTimeoutError | No response within timeoutMs |
FlittConnectionError | Network failure (DNS, TLS, reset…) |
FlittAbortError | Aborted through your AbortSignal |
import { FlittNotFoundError, FlittRateLimitError } from "@flitt/api";
try {
await flitt.servers.get("srv_123");
} catch (err) {
if (err instanceof FlittNotFoundError) console.log("Unknown server");
else if (err instanceof FlittRateLimitError) console.log(`Retry in ${err.retryAfterMs} ms`);
else throw err;
}Options
new Flitt({
apiKey: "flt_...",
serverToken: "flv_...",
baseUrl: "https://api.flitt.me",
timeoutMs: 15_000,
maxRetries: 2,
maxRetryDelayMs: 30_000,
fetch: customFetch,
userAgent: "my-bot/1.0",
onRateLimit: (rl) => metrics.gauge("flitt.remaining", rl.remaining ?? Infinity),
dangerouslyAllowBrowser: false,
});| Option | Default | Purpose |
|---|---|---|
baseUrl | https://api.flitt.me | HTTPS required (HTTP accepted for localhost) |
timeoutMs | 15000 | Timeout per attempt |
maxRetries | 2 | Retries of idempotent requests: network errors, 408, 425, 5xx, short 429s |
maxRetryDelayMs | 30000 | Longest single wait between retries; a longer Retry-After is not waited for |
fetch | global fetch | Tests, proxies, instrumentation |
userAgent | flitt-api-js/<version> | Server-side only |
onRateLimit | none | Called with the quota state after every response |
dangerouslyAllowBrowser | false | Allows running in a browser (not recommended) |
Every method also takes a last { signal?, timeoutMs?, maxRetries? } argument:
const controller = new AbortController();
setTimeout(() => controller.abort(), 2000);
await flitt.servers.list({}, { signal: controller.signal, timeoutMs: 5000, maxRetries: 0 });Security
- HTTPS only (HTTP accepted for
localhostduring development); credentials, query strings and fragments refused inbaseUrl. - Redirects never followed: the
Authorizationheader cannot leak to another host. - Path segments are URL-encoded,
.and..segments refused. - Credentials live in private fields:
console.log,util.inspect,JSON.stringifyand errors never show them. - Credential formats checked locally: a server token passed as an account key (or the reverse) fails before any request.
- Real-time streams use
wss://only; the credential travels in the first message, never in the URL. - Responses larger than 32 MB are refused.
Versions and updates
Current version: 1.3.0. The module follows semantic versioning: a minor version (1.x) never introduces a breaking change. To update:
npm install @flitt/api@latest
npm ls @flitt/api| Version | What's new |
|---|---|
| 1.3.0 | Real-time events: flitt.events.connect(), automatic reconnection and replay, typed errors. |
| 1.2.x | hold on servers (maintenance, development, closure) and server_hold_started / server_hold_ended events. |
| 1.1.0 | Votes and reviews in the Flitt client (flitt.votes, flitt.reviews), server token through serverToken. |
| 1.0.0 | First release: servers, players, firewall, commands, Discord, webhooks. |
From 1.2: nothing to change, flitt.events is simply new. From 1.0: the separate FlittVotes client still works but is deprecated; move to flitt.votes:
const votes = new FlittVotes({ token: process.env.FLITT_VOTE_TOKEN });
await votes.claim({ player: "Alex_RP" });
const flitt = new Flitt({ serverToken: process.env.FLITT_VOTE_TOKEN });
await flitt.votes.claim("srv_123", { player: "Alex_RP" });The installed version is exported by the module, and sent in the User-Agent:
import { VERSION } from "@flitt/api";
console.log(VERSION);