Skip to content
SteamLabs API

Endpoints

Settings

Read and change your own account settings: display preferences, session and boost limits, notification routing, connected destinations, and billing details.

Everything the Your account menu opens, over HTTP: timezone and display currency, the Steam session and hour boosting limits, which events reach you and where, and the name and address printed on your invoices.

One scope pair covers the whole domain. account.read for every read, account.write for every write. These are all preferences on your own row, and a key trusted to set a timezone is already trusted to flip a notification toggle.

What is not here

Password changes, email changes, MFA enrolment, browser session revocation and account deletion are not on this API, and will not be added.

Connected marketplaces and Steam Web API keys are also not here. They live on Connected services under integrations.read and integrations.write. A Discord webhook is a notification setting, not a trading integration, so it stays on this page.

Read your account preferences

Your display preferences: the timezone every time is rendered in, and the currency money is additionally shown converted into.

GET/api/v1/settings/account

Your timezone and display currency.

API key required

Requires account.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/settings/account \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "timezone": "Europe/Amsterdam",
    "effective_timezone": "Europe/Amsterdam",
    "preferred_currency": "EUR",
    "available_currencies": ["USD", "GBP", "EUR", "CHF", "RUB", "PLN", "BRL", "JPY", "…"]
}
Field Type Description
timezone string, null Your stored choice. null means you have never set one
effective_timezone string What times actually render in. Falls back to the app timezone when timezone is null, so you never have to know the fallback rule
preferred_currency string, null The currency money is also shown converted into. null shows amounts in their original currency with no conversion line
available_currencies array Every currency code preferred_currency accepts. Build your picker from this rather than a hard-coded list

Update your account preferences

Change either preference. Both PUT and PATCH behave the same way: a field you do not send keeps its stored value.

PATCH/api/v1/settings/account

Set your timezone or display currency.

API key required

Requires account.write.

Field Type Description
timezone string, null An IANA zone this deployment can resolve, for example America/New_York. Send null (or "") to clear it back to the app default
preferred_currency string, null One of available_currencies. Send null (or "") to stop converting
Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/settings/account \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timezone": "America/New_York", "preferred_currency": "GBP"}'
JSON
{
    "timezone": "America/New_York",
    "effective_timezone": "America/New_York",
    "preferred_currency": "GBP",
    "available_currencies": ["USD", "GBP", "EUR", "CHF", "RUB", "PLN", "BRL", "JPY", "…"]
}

An unknown zone or currency is refused with 422, never stored and never silently ignored.

Read your session and boost limits

The two cards on the Steam Sessions page: Session lifetime (how long a session lingers after its last task, how many may linger at once) and Hour boosting (how many accounts may boost at the same time).

GET/api/v1/settings/sessions

Your stored session and boost limits, what the fleet honours, and the bounds you may write.

API key required

Requires account.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/settings/sessions \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "steam_session_idle_timeout_minutes": 60,
    "steam_session_max_warm": 500,
    "boost_max_concurrent": 30,
    "effective": {
        "steam_session_idle_timeout_minutes": 60,
        "steam_session_max_warm": 100,
        "boost_max_concurrent": 30
    },
    "limits": {
        "idle_timeout_minutes_min": 5,
        "idle_timeout_minutes_max": 1440,
        "max_warm_ceiling": 100,
        "boost_max_concurrent_ceiling": 250
    }
}
Field Type Description
steam_session_idle_timeout_minutes integer How long a signed-in account stays open after its last task finishes. Defaults to 30
steam_session_max_warm integer How many of your accounts may stay signed in while idle. 0 signs out after every task. Defaults to 25
boost_max_concurrent integer How many accounts may boost at once across all your plans. 0 parks your boosting without touching your plans. Defaults to 25
effective object The same three numbers as the fleet honours them, after the ceilings are applied
limits object The bounds a write is validated against

Stored and effective are both reported because they can legitimately differ. A ceiling clamps on read and never rewrites what you stored, so an admin lowering your ceiling leaves your preference intact and raising it back restores your original number verbatim. In the example above the account asked for 500 warm sessions and the fleet is honouring 100.

Update your session and boost limits

PATCH/api/v1/settings/sessions

Set the idle timeout, the warm session cap, or the boost concurrency cap.

API key required

Requires account.write.

Field Type Description
steam_session_idle_timeout_minutes integer Minutes, between limits.idle_timeout_minutes_min (5) and limits.idle_timeout_minutes_max (1440). Any value in range works, the dashboard's preset list is only there to keep a select short
steam_session_max_warm integer 0 to limits.max_warm_ceiling
boost_max_concurrent integer 0 to limits.boost_max_concurrent_ceiling
Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/settings/sessions \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"steam_session_idle_timeout_minutes": 45, "steam_session_max_warm": 40}'

The response is the full read shape again, so one call tells you both what was stored and what the fleet will honour.

The ceilings are refused, not trimmed

steam_session_max_warm is bounded by a ceiling an admin sets on your account. Asking for more than it returns 422, and nothing is written:

JSON
{
    "message": "The given data was invalid.",
    "code": "validation_failed",
    "errors": {
        "steam_session_max_warm": ["Your warm session ceiling is 100. Read it from GET /api/v1/settings/sessions."]
    }
}

max_warm_ceiling is per user, set by an admin, and capped at 10,000 platform-wide.

boost_max_concurrent_ceiling resolves in this order, first answer wins: a per-user override an admin typed by hand, then your subscription plan's boost cap, then 10,000. Because the override can sit above or below the plan number, limits.boost_max_concurrent_ceiling on this endpoint is the authority for a write, not plan.max_boost_concurrent on GET /api/v1/me, which reports the plan's own cap.

Neither ceiling produces a 403 plan_limit_reached. Over the bound is a 422, whichever source the bound came from.

Read your notification preferences

The master switch, the full event by channel routing matrix, and the chat unread mode stored beside it. This is what the Notifications page renders.

GET/api/v1/settings/notifications

The master switch and every event x channel routing cell.

API key required

Requires account.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/settings/notifications \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "notifications": true,
    "chat_unread_mode": "mentions",
    "channels": ["dashboard", "discord", "telegram", "custom_webhook"],
    "connected_channels": ["discord"],
    "preferences": {
        "task_failed": { "dashboard": true, "discord": true, "telegram": false, "custom_webhook": false },
        "market_listing_sold": { "dashboard": true, "discord": false, "telegram": false, "custom_webhook": false },
        "billing_invoice_issued": { "dashboard": true, "discord": false, "telegram": false, "custom_webhook": false }
    }
}
Field Type Description
notifications boolean The master switch (All notifications). Off means nothing goes out on any channel, whatever the matrix says
chat_unread_mode string What counts toward your chat unread badges: all, mentions, or mute
channels array Every channel the matrix has columns for
connected_channels array Which external channels have a destination connected and turned on
preferences object Every event, with every channel, as a boolean

preferences always comes back complete, every event crossed with every channel. Your stored row only holds the cells that differ from an event's default, which is what lets a changed default reach everyone who never touched that row, but you never have to know that: read the merged matrix, patch the cells you care about.

Channels

Channel Delivers to
dashboard The bell in the dashboard, plus the live toast
discord Your connected Discord webhook
telegram Your connected Telegram bot and chat
custom_webhook Your own https endpoint, as a JSON POST

Every event defaults to dashboard on and the three external channels off.

Events

Event Fires when
task_failed A task ends Failed
workflow_run_completed An automation run finishes
workflow_run_failed An automation run fails
market_listing_sold One of your listings sold, on any marketplace
market_sale_delivered A sale reached its buyer and the payout is on its way
market_sale_settled A sale cleared its hold and the money moved to your marketplace balance
market_sale_needs_attention A sale needs manual delivery, failed to deliver, or is near the end of its window
marketcsgo_presence_broken An account with live Market.CSGO listings cannot be kept online
trade_up_completed A trade-up contract completed
trade_up_failed A trade-up contract failed
trade_up_dry_run The trade-up dry run digest is ready
chat_mentioned Someone @mentions you in chat
billing_deposit_credited A top-up went through and landed on your balance
billing_deposit_failed A top-up did not go through
billing_invoice_issued An invoice is ready to download
subscription_started You subscribed to a plan, or switched plans
subscription_renewed Your plan renewed itself and the money came off your balance
subscription_renewal_failed Your balance could not cover a renewal
subscription_expired Your paid period ran out and your plan ended

Sale events are venue-agnostic. Selling on CSFloat and on market.csgo raises the same situations, so the marketplace is in the message body rather than in a toggle per venue.

Update your notification preferences

A real PATCH: the cells you send are applied over the current matrix and everything else is left alone. Turning one event's Discord routing on does not mean restating sixty booleans.

PATCH/api/v1/settings/notifications

Flip the master switch, individual routing cells, or the chat unread mode.

API key required

Requires account.write.

Field Type Description
notifications boolean The master switch
chat_unread_mode string all, mentions, or mute
preferences object Event keys, each holding channel keys, each a boolean. Only the cells you send are changed
Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/settings/notifications \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"preferences": {"task_failed": {"discord": true}}}'

The response is the full read shape, with your change merged in.

An unknown event key or channel key is refused with 422 rather than stored. A typo would otherwise write a row nothing ever reads and raise no error anyone ever sees:

JSON
{
    "message": "The given data was invalid.",
    "code": "validation_failed",
    "errors": {
        "preferences.task_failed": ["Unknown channel: carrier_pigeon."]
    }
}

Read your billing details

The name and address printed on every invoice, plus the optional EU VAT number.

GET/api/v1/settings/billing-details

Your invoice name, address, VAT number, and whether the profile is complete.

API key required

Requires account.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/settings/billing-details \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "billing_is_business": true,
    "billing_first_name": "Ada",
    "billing_last_name": "Lovelace",
    "billing_company": "Analytical Engines BV",
    "billing_address_line_1": "12 Analytical Way",
    "billing_address_line_2": null,
    "billing_postal_code": "1011AB",
    "billing_city": "Amsterdam",
    "billing_state": null,
    "billing_country": "NL",
    "billing_vat_number": "NL123456789B01",
    "vat_number_valid": true,
    "vat_number_validated_at": "2026-07-30T14:02:11+00:00",
    "is_complete": true
}
Field Type Description
vat_number_valid boolean, null The cached VIES verdict. Read only. null means "not answered", either there was nothing to check or VIES was unreachable, and VAT is charged in both cases
vat_number_validated_at timestamp, null When that verdict was last refreshed
is_complete boolean Whether this profile carries everything an invoice needs. The top-up flow asks the same question before starting a checkout

Read is_complete rather than reimplementing the rule. It is the only question that actually matters, and a top-up refuses to start while it is false.

This is the one billing thing the API may write, and it is address data, not payment data. Balances, deposits, ledger and invoices are read only, on Billing. No endpoint anywhere can spend your balance or start a payment.

Update your billing details

PATCH/api/v1/settings/billing-details

Set the name, address, and VAT number your invoices are issued to.

API key required

Requires account.write.

Field Type Description
billing_is_business boolean Declares who is buying. It decides which fields apply, never what VAT is charged
billing_first_name string, max 100 Invoice name. May be left out, may not be blanked
billing_last_name string, max 100 Invoice name. May be left out, may not be blanked
billing_company string, max 150 Business customers only
billing_address_line_1 string, max 150 Street and number
billing_address_line_2 string, max 150 Optional second line
billing_postal_code string, max 20 Postal code
billing_city string, max 100 City
billing_state string, max 100 State or province, where one applies
billing_country string ISO 3166 alpha-2. Decides the VAT treatment
billing_vat_number string, max 20 EU VAT number with its country prefix, for example NL123456789B01

Bold fields are the ones an invoice cannot be issued without. Every field is optional in the request (this is a PATCH, so leaving one out keeps it), but a bold field sent as "" or null is refused with 422. An invoice is a legal document: it has to name someone and place them somewhere.

Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/settings/billing-details \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "billing_first_name": "Ada",
        "billing_last_name": "Lovelace",
        "billing_address_line_1": "12 Analytical Way",
        "billing_postal_code": "1011AB",
        "billing_city": "Amsterdam",
        "billing_country": "NL"
      }'

Three things happen on save that are worth knowing about:

  • Empty optional fields become null. A stored blank would print as an empty line on an invoice and read as present to every completeness check.
  • Turning billing_is_business off clears billing_company and billing_vat_number. A stale company name would keep printing as your legal name, and a stale VAT number would keep being offered to VIES.
  • The VAT number is re-checked against VIES on every save, not trusted from the previous verdict. The number and the country can both move in one request, and a stale "valid" would zero-rate a customer who no longer qualifies.

The VAT number's shape is validated first: it must parse, and its country prefix must match the billing country this patch leaves you with, not the one you had before it.

JSON
{
    "message": "The given data was invalid.",
    "code": "validation_failed",
    "errors": {
        "billing_vat_number": ["The VAT number must be registered in your billing country."]
    }
}

List notification destinations

Your connected external destinations: the Discord webhook, the Telegram bot, and your own webhook endpoint. This is the Notification destinations card, as a collection.

GET/api/v1/settings/notification-integrations

Your connected destinations, with every secret masked.

API key required

Requires account.read.

Parameter Type Description
type string discord, telegram, or custom_webhook
is_enabled boolean true or false
per_page integer Defaults to 50, capped at 200
Bash
curl "https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations?is_enabled=true" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "data": [
        {
            "id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
            "type": "discord",
            "channel": "discord",
            "is_enabled": true,
            "config": { "webhook_url": "••••tail" },
            "verified_at": "2026-07-28T11:20:04+00:00",
            "last_tested_at": "2026-07-30T14:02:11+00:00",
            "last_test_status": "completed",
            "last_test_error": null,
            "created_at": "2026-07-01T09:15:00+00:00",
            "updated_at": "2026-07-30T14:02:11+00:00"
        }
    ],
    "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1 }
}
Field Type Description
type string Which service this row is
channel string The matrix channel it feeds, so you can line a row up with preferences without knowing the mapping
is_enabled boolean Whether this destination is turned on. A row that is off delivers nothing and cannot be tested
config object The stored settings, with every credential masked
verified_at timestamp, null When a test send first succeeded. null means never proven to work
last_test_status string, null completed or failed, from the last test send
last_test_error string, null Why the last test failed, when it did

There is at most one row per type, so this collection is three rows on its worst day. It paginates anyway, because every collection on this API answers the same envelope. See Pagination and filtering.

Secrets are write-only

No response ever returns a stored credential in full. webhook_url, bot_token and url come back as •••• plus their last four characters, which is enough to recognize a value and not enough to use one. A Telegram chat_id reads back verbatim, because it is an address, not a secret.

The dashboard reveals a stored webhook URL to a session that just passed an interactive login. This surface answers a bearer token that may itself be the thing that leaked, and a readable webhook URL is a way to post as you into your own Discord forever. To change a value, send a new one.

Connect a notification destination

POST/api/v1/settings/notification-integrations

Connect Discord, Telegram, or your own webhook endpoint.

API key required

Requires account.write.

Field Type Description
type string discord, telegram, or custom_webhook
config object The settings for that type, from the table below
is_enabled boolean Defaults to true, so a destination works the moment it is connected

Config keys per type:

Type Key Rules
discord webhook_url A URL, max 500 characters. Create it in your Discord server settings
telegram bot_token Max 255 characters. From @BotFather
telegram chat_id Max 64 characters. Message your bot once to find it
custom_webhook url Must start with https://, max 500 characters

Keys the type does not define are ignored rather than rejected, so nothing extra can be smuggled into the stored config.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "type": "telegram",
        "config": { "bot_token": "8012345678:AAG_exampletoken", "chat_id": "99887766" }
      }'

Answers 201:

JSON
{
    "id": "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44",
    "type": "telegram",
    "channel": "telegram",
    "is_enabled": true,
    "config": { "bot_token": "••••oken", "chat_id": "99887766" },
    "verified_at": null,
    "last_tested_at": null,
    "last_test_status": null,
    "last_test_error": null,
    "created_at": "2026-07-30T14:02:11+00:00",
    "updated_at": "2026-07-30T14:02:11+00:00"
}

You get one row per type. A second POST for a type you already have is 409 integration_already_exists rather than a silent overwrite, because the caller either meant to PATCH the existing row or is about to destroy a webhook URL it cannot read back.

Read one destination

GET/api/v1/settings/notification-integrations/{id}

One connected destination.

API key required

Requires account.read.

Bash
curl https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations/019fb430-1c22-73a4-9f0e-2b7c5d1e8a44 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"

Same shape as one row of the listing, secrets masked the same way. An id that is not yours returns 404 not_found, exactly as an id that does not exist does.

Update a destination

PATCH/api/v1/settings/notification-integrations/{id}

Replace a destination's settings, or turn it on and off.

API key required

Requires account.write.

Field Type Description
is_enabled boolean Turn the destination on or off
config object Any of the keys that type defines. Merged over what is stored

config keys are merged, so a patch that only flips is_enabled keeps the credentials. Sending a key as "" removes it: an empty webhook URL is not a webhook URL.

Bash
curl -X PATCH https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations/019fb430-1c22-73a4-9f0e-2b7c5d1e8a44 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_enabled": false}'

Sending type is refused with 422, not ignored. There is one row per type, so "change the type" is really "delete this one and connect the other", and quietly dropping the field would let you believe you had done something.

Disconnect a destination

DELETE/api/v1/settings/notification-integrations/{id}

Disconnect a destination. Returns 204.

API key required

Requires account.write.

Bash
curl -X DELETE https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations/019fb430-1c22-73a4-9f0e-2b7c5d1e8a44 \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"

Your routing matrix is untouched. Cells pointing at that channel stay set and simply deliver nothing until you connect a destination again, which is what connected_channels on the notification settings endpoint reports.

Send a test notification

Sends the same test message the Send test button does, so you can prove a destination works before an event depends on it.

POST/api/v1/settings/notification-integrations/{id}/test

Queue a test send to one destination.

API key required

Requires account.write.

Bash
curl -X POST https://dashboard.steamlabs.dev/api/v1/settings/notification-integrations/019fb430-1c22-73a4-9f0e-2b7c5d1e8a44/test \
  -H "Authorization: Bearer $STEAMLABS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Answers 202, because the fleet does the sending:

JSON
{
    "task_id": "019fb457-d631-71a5-9042-d663a53bc51e",
    "accounts_affected": 0,
    "integration_id": "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44"
}

accounts_affected is 0 because no Steam account is involved. The envelope is the house 202 shape all the same, so you parse one thing everywhere.

Follow the outcome either through GET /api/v1/tasks/{id} or by re-reading the destination: last_test_status becomes completed or failed, last_test_error carries the reason on a failure, and verified_at is stamped on the first success.

Two refusals are specific to this endpoint. A destination that is turned off answers 409 integration_disabled: enable it first. And when the shared egress pool is momentarily empty you get 503 shared_pool_empty, which is worth retrying shortly. There is deliberately no fallback to sending from our own address, because that would leak the platform's IP to whatever endpoint you configured.

Errors

Beyond the universal codes, this group returns:

Code Status Meaning
integration_already_exists 409 You already have a destination of that type. Update it instead
integration_disabled 409 The destination is turned off, so it cannot be tested
shared_pool_empty 503 No shared egress proxy was free to send the test from. Retry shortly
maintenance_mode 503 Platform maintenance pauses test sends (they ride the fleet). Adds reason, see Errors

No endpoint on this page returns 403 plan_limit_reached. The boost concurrency ceiling is plan-derived, but exceeding it is a 422 on boost_max_concurrent, not a plan refusal, because you are being told about a bound you can read rather than sold an upgrade.

Everything else follows the standard shapes: 422 validation_failed with errors keyed by field, 404 not_found for an id that is not yours, 403 missing_scope for a key without account.read or account.write. See Errors.