# Authentication

> Create an API key, choose what it can reach, and keep it out of trouble.


Every request carries a bearer token. Keys are created and revoked from the **API keys** page under **Your account** in the dashboard.

## Create a key

1. Open **API keys** under **Your account**.
2. Press **New API key**.
3. Give it a **name**. This is just a label, so make it the thing you would search for later: "Discord bot", "Nightly sync", "Laptop testing".
4. Choose its **permissions**. Pick only what the key needs, or flip **Full access** if it is your own tooling and you want everything.
5. Optionally set an **expiry** and a per-key **rate limit**.
6. Press **Create**.

Your key appears once:

```
slabs_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8S9t0U1v2
```

> [!WARNING]
> Copy it now. We store a SHA-256 hash of your key, never the key itself, so we genuinely cannot show it to you again or recover it if you lose it. If that happens, revoke the key and create another.

## Use a key

Send it in the `Authorization` header on every request.

```bash tab=curl
curl https://dashboard.steamlabs.dev/api/v1/accounts \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
```

```php tab=PHP
$accounts = Http::withToken($apiKey)
    ->get('https://dashboard.steamlabs.dev/api/v1/accounts')
    ->json('data');
```

```javascript tab=Node
const response = await fetch('https://dashboard.steamlabs.dev/api/v1/accounts', {
    headers: { Authorization: `Bearer ${apiKey}` },
});

const { data } = await response.json();
```

```python tab=Python
import requests

accounts = requests.get(
    "https://dashboard.steamlabs.dev/api/v1/accounts",
    headers={"Authorization": f"Bearer {api_key}"},
).json()["data"]
```

For write requests, add `Content-Type: application/json` and send a JSON body.

## Permissions

Permissions are named `resource.verb`. A key holds a list of them, and a request is refused if the key does not hold the one its endpoint needs.

| Resource | Read | Write |
| --- | --- | --- |
| Account | `account.read` | `account.write` |
| Steam accounts | `accounts.read` | `accounts.write` |
| Tasks | `tasks.read` | `tasks.write` |
| Proxies | `proxies.read` | `proxies.write` |
| Profiles | `profiles.read` | `profiles.write` |
| Inventory | `inventory.read` | `inventory.write` |
| Market | `market.read` | `market.write` |
| Trading | `trading.read` | `trading.write` |
| Trade-ups | `tradeups.read` | `tradeups.write` |
| Hour boosting | `boost.read` | `boost.write` |
| Integrations | `integrations.read` | `integrations.write` |
| Billing | `billing.read` | none, billing is read-only |
| Activity | `activity.read` | none |
| Statistics | `stats.read` | none |

A read permission never lets a key change anything. If a key only needs to report on your farm, give it read permissions and nothing else: that key is then safe to paste into a script, a dashboard, or a shared machine.

Every endpoint's page lists the permission it needs.

### Full access

A key can instead be marked **Full access**, which grants every permission including ones added to the API later. Convenient for your own tooling. Worth thinking twice about for anything running somewhere you do not control.

`GET /api/v1/me` reports `"scopes": ["*"]` for a full-access key.

## Expiry

A key can carry an expiry date. Past it, every request returns `401` and the key shows as **Expired** in the dashboard. Leave the field empty for a key that never expires.

## Revoking

Press **Revoke** on the key's row. It stops working immediately.

Revoked keys stay in your list rather than disappearing, so you can still see what the key was called and when it was last used. That matters when you are working out what a leaked key touched.

> [!IMPORTANT]
> Revoke first, investigate second. A key you are unsure about is a key worth killing: creating a replacement takes ten seconds.

## Keeping keys safe

- Treat a key like a password. It acts as you, within its permissions.
- Keep keys in environment variables or a secret manager, never in source control.
- Give each integration its own key. Then revoking one does not break the others, and `last_used_at` tells you which is which.
- Scope keys down. Most integrations need far less than full access.
- Rotate anything that has been on a shared machine, in a screenshot, or in a support thread.

## Error responses

| Status | Code | Meaning |
| --- | --- | --- |
| `401` | `missing_api_key` | No `Authorization` header |
| `401` | `invalid_api_key` | Unknown, expired, or revoked key |
| `403` | `account_not_entitled` | Your account cannot use the API |
| `403` | `missing_scope` | The key lacks the permission this endpoint needs |
| `429` | `rate_limit_exceeded` | Too many requests. See [Rate limits](/docs/api/en/concepts/rate-limits) |

A missing permission tells you which one it wanted:

```json
{
    "message": "This API key is missing the required scope: accounts.write.",
    "code": "missing_scope",
    "required_scope": "accounts.write"
}
```

Unknown, expired, and revoked keys all return the same message on purpose. Telling a caller that a stolen key is merely expired tells them the account exists and the key was once real.
