TypeScript SDK
@truetick/sdk is a small, typed client for the TrueTick API. It handles the x-api-key header,
coerces JSON int64 strings to numbers, derives server identifiers from names, and exposes everything as
ergonomic resource methods.
Install
npm install @truetick/sdkUse 0.3.0 or later. @truetick/sdk 0.2.0 doesn't load under Node's ESM loader: one of its files
imports ./errors without the .js extension, so import fails with ERR_MODULE_NOT_FOUND.
Authenticate
import { TrueTickClient } from "@truetick/sdk";
// Explicit key
const client = new TrueTickClient({ apiKey: "ttk_your_key" });
// …or from the environment (TRUETICK_API_KEY)
const client2 = new TrueTickClient();
// Point at a non-default base URL (default: https://api.truetick.gg)
const client3 = new TrueTickClient({ apiKey: "ttk_…", baseUrl: "https://api.truetick.gg" });ClientOptions: { apiKey?: string; baseUrl?: string; userAgent?: string }. apiKey falls back to
TRUETICK_API_KEY, baseUrl to TRUETICK_API_URL. The constructor throws if no key is found.
Outside a browser, every request carries User-Agent: truetick-sdk/<version>. Set userAgent to put
your own product token in front of it: userAgent: "my-bot/1.2" sends my-bot/1.2 truetick-sdk/<version>.
In a browser page or worker the SDK adds no User-Agent: the browser sends its own, and the API's CORS
rules don't list User-Agent among the headers a page may set. The header and the userAgent option are
SDK 0.3.0 and later; 0.1.x and 0.2.0 send no User-Agent of their own, and TypeScript rejects userAgent
there as an unknown option.
Account
const me = await client.whoami();
// { accountId, email, emailVerified }Servers
// List / get
const servers = await client.servers.list();
const server = await client.servers.get("my-smp"); // id, hostname, state, ramMb, type, version, region, plan, …
// Create (id/hostname/container derived from name)
const created = await client.servers.create({
name: "My SMP",
ramMb: 4096,
type: "PAPER", // optional
version: "1.21.1", // optional
region: "na", // optional
plan: "metered", // optional
});
// Lifecycle
await client.servers.start("my-smp");
await client.servers.stop("my-smp");
await client.servers.restart("my-smp");
await client.servers.delete("my-smp"); // permanent
// Configure
await client.servers.updateVersion("my-smp", { type: "PURPUR", version: "1.21.1" }); // stopped only
await client.servers.setMotd("my-smp", "Welcome!");
await client.servers.setProperties("my-smp", {
properties: { difficulty: "hard", pvp: "true" },
idleTimeoutMinutes: 20,
});
// Templates
const templates = await client.templates.list();
const fromTpl = await client.servers.createFromTemplate("paper-survival", "my-server", { ramMb: 8192 });Metrics
const m = await client.servers.metrics("my-smp");
// { tps, tpsSource, mspt, msptP95, tickStatus, targetTps, headlineTps,
// headlineWindowSeconds, players, live }
// live:false is an honest "no data", not zero — and `live: true` on its own
// does not mean a TPS reading exists. tpsSource === "TPS_SOURCE_UNSPECIFIED"
// means there is none, and `tps` is a zero VALUE there, not a measured zero.
// SDK 0.2.0+: minute buckets of tick health, oldest first; a missing minute is a real gap.
const h = await client.servers.tickHistory("my-smp", { hours: 24 });See Honesty metrics for how to read these.
Console (RCON)
const { output } = await client.console.run("my-smp", "say Hello from the SDK!");Logs
// Snapshot (+ cursor for incremental polling)
const { lines, cursor, containerMissing } = await client.servers.recentLogs("my-smp", { tail: 100 });
// Live stream — async iterable of plain lines (heartbeats filtered)
for await (const line of client.servers.streamLogs("my-smp", { tail: 50 })) {
console.log(line);
}Full streaming guide: Stream logs.
Files & SFTP
const entries = await client.files.list("my-smp", "/data"); // [{ name, isDir, size }]
const text = await client.files.read("my-smp", "/data/server.properties");
await client.files.write("my-smp", "/data/motd.txt", "New motd"); // text, base64-encoded for you
await client.files.delete("my-smp", "/data/old.txt");
// Binary uploads (jars): use SFTP credentials
const cred = await client.servers.enableSftp("my-smp"); // { host, port, username, password }The file API encodes content as UTF-8 text → base64. For binary artifacts like plugin jars, use the
SFTP credentials from enableSftp instead. See Deploy a plugin.
Each enableSftp call issues a new SFTP password for the server, and the previous one stops working, a
login saved in an SFTP client included.
Backups
const backup = await client.backups.create("my-smp"); // { id, serverId, createdAt, sizeBytes }
const backups = await client.backups.list("my-smp");
await client.backups.restore("my-smp", backup.id); // server must be stoppedMods
const mods = await client.mods.list("my-smp");
await client.mods.add("my-smp", { source: "modrinth", projectId: "chunky", versionSpec: "1.4.28" }); // versionSpec optional
await client.mods.remove("my-smp", { source: "modrinth", projectId: "chunky" });my-smp above runs Paper (or, after updateVersion, Purpur) 1.21.1, so the project is a plugin built for
that core and version. An add can bring required dependencies as entries of their own, and some adds are
refused — see Mods & modpacks.
Wallet & billing
const wallet = await client.wallet.get(); // { accountId, balanceMicros }
console.log(`$${(wallet.balanceMicros / 1_000_000).toFixed(2)}`);
const { checkoutUrl } = await client.billing.createCheckout(10); // Paddle checkout for $10Standalone auth helpers
For sign-up / login flows (minting a key from credentials) the SDK also exports standalone functions — these hit the public auth endpoints directly and don't need a client:
import { signup, login, deviceStart, devicePoll } from "@truetick/sdk";
const minted = await login("https://api.truetick.gg", "you@example.com", "password");
// { apiKey, accountId, email, emailVerified }Each takes an optional last argument { userAgent } (SDK 0.3.0+) that goes in front of
truetick-sdk/<version>, the same as ClientOptions.userAgent.
Error handling
Every failed call of the client or the auth helpers throws a TrueTickError. Its message is the
server's own explanation (top up your wallet to start this server, node at capacity). status is the
HTTP code and code a name derived from it; grpcCode is the gRPC code the API sent
(failed_precondition, resource_exhausted, …), and retryAfter the seconds from a Retry-After
header when there was one. The app helpers signInWithDevice and AppClient use codes of their own,
listed in Errors:
import { TrueTickError } from "@truetick/sdk";
try {
await client.servers.start("my-smp");
} catch (e) {
if (e instanceof TrueTickError) {
console.error(`${e.message} (HTTP ${e.status}${e.grpcCode ? `, ${e.grpcCode}` : ""})`);
if (e.message === "node at capacity") {
// an honest refusal, not a rate limit: place the server in the other region
}
}
}The server's own message, grpcCode, retryAfter and details are SDK 0.3.0 and later. In 0.1.x and
0.2.0, TrueTickClient's message is a generic sentence for the HTTP status (0.2.0's AppClient already
carries the server's text), those three fields don't exist, and the auth helpers throw a plain Error.
See Errors for the full reference, including which refusals are worth retrying.
Exported types
TrueTickClient, ClientOptions, TrueTickError, TrueTickErrorDetails, Server, ServerMetrics, TPSSource,
TickMinute, TickHistory, Wallet, Backup,
FileEntry, Mod, WhoAmI, CreateServerInput, Template, TemplateOverrides, SftpCredential,
plus the naming helpers parseLabel, serverHostname, gameDomainFromBaseUrl and the auth helpers
above.