Back to documentation
Features

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.

1

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-After honoured, idempotent vote claims.
  • Real time: votes, players, chat and status pushed over WebSocket, with automatic reconnection and replay.
2

Installation

npm install @flitt/api
# pnpm add @flitt/api · yarn add @flitt/api · bun add @flitt/api

First 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");
3

Credentials

There are two credentials. Pass one or both to the constructor, or let the module read them from environment variables:

Account keyServer token
OptionapiKey (env FLITT_API_KEY)serverToken (env FLITT_SERVER_TOKEN)
Formatflt_…flv_…
Created inDashboard → Profile → APIDashboard → your server → Votes → API
OpensEvery endpoint, on every server you own or are a member of (within your role)Votes and reviews of one server
QuotaMonthly, per plan120 requests / minute
Use itYour backend, your Discord bot, your toolsOn 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" });
4

Quotas and limits

PlanAccount keyServer token
Freemium1,000 requests / month120 requests / minute
Premium20,000 requests / month120 requests / minute
EnterpriseUnlimited120 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);
5

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 / deleteMany

ALLOW / 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 / unlink

Discord 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).

6

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);
}
7

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.

8

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);
}
OptionDefaultPurpose
serversall your serversServers to follow (200 at most)
eventsevery type your role allowsEvent types to receive
resumetrueAfter a reconnection, replay what was missed (up to 1 hour)
lastEventIdsnonePositions saved from stream.lastEventIds, to resume after a restart
WebSocketglobal WebSocketConstructor to use (ws package on Node.js 20)
signalnoneAbortSignal that closes the stream
maxReconnectDelayMs30000Longest wait between two reconnection attempts
bufferSize1000Events 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 });
9

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 }.

10

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.

ClassWhen
FlittConfigErrorBad option or argument, detected before any request
FlittValidationError400: invalid parameter
FlittAuthenticationError401: missing, invalid or revoked key / token
FlittPermissionError403: plan or role does not allow it
FlittNotFoundError404: not found, or not yours
FlittConflictError409: conflict (duplicate rule, inactive server…)
FlittRateLimitError429: quota reached, see retryAfterMs and rateLimit.reset
FlittServerError5xx: temporary error on Flitt's side
FlittTimeoutErrorNo response within timeoutMs
FlittConnectionErrorNetwork failure (DNS, TLS, reset…)
FlittAbortErrorAborted 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;
}
11

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,
});
OptionDefaultPurpose
baseUrlhttps://api.flitt.meHTTPS required (HTTP accepted for localhost)
timeoutMs15000Timeout per attempt
maxRetries2Retries of idempotent requests: network errors, 408, 425, 5xx, short 429s
maxRetryDelayMs30000Longest single wait between retries; a longer Retry-After is not waited for
fetchglobal fetchTests, proxies, instrumentation
userAgentflitt-api-js/<version>Server-side only
onRateLimitnoneCalled with the quota state after every response
dangerouslyAllowBrowserfalseAllows 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 });
12

Security

  • HTTPS only (HTTP accepted for localhost during development); credentials, query strings and fragments refused in baseUrl.
  • Redirects never followed: the Authorization header cannot leak to another host.
  • Path segments are URL-encoded, . and .. segments refused.
  • Credentials live in private fields: console.log, util.inspect, JSON.stringify and 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.
13

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
VersionWhat's new
1.3.0Real-time events: flitt.events.connect(), automatic reconnection and replay, typed errors.
1.2.xhold on servers (maintenance, development, closure) and server_hold_started / server_hold_ended events.
1.1.0Votes and reviews in the Flitt client (flitt.votes, flitt.reviews), server token through serverToken.
1.0.0First 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);