Skip to main content
Building blocks

Not translated yet — showing English.

Core

Core is the one object you create, and everything else hangs off it. One key in, and you hold context, identity, typed failures, tracing and batching. Every module call goes through it; no module has its own copy.

When to use it

  • You need to know who and where you are: identity, roles, unlocked modules, project, environment and region.
  • A cloud function has to write as a player. The write is attributed to that player, and the record names both the function and the player.
  • Retries must not apply twice: batch operations carry an idempotency key.
  • Failures must be branchable and searchable: every throw is a typed Problem with a stable code.
  • Skip it for messaging, calls and state. Those are the primitives: Events, RPC and Data, addressed through Entity; their audiences are Groups. Uploads and downloads are in Files & UGC, mounting and namespacing in Under the Hood.

Who does what

ActorOn this page
any actorreads identity, context, roles via Whoami
backend-serviceacts as a player; batches idempotent operations
operatorreads traces for failed or retried calls

At a glance

Whoami, the ambient context, and a batch that retries safely
var me  = PlayServ.Whoami();              // identity, roles, unlocked modules
var env = PlayServ.Context;               // project · env · region

// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
    b.Inventory.Grant(playerId, "starter.pack");
    b.Inventory.Grant(playerId, "starter.emote");
});
const me = playserv.whoami();             // identity, roles, unlocked modules
const env = playserv.context;             // project · env · region

// retries never double-apply: the batch carries an idempotency key
await playserv.batch(orderId, (b) => {
  b.inventory.grant(playerId, 'starter.pack');
  b.inventory.grant(playerId, 'starter.emote');
});
me = playserv.whoami()                    # identity, roles, unlocked modules
env = playserv.context                    # project · env · region

# retries never double-apply: the batch carries an idempotency key
async with playserv.batch(key=order_id) as b:
    b.inventory.grant(player_id, "starter.pack")
    b.inventory.grant(player_id, "starter.emote")
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

const FPSActor Me = Client->Whoami();               // identity, roles, unlocked modules
const FPSPlatformContext Env = Client->Context();   // project · env · region

// retries never double-apply: each keyed operation is safe to repeat
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(PackGrantId), PlayerId, PSKeys::Item::StarterPack);
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(EmoteGrantId), PlayerId, PSKeys::Item::StarterEmote);
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
var me  = PlayServ.Whoami();              // identity, roles, unlocked modules
var env = PlayServ.Context;               // project · env · region

// retries never double-apply: the batch carries an idempotency key
await PlayServ.Batch(key: orderId, b =>
{
    b.Inventory.Grant(playerId, "starter.pack");
    b.Inventory.Grant(playerId, "starter.emote");
});

Identity lives in Core, so no call takes a session or ctx parameter.

The model

What every error carries.

FieldWhat it is
codethe machine-readable name of the refusal. Stable: it reuses the platform's existing codes instead of inventing new ones
categorythe class of the refusal; it tells you whether a retry makes sense
trace identifierthe id of this occurrence. Always present, local errors included, so support never needs a reproduction
explanationtext for a person to read. Not stable: it changes and is localised at any time
per-field errorsfor validation refusals: field, code and message for each rejected field

Branch on the code, not the text

Put code and category in your logic. explanation is for people and changes without notice. A binding that exposes only the message has a bug.

Three origins.

OriginWhat happened
platformthe platform refused, with a code from its catalogue
localthe SDK refused before sending, with a code from its own published list
unknownthe call was sent and no answer came back, so the outcome is unknown

True of every refusal.

  • A refused operation applied nothing, so an error branch needs no compensating read. The two exceptions say so where they occur: a timeout, whose outcome is unknown, and a batch with per-element semantics.
  • The error is the same however it reaches you: the same code, category and origin whether the binding throws, returns a result or calls back on a subscription.
  • A timeout is the unknown origin, not an answer. What to do about it is declared per operation.

Errors

The same call, refused. rate_limited also says when a retry is allowed
try { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
    Hud.RetryAt(p.RetryAfter);
}
try { await playserv.inventory.grant(playerId, 'starter.pack'); }
catch (p) {
  if (Problem.code(p) === 'rate_limited') hud.retryAt(p.retryAfter);
  else throw p;
}
try:
    await playserv.inventory.grant(player_id, "starter.pack")
except Problem as p:
    if p.code == "rate_limited":
        hud.retry_at(p.retry_after)
    else:
        raise
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// UE builds run without exceptions — the completion carries the result, read explicitly
Client->Commerce->Entitlements->Grant(FPSIdempotencyKey(GrantId), PlayerId, PSKeys::Item::StarterPack,
    TPSOnResult<void>::CreateWeakLambda(this, [this](const TPSResult<void>& Result)
    {
        if (Result.IsRefused() && Result.Refusal().Code == FPSFailureCode::RateLimited)
        {
            Hud->RetryAt(Result.Refusal().RetryNotBefore);   // TOptional<FDateTime> — an instant, not a delay
        }
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
try { await PlayServ.Inventory.Grant(playerId, "starter.pack"); }
catch (Problem p) when (p.Code == "rate_limited")
{
    Hud.RetryAt(p.RetryAfter);
}

A caller without the role is refused, not served less. For a player or client session the fn and adm rows fail: calling act as player, emitting a trace or reading one returns a Problem with code forbidden. The credential is valid; its roles don't carry the right. Retrying with the same idempotency key changes nothing.

Limits

Don't retry an accepted call

Limits apply at admission. An accepted call has passed the limit and will run, however long it waits. A refusal of some later call doesn't affect it, so retrying the accepted call does the work twice.

You learn a limit by being refused. The SDK doesn't expose the value in force, the headroom left or a warning that you are close, and players never see limits. The refusal carries what you need:

  • the category, which says whether a retry makes sense
  • whose limit it was
  • when a retry is allowed, and over what window