Libraries
TypeScript SDK

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/sdk
⚠️

Use 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 stopped

Mods

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 $10

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