> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trysoar.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> API keys, scopes, and what a 401 means that a 403 does not

Every request carries exactly one credential. For an integration you write
yourself, that is your API key in an `X-API-KEY` header:

```bash theme={null}
curl -s https://api.trysoar.com/v1/portfolio/balance \
  -H "X-API-KEY: $SOAR_API_KEY"
```

There is no request signing and no per-request HMAC. One header.

The API also accepts an **OAuth 2.0 bearer token** in `Authorization`. That is how an
approved application — [SOAR's MCP server](/mcp/overview), for one — acts for a user
who never hands over a key. Tokens are issued to registered clients only, so if you
are building your own bot, use an API key.

<Warning>
  Send one credential, never two. A request carrying both `X-API-KEY` and
  `Authorization` is `400 conflicting_credentials`. A caller presenting two
  identities does not know which one it means, and guessing on a trading request is
  the wrong kind of helpful.
</Warning>

One route needs no credential at all — the contract itself:

```bash theme={null}
curl -s https://api.trysoar.com/v1/openapi.yaml
```

That is the OpenAPI 3.1 document describing every endpoint on this API, and it is
the same file these docs render. An agent holding only the base URL can discover the
whole surface from it. A public contract behind a key would be a contradiction: you
would need a key to learn how to get a key.

## Creating a key

<Steps>
  <Step title="Sign in at app.trysoar.com">
    Keys are issued to your account, so this step happens as you, in the app — not
    as a bot. Open **Settings → Developer settings**.
  </Step>

  <Step title="Create a key with the scopes you need">
    Name it for the thing that will use it. Almost every trading integration wants
    both `read` and `trade`:

    ```json theme={null}
    { "name": "my-quoting-bot", "scopes": ["read", "trade"] }
    ```
  </Step>

  <Step title="Save the key immediately">
    The raw key is shown **once**. We store only a hash of it, so losing it means
    issuing a new one. We do keep a short prefix so you can tell your keys apart in
    a list.
  </Step>
</Steps>

You may hold up to **10 active keys** at a time. Revoke one to create another. A
revoked key stops working within 60 seconds.

<Note>
  Creating, listing, and revoking keys happens in the app, authenticated by your
  session rather than by a key — so no endpoint here can issue its own
  successor. Everything a bot does is under `https://api.trysoar.com/v1`.
</Note>

## Scopes

A key carries an explicit list of scopes. Each route requires exactly one.

| Scope | Grants |
| - | - |
| `read` | Market data, order and fill history, positions, balance |
| `trade` | Placing, amending, and canceling orders |

Those two are the whole vocabulary for a key, and both are yours to issue.

Bearer tokens carry finer-grained scopes instead, granted per application at consent
time — `soar:markets:read`, `soar:account:read`, `soar:orders:write`, and
`soar:orders:bulk-cancel`. Each endpoint in the **API Reference** names the one
it needs.

<Warning>
  **Scopes do not nest.** A `trade`-only key is rejected on reads — including
  reading back the order it just placed. This is the most common first-integration
  failure.

  If your bot places orders and then checks on them, it needs `["read", "trade"]`.
</Warning>

A key with an empty scope list is rejected everywhere.

## 401 versus 403

These are different problems with different fixes, and the distinction is worth
coding against.

| Status | Code | Meaning | What to do |
| - | - | - | - |
| `401` | `unauthenticated` | No key, or the key is unknown or revoked | Fix the credential. Retrying with the same key never succeeds. |
| `403` | `forbidden_scope` | The key is valid but lacks the scope this route needs | Reissue the key with that scope. Retrying is pointless. |
| `403` | `api_key_misconfigured` | The key's account reference is unusable | You cannot fix this from the client. Contact support. |

Whether a `WWW-Authenticate` header comes back depends on which credential failed.
A **bearer** failure carries one, because an OAuth client is expected to act on it: a
`401` names the protected-resource metadata URL, and a `403` names the scope that was
missing. An **API-key** failure carries none — there is no challenge-response step for
a key you already hold, and a browser that receives a challenge pops a native
credential dialog that would break the playground on this site.

See [Errors](/api-reference/errors) for the full code vocabulary.

## Rate limits

One token bucket per credential across the whole API: **250 requests/second
sustained, 500 burst**. A key gets its own bucket; a bearer token draws on one keyed
to the granting application and user. Every response to an authenticated request
carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`; a `429` adds `Retry-After`.

The limit is per **credential**, not per route — a busy market-data poller and your
order flow draw from the same bucket. Use separate keys if you want them isolated.

Details, and why a throttled cancel is the case to be careful about, are in
[Errors](/api-reference/errors#rate-limits).

## Handling keys

* Send the key in the `X-API-KEY` header only. Never in a URL or query string —
  URLs end up in logs, proxies, and browser history.
* Revoke a key the moment it is no longer used, and whenever you suspect exposure.
* A leaked `trade` key can move your positions. The venue trades paper money today;
  that is not a reason to be casual with a credential whose habits you will carry
  forward.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.