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

# Authentication

> How to authenticate with the River Markets WebSocket API

WebSocket connections use the same Ed25519 keypair as the REST API — but
because browsers cannot set custom headers on the WS upgrade, the three
signing values move from headers into query params.

<Tip>
  The official SDKs sign the handshake for you. The raw recipe below is for
  integrations without an SDK.
</Tip>

## Query parameters

| Param    | Value                                                 |
| -------- | ----------------------------------------------------- |
| `key_id` | Your key UUID                                         |
| `ts`     | Current unix seconds (within 30s of server time)      |
| `sig`    | Base64 Ed25519 signature over the WS canonical string |

## Canonical string

LF-joined (no trailing newline). The method is the literal `"WS"` and there is
no body to hash:

```
WS\n
PATH\n
SORTED_QUERY (excluding key_id/ts/sig)\n
TIMESTAMP
```

`SORTED_QUERY` is everything **except** the three signing params, with keys
sorted alphabetically and values percent-encoded per RFC 3986. Empty when the
endpoint has no additional query string.

## Example

```python theme={null}
import asyncio, base64, time
from urllib.parse import urlencode, quote
import websockets
from nacl.signing import SigningKey

KEY_ID = "<uuid from Settings → API Keys>"
PRIVATE_KEY_B64 = "<base64 private key shown once at creation>"

signing_key = SigningKey(base64.b64decode(PRIVATE_KEY_B64))


def sign_ws(path: str, extra_params: dict | None = None) -> str:
    """Return a fully-formed wss:// URL with signing query params appended."""
    params = dict(extra_params or {})
    # quote_via=quote encodes spaces as %20 to match RFC 3986 / the server.
    sorted_q = urlencode(sorted(params.items()), quote_via=quote)
    ts = str(int(time.time()))
    canonical = "\n".join(["WS", path, sorted_q, ts]).encode()
    sig = base64.b64encode(signing_key.sign(canonical).signature).decode()
    params.update({"key_id": KEY_ID, "ts": ts, "sig": sig})
    return f"wss://api.rivermarkets.com{path}?{urlencode(params)}"


async def stream_orders(subaccount_id: str):
    url = sign_ws("/v1/ws/orders", {"subaccount_id": subaccount_id})
    async with websockets.connect(url) as ws:
        async for frame in ws:
            print(frame)


asyncio.run(stream_orders("<subaccount-uuid>"))
```

## Close codes

Invalid or missing auth closes the connection with code `4401`. See each
endpoint's reference page for endpoint-specific close codes.

<Warning>
  Never share your private key or commit it to source control. Use environment
  variables or a secret manager.
</Warning>
