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
CallEndpointAspath builds no envelope, so a no-input call on behalf of another player is refused locally withENCRYPTION_REQUIRES_POST. - Pin the signing key so a proxy or swapped config cannot substitute its own: pass
securityKeyFingerprintas the seventhConfigureargument, or enter it in the Setup window. Copy it fromsbox-ns security status("Security signing key"); spaces, colons and case are ignored. With a pin, endpoint calls are refused withENCRYPTION_KEY_UNVERIFIEDuntil 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 withENCRYPTION_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
Configurewith 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