The connector is configured once, through a readonly SteamConfig — everything the SDK needs at runtime lives on that object.

SteamConnector targets https://api.steampowered.com and carries your credentials:

  1. Keep your Steam Web API key out of the source tree:

    .env
    STEAM_API_KEY=your-steam-web-api-key
  2. Hand it to a SteamConfig, and the config to a connector:

    steam.php
    use Fkrzski\SteamApiSdk\SteamConfig;
    use Fkrzski\SteamApiSdk\SteamConnector;
    $connector = new SteamConnector(new SteamConfig(
    apiKey: $_ENV['STEAM_API_KEY'],
    ));
  3. Reuse that one connector — it carries the key and the daily budget, so building a fresh one per call buys you nothing.

The key is appended to every request as the key query parameter, so you never pass it per call. The endpoints Steam serves anonymously — GetNumberOfCurrentPlayers, GetGlobalAchievementPercentagesForApp, GetSDRConfig, GetServersAtAddress, UpToDateCheck, GetNewsForApp and GetServerInfo — go out with it stripped back off. GetSupportedAPIList keeps it, to list the methods the key can call. A request of your own picks the same way, through marker interfaces.

Option Type Default Description
apiKey ?string null Your Steam Web API key. Leave it out to reach only the anonymous endpoints.
rateLimitStore ?RateLimitStore MemoryStore Where the request budget, and the lock after a 429, are kept.
sender ?Sender GuzzleSender The HTTP client requests go out through.
language ?Language null Language for endpoints that localise their payload.
connectTimeout ?float 10.0 Seconds to wait for a connection to Steam.
requestTimeout ?float 30.0 Seconds the whole request may take.
tries int 1 Attempts per request, the first one included.
retryInterval int 0 Milliseconds to pause before a retry.
exponentialBackoff bool false Double the pause after every retry.
rateLimits ?array<Limit> 100 000 a day with a key Limits that replace the daily budget; [] meters nothing locally.
tooManyRequestsCooldown int 60 Seconds a 429 without Retry-After refuses requests locally.

A config built without one reaches the anonymous endpoints and GetSupportedAPIList, which then lists only the methods Steam serves without a key. Every other request raises ApiKeyNotConfiguredException before it leaves your process, naming the request class that needed the key:

steam.php
$connector = new SteamConnector(new SteamConfig);
$connector->stats()->currentPlayers(381210); // sent
$connector->users()->summaries([$steamId]); // ApiKeyNotConfiguredException

Steam localises the achievement and stat names it sends back through the l query parameter. Set it once on the config rather than on every call:

steam.php
use Fkrzski\SteamApiSdk\Enums\Language;
$connector = new SteamConnector(new SteamConfig(
apiKey: $_ENV['STEAM_API_KEY'],
language: Language::Polish,
));

A language passed to a call wins over the config, and endpoints that do not localise anything never receive one:

$connector->stats()->achievements($steamId, 381210); // polish
$connector->stats()->achievements($steamId, 381210, Language::English); // english

Steam’s codes are Valve’s own rather than ISO, so the Language enum transcribes their table.

Both are in seconds, and both fall back to Saloon’s own defaults when left out:

steam.php
$connector = new SteamConnector(new SteamConfig(
apiKey: $_ENV['STEAM_API_KEY'],
connectTimeout: 3,
requestTimeout: 120,
));

0 waits indefinitely, and a negative value throws InvalidTimeoutException as the config is built. A request that runs out of either raises SteamConnectionException, like any other request Steam never answered.

Every request is sent once until tries says otherwise:

steam.php
$connector = new SteamConnector(new SteamConfig(
apiKey: $_ENV['STEAM_API_KEY'],
tries: 3,
retryInterval: 500,
exponentialBackoff: true,
));

retryInterval is in milliseconds and exponentialBackoff doubles it after each retry, so the config above pauses 500 ms, then 1 s. Fewer than one try or a negative interval throws InvalidRetryException as the config is built.

Only what can change by the next attempt is retried: Steam unreachable, a timeout, a 5xx. A 4xx is thrown at once, and so is SteamRateLimitException, whether the budget ran out locally or Steam answered 429. So are the 500 GetSDRConfig answers for an app ID Steam does not know, as AppNotFoundException, and a failure Steam reports in a 200 payload. Once the tries are spent, the last attempt’s exception is thrown. sendAsync() and pool() send every request once, and so do summaries() and bans() with more than 100 IDs.

sender takes any Saloon\Contracts\Sender: one your framework already configures, one built on another HTTP client, or one GuzzleSender with your middleware, shared by every connector:

steam.php
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Saloon\Http\Senders\GuzzleSender;
$sender = new GuzzleSender;
$sender->addMiddleware(Middleware::mapRequest(
static fn (RequestInterface $request): RequestInterface => $request->withHeader('User-Agent', 'my-app/1.0'),
));
$store = new SteamConnector(new SteamConfig(apiKey: $_ENV['STEAM_STORE_KEY'], sender: $sender));
$stats = new SteamConnector(new SteamConfig(apiKey: $_ENV['STEAM_STATS_KEY'], sender: $sender));

The connector wraps whatever you pass in SteamSender, so a request Steam never answered still raises SteamConnectionException and is retried, and a 4xx or 5xx maps the same under send(), sendAsync() and pool(). $connector->sender() returns that wrapper rather than a GuzzleSender, so keep your own handle to reach Guzzle.

A sender built on another client has to:

  • read connect_timeout and timeout from $pendingRequest->config(): seconds, set from connectTimeout and requestTimeout, 0 for no limit;
  • send $pendingRequest->createPsrRequest() as built, the API key in its query;
  • return a Response from $pendingRequest->getResponseClass()::fromPsrResponse() for every status, and under sendAsync() resolve it or reject with $response->toException();
  • report a request that got no answer as FatalRequestException or a PSR-18 NetworkExceptionInterface. Anything else reaches the caller as it is, without a retry.