Runtime guide

Endpoint calls, game values, encrypted requests and key pinning, compatibility status and helpers.

Everything game code calls. The generated per-member reference is at sboxns.com/docs/library/api; copyable patterns are in Examples/.

Calling the server

if ( !NetworkStorage.IsConfigured )
    NetworkStorage.Configure( "your_project_id", "sbox_ns_your_public_key" );

// Endpoint without input (GET; POST when encrypted requests are required)
var player = await NetworkStorage.CallEndpoint( "load-player" );

// Endpoint with input (POST)
var result = await NetworkStorage.CallEndpoint( "mine-ore", new { ore_id = "iron", kg = 5.0f } );

// Endpoint URLs copied from the dashboard work too; the slug is extracted.
await NetworkStorage.CallEndpoint(
    "https://api.sboxcool.com/v3/endpoints/your_project_id/mine-ore?apiKey=sbox_ns_your_public_key",
    new { ore_id = "iron", kg = 5.0f } );

// Documents (dedicated servers attach +network_storage_secret_key automatically)
await NetworkStorage.SaveDocument( "player-data", Game.SteamId.ToString(), new { level = 2, xp = 100 } );
await NetworkStorage.UpdateDocument( "player-data", Game.SteamId.ToString(),
    NetworkStorageOperation.Increment( "xp", 50, source: "server", reason: "quest" ),
    NetworkStorageOperation.Set( "lastSeen", DateTimeOffset.UtcNow.ToUnixTimeSeconds() ) );
await NetworkStorage.DeleteDocument( "player-data", Game.SteamId.ToString() );

if ( result.HasValue )
{
    var currency = result.Value.Int( "currency", 0 );
}
Member Description
Configure(projectId, apiKey, ...) Set credentials once at startup. See getting started.
IsConfigured / ApiRoot Whether Configure ran; the full API URL (.../v3, or .../api with apiVersion api).
CallEndpoint(slugOrUrl, input?) Call an endpoint by slug or dashboard URL. Returns JsonElement?.
TryGetLastEndpointError(...) The last error code and message for a call (endpoint slug, storage or game-values).
GetGameValues() All game value groups and tables in one request.
GetDocument(collectionId, documentId?) Read a document; defaults to the current player's Steam ID.
SaveDocument / UpdateDocument / DeleteDocument Replace a document, apply set/inc/push/pull/remove operations, or delete one row. Public deletes need record deletes enabled on the server.
ListRecords / CreateRecord / RenameRecord / DeleteRecord Multi-record save slots.
TrackAnalyticsEvent(eventType, payload?) Custom Player Analytics event; the name must be listed in the project's analytics settings.
NetworkStorageAnalytics.Warning/Error/SessionStart/SessionEnd Warnings, caught exceptions and session boundaries on the player timeline.
LibraryVersion / MinServerVersion Library SemVer (1.2.0) and the oldest supported self-hosted server (0.4.0, metadata only).
ServerVersion / LibraryStatus / OnLibraryStatusChanged / HasCapability(name) What the server reported; see server compatibility.

Retries and errors

Rate-limited calls (HTTP 429) are retried at most twice, with the total wait capped at 5 seconds, using the server's Retry-After. GET requests always retry; other methods only when the server marks the response retrySafe. A call that still fails is reported as RATE_LIMITED through TryGetLastEndpointError.

Analytics batching

When the server advertises analytics-batch, events are queued and sent together (up to 50 events or 256 KiB per request; the first event waits at most 2 seconds). Servers without the capability get one request per event. Rejected events, events over 32 KiB and events beyond the 500-event queue are counted in a [NetworkStorage] Dropped N analytics event(s) warning, at most once every 30 seconds. An abrupt process exit can lose in-flight events.

Game values

Shared numbers the game and server both read: tuning constants, named groups and typed tables. Author them in the server's owner console or over its API; see the server's game values guide for revisions, snapshots and a worked anti-exploit example.

var gv = (await NetworkStorage.GetGameValues()).Value;
var combat = gv.GetProperty( "groups" ).GetProperty( "combat" ).GetProperty( "values" );
int zombieHp = combat.Int( "zombie_hp_per_level" ) * zombieLevel;
var weapons = gv.GetProperty( "tables" ).GetProperty( "weapons" )
    .ReadList( "rows", r => (Id: r.Str( "weapon_id" ), Damage: r.Int( "damage" ), Model: r.Str( "model" )) );

GetGameValues() is one GET {ApiRoot}/values/{projectId}. The library keeps no offline copy: load once at startup and look up locally afterwards. Game code cannot write game values. Endpoints read the same numbers with {{values.combat.zombie_hp_per_level}} and lookup steps with source: values.

A build keeps its published snapshot only when the project has an explicit revision policy and its revision is retained inside the support window; otherwise it reads live values.

GetGameValues is a public, unsigned GET outside the endpoint path: neither the encryption pin nor a refused config covers it, and a proxy can read or change it. Do not put secrets in game values.

Encrypted requests

The server owner picks the mode (off, v1, v2-preferred, v2-required) and the game follows the server's signed security config. v2 seals each request to a server-generated key that can rotate without a game update.

  • With encryption required, a call without input is still sent as an encrypted POST. The host's CallEndpointAs path builds no envelope, so a no-input call on behalf of another player is refused locally with ENCRYPTION_REQUIRES_POST.
  • Pin the signing key so a proxy or swapped config cannot substitute its own: pass securityKeyFingerprint as the seventh Configure argument, or enter it in the Setup window. Copy it from sbox-ns security status ("Security signing key"); spaces, colons and case are ignored. With a pin, endpoint calls are refused with ENCRYPTION_KEY_UNVERIFIED until a config signed by that key loads.
  • Without a pin, the first signing key seen per API root is trusted and saved in the game's data folder (network-storage-signing-key-trust.json). A later change warns and is refused with ENCRYPTION_SIGNING_KEY_CHANGED; the last trusted config is kept. Verify the server out of band before setting a new pin or resetting the trust store. Unpinned games that never receive a trusted config stay on v1.
  • A config that fails verification refuses every endpoint call, plain or encrypted. Calling Configure with a different pin, base URL, API version or project drops the held config and verifies again.

Server compatibility status

The library reads x-ns-server-version and x-ns-library-status from responses and capabilities from /v3/server-info:

Member Meaning
ServerVersion Last version the server reported, or null.
LibraryStatus supported, outdated, deprecated, unsupported, blocked or unknown.
OnLibraryStatusChanged Raised when the status changes. No player-facing UI is shown.
HasCapability(name) true unless the server lists capabilities and the name is missing.

A 426 LIBRARY_UNSUPPORTED or LIBRARY_BLOCKED response is surfaced through TryGetLastEndpointError and logged once. In the editor, Setup and the Sync Tool show a banner for outdated/unknown (or a newer published revision), a once-per-session dialog for deprecated, and refuse Push All for unsupported/blocked; Pull and Test Live keep working. Nothing here runs in play mode or published games.

Runtime, Sync Tool and Python sync requests send x-ns-library-version. Editor tools also send x-ns-client-type: editor and, when the installed .version has a positive revision, x-ns-library-revision. Runtime never reads .version. These headers are self-reported diagnostics, not authentication.

Helpers

JSON

var name   = data.Str( "playerName", "Unknown" );   // JsonHelpers.GetString( data, ... )
var level  = data.Int( "level", 1 );                // missing keys and "5" strings handled
var speed  = data.Float( "speed", 1.0f );
var active = JsonHelpers.GetBool( data, "active", true );
var upgrades = data.ReadStringList( "purchasedUpgrades" );
var ores     = data.ReadDictionary( "ores", v => (float)v.GetDouble() );
var items    = data.ReadList( "rows", row => new ItemInfo( row.Str( "id" ), row.Str( "name" ), row.Int( "tier" ) ) );

SaveStateTracker

Wraps endpoint calls with state for HUD feedback:

var tracker = new SaveStateTracker();
var result = await tracker.Call( "mine-ore", new { ore_id = "iron", kg = 5 } ); // State: Saved or Error; IsBusy while in flight

await tracker.CallAndApply( "sell-ore", input,
    applyOptimistic: () => { /* update local state */ },
    applyServer: data => { /* apply authoritative response */ },
    revert: () => { /* undo on failure */ } );

NetLog

Ring buffer of every Network Storage request, response and error, for debug panels:

foreach ( var entry in NetLog.Entries ) { /* entry.Time, entry.Kind, entry.Tag, entry.Message */ }
NetLog.Info( "my-system", "Custom log message" );
var version = NetLog.Version; // increments on every add or clear

Generated from sbox-cool/sbox-network-storage/docs/runtime-api.md at 7f77770c81dd6ab84facfa05e9a87010b6f27983 (7f77770).

Publication and source

GitHub source; s&box publication unverified. No confirmed package revision. All versions

Server: 0.5.1 (0.5.1), GitHub source sbox-cool/sbox-network-storage-server at v0.5.1 (1b949e4). Library: main source (7f77770) (source-7f77770), GitHub source sbox-cool/sbox-network-storage at 7f77770c81dd6ab84facfa05e9a87010b6f27983 (7f77770).