The bridge wires the SteamConnector from a single config file. In most apps the only value you set is your API key.

One command covers both setup steps:

Terminal window
php artisan steam:install

Publishes config/steam-api.php, then prompts for your Steam Web API key — hidden as you type — and appends STEAM_API_KEY to .env.

Safe to re-run: an existing config file is kept, an existing key is replaced only once you confirm, and a blank answer leaves .env alone.

The provider merges its own steam-api config, so the package works unpublished. Publish it when you want the file in your repo to override the defaults:

Terminal window
php artisan vendor:publish --tag=steam-api-config

This writes config/steam-api.php:

config/steam-api.php
return [
// Your Steam Web API key, obtained from https://steamcommunity.com/dev.
// Sent as the "key" query parameter on every request that needs one.
'key' => env('STEAM_API_KEY'),
// Steam's own language code, sent on every request whose payload is
// localised. Unset, it follows the application locale.
'language' => env('STEAM_API_LANGUAGE'),
// How long to wait on Steam, in seconds, and how many attempts a request
// Steam never answered, or answered with a 5xx, gets in all.
'http' => [
'connect_timeout' => env('STEAM_API_CONNECT_TIMEOUT'),
'request_timeout' => env('STEAM_API_REQUEST_TIMEOUT'),
'retry' => [
'tries' => env('STEAM_API_RETRY_TRIES', 1),
'interval' => env('STEAM_API_RETRY_INTERVAL', 0),
'exponential_backoff' => env('STEAM_API_RETRY_EXPONENTIAL_BACKOFF', false),
],
// A Saloon Sender class, built by the container. Unset, Guzzle.
'sender' => env('STEAM_API_SENDER'),
],
// The cache store holding the request counters, the limits replacing
// Steam's 100 000 a day, and the seconds a 429 holds requests back.
'rate_limit' => [
'store' => env('STEAM_API_RATE_LIMIT_STORE'),
'limits' => null,
'cooldown' => env('STEAM_API_RATE_LIMIT_COOLDOWN', 60),
],
// Resolve a route parameter into a SteamId value object. Opt-in. See the
// route binding guide for details.
'route_binding' => [
'enabled' => false,
'parameter' => 'steamId',
],
// Render an unhandled Steam failure as the status it maps to instead of a
// blanket 500. On by default.
'exceptions' => [
'render' => true,
],
// The channel from config/logging.php failed Steam calls are logged to, and
// whether every response is too. Unset, nothing is logged.
'logging' => [
'channel' => env('STEAM_API_LOG_CHANNEL'),
'responses' => env('STEAM_API_LOG_RESPONSES', false),
],
];

Set the key in .env. It is read through config('steam-api.key') and appended to every request that needs one, so you never pass it per call:

.env
STEAM_API_KEY=your-steam-web-api-key

Get one from the Steam Web API dashboard. The key is optional: Steam serves the apps() and news() endpoints, currentPlayers(), globalAchievements(), newsFeed() and webApi()->serverTime() anonymously, so those go out without one, and webApi()->supportedApis() sends it only when there is one. With the key unset — or present but blank — any other request raises the base SDK’s ApiKeyNotConfiguredException. Its message says to pass a key to SteamConfig, which here means setting STEAM_API_KEY, or steam-api.key directly.

Steam localises achievement names, descriptions and stat labels, and the connector carries one language code on every request that reads them. The codes are Valve’s own rather than ISO ones — koreana, not ko; see Fkrzski\SteamApiSdk\Enums\Language for the list:

.env
STEAM_API_LANGUAGE=polish
steam-api.language What the request carries
unset — the default the language the application locale maps to, or nothing where it maps to none
'' nothing; Steam picks
'polish' polish
anything else nothing goes out — InvalidSteamLanguageException instead

A rejected code raises the first time the connector is resolved, so it surfaces on your first Steam call rather than at boot. It is a misconfiguration, so it renders as a 500 whatever steam-api.exceptions.render says.

The language: argument on achievements(), schema() and userStats() overrides the default for that one call.

With steam-api.language unset, Localization\LocaleLanguageResolver maps app()->getLocale() through a table of Valve’s spellings: pl is polish, ko is koreana, pt_BR is brazilian. A locale whose region Steam does not distinguish falls back to the base one — de_AT is german — and one Steam publishes no language for sends nothing rather than raising.

Bind Contracts\SteamLanguageResolver when your application has a locale scheme of its own:

app/Providers/AppServiceProvider.php
use Fkrzski\LaravelSteamApiSdk\Contracts\SteamLanguageResolver;
$this->app->bind(SteamLanguageResolver::class, TenantLanguageResolver::class);

The http block sets how long the connector waits on Steam and how many times it tries, each option from its own variable:

.env
STEAM_API_CONNECT_TIMEOUT=3
STEAM_API_REQUEST_TIMEOUT=60
STEAM_API_RETRY_TRIES=3
STEAM_API_RETRY_INTERVAL=500
STEAM_API_RETRY_EXPONENTIAL_BACKOFF=true
Variable Unset Meaning
STEAM_API_CONNECT_TIMEOUT 10, Saloon’s seconds to connect, 0 waits forever
STEAM_API_REQUEST_TIMEOUT 30, Saloon’s seconds for the whole request, 0 waits forever
STEAM_API_RETRY_TRIES 1 attempts in all, so 1 never retries
STEAM_API_RETRY_INTERVAL 0 milliseconds between attempts
STEAM_API_RETRY_EXPONENTIAL_BACKOFF false doubles the interval after each retry

Only what can change by the next attempt is retried: Steam unreachable, a timeout, a 5xx. A 4xx and a spent rate limit are thrown at once, and so is the 500 sdrConfig() answers an unknown app with. Once the tries are spent, a request Steam never answered raises SteamConnectionException and a 5xx the root SteamApiException. Requests sent through pool() go out once.

A value that is not a number, or is out of range, raises InvalidSteamHttpOptionException naming the key. Like a rejected language code, it surfaces on your first Steam call and renders as a 500.

Requests go out through Guzzle until STEAM_API_SENDER names a class implementing Saloon\Contracts\Sender. The container builds it each time the connector is built, so a binding of that class decides how: a singleton is shared, and an unbound class is built anew.

That is where Guzzle middleware goes, since Steam::connector()->sender() returns the base SDK’s SteamSender wrapper rather than a GuzzleSender. Give the sender a class of its own and bind it with the middleware attached:

app/Http/SteamGuzzleSender.php
namespace App\Http;
use Saloon\Http\Senders\GuzzleSender;
final class SteamGuzzleSender extends GuzzleSender {}
app/Providers/AppServiceProvider.php
use App\Http\SteamGuzzleSender;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
public function register(): void
{
$this->app->singleton(SteamGuzzleSender::class, fn (): SteamGuzzleSender => new SteamGuzzleSender()->addMiddleware(
Middleware::mapRequest(
static fn (RequestInterface $request): RequestInterface => $request->withHeader('User-Agent', 'my-app/1.0'),
),
));
}
.env
STEAM_API_SENDER=App\Http\SteamGuzzleSender

A sender built on another client has to read connect_timeout and timeout off the request, which is how the timeouts reach it — What a sender has to honour lists the rest.

The class name is normalised to the one it is declared with, so a leading \ or another casing still reaches the binding. A value naming no sender class raises InvalidSteamHttpOptionException on your first Steam call, rendered as a 500, and a binding that hands back something other than a Sender raises UnexpectedValueException.

With Laravel Nightwatch installed and enabled, the default Guzzle reports every Steam request to it as an outgoing request, which saloonphp/laravel-plugin cannot do, since the base SDK wraps every sender. NIGHTWATCH_ENABLED=false leaves plain Guzzle.

A sender of your own is left as it is, so it reports only once Nightwatch’s middleware is attached to it:

app/Providers/AppServiceProvider.php
use App\Http\SteamGuzzleSender;
use Laravel\Nightwatch\Facades\Nightwatch;
$this->app->singleton(SteamGuzzleSender::class, fn (): SteamGuzzleSender => new SteamGuzzleSender()
->addMiddleware(Nightwatch::guzzleMiddleware()));

Whichever sender carries a request, the API key is stripped from every Steam URL Nightwatch records.

The route_binding block resolves a route parameter straight into a SteamId. It is off by default — the parameter name is claimed application-wide. Set enabled to true to opt in; Route binding has the full guide.

The provider binds SteamConnector as a scoped instance: one per request, torn down with it. Under Laravel Octane and between queue jobs the container outlives the request, and a shared connector would carry its API key into the next one.

It resolves lazily, so a misconfigured option surfaces on your first Steam call rather than at boot. Reach it through Steam::connector() — for the current request only.

Contracts\SteamManager is what the Steam facade resolves — type-hint it to inject the manager instead of reaching through the facade:

app/Http/Controllers/PlayerController.php
use Fkrzski\LaravelSteamApiSdk\Contracts\SteamManager;
final class PlayerController
{
public function __construct(private readonly SteamManager $steam) {}
}

The contract and the concrete SteamManager resolve to one scoped instance, so binding either swaps what the facade hands back — a decorator wrapping the shipped manager, say. Contracts\SteamIdBinder is the same seam for the route binding, and Contracts\SteamLanguageResolver for the locale mapping. Every class the package ships is final; the container is the extension point.

The rate_limit block sets the cache store the request counters live in, the limits replacing Steam’s 100 000 a day, and the cooldown after a 429. Rate limiting has the full guide, queued jobs included.

A Steam failure nothing in the application caught renders as the status it maps to rather than a blanket 500 — the guide has the table. Turn the group off to render it yourself:

config/steam-api.php
'exceptions' => [
'render' => false,
],

The flag does not reach misconfiguration. A rejected API key, a missing one, a batch over 100 IDs, and a language code, HTTP option, rate limit option or logging option the config cannot use are a 500 whatever it is set to: Steam answers a bad key with a 400 or a 403, and passing that on would blame the caller for something only you can fix.

The logging block logs Steam traffic to a channel of yours. It is off by default — set STEAM_API_LOG_CHANNEL to opt in, and STEAM_API_LOG_RESPONSES to log every response too; Logging has the full guide.

The provider registers a Steam API section on Laravel’s about command:

Terminal window
php artisan about --only=steam_api
php artisan about --only=steam_api
Steam API .............................................................
429 Cool-down ........................................... 60s (default)
API Key .................................................. ********4f2a
Language .......................................... polish (locale: pl)
Logging ........................................... steam (+ responses)
Rate Limit Store ................................................ redis
Requests Remaining . 99,412 of 100,000 per day, 9 of 10 per 1s (sleeps)
Retries ............................. 3 attempts, 500ms apart, doubling
Route Binding ....................................... enabled (steamId)
Sender .................................. Guzzle + Nightwatch (default)
Timeouts ...................................... connect 3s, request 60s
  • API Key — masked to its last four characters, so the output is safe to paste into a bug report. NOT SET when no key is configured.
  • Language — the code every localised request carries, and which of the two answers it is: polish (config) or polish (locale: pl). NOT SET names its source the same way, and INVALID stands alone, since only config can carry a rejected code.
  • Logging — disabled until a channel is set, then its name, with (+ responses) once every response is logged too.
  • Route Binding — disabled, or enabled with the parameter name it claims.
  • Timeouts — the connect and request timeouts, Saloon’s own marked (default), and unlimited for 0.
  • Retries — none (default) for a single attempt, otherwise the attempts and the pause between them.
  • Sender — the class STEAM_API_SENDER names, otherwise Guzzle (default), or Guzzle + Nightwatch (default) while Nightwatch is reporting.

The timeout, retry, sender and logging rows read INVALID for a value the connector does not take. The three rate limit rows are covered in Rate limiting.

Nothing here runs on boot: the values are computed when the command renders, and no request is sent to Steam.