Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 79 additions & 11 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
Kalshi uses RSA-PSS request signing. You'll need:

- A **key ID** (string, identifies the key on Kalshi's side).
- A **private key** — RSA, PEM-encoded.
- A **private key** — RSA, **PEM-encoded, PKCS#8, unencrypted**.

Generate the pair in your [Kalshi account settings](https://kalshi.com/account/profile)
and download the PEM file. The signing scheme used internally is
RSA-PSS / SHA256 / MGF1(SHA256) / salt = digest length / base64 — you don't need
to implement any of that yourself; the SDK does it for you.

You can also mint keys programmatically once authenticated; see
[API keys](resources/api-keys.md).

## Option 1 — Key file path (most common)

```python
Expand All @@ -22,7 +25,8 @@ with KalshiClient(
...
```

`~` is expanded for you. Pass a `pathlib.Path` or a string.
`~` is expanded for you. Pass a `pathlib.Path` or a string. The constructor is
keyword-only; an empty `key_id` raises `ValueError` immediately.

## Option 2 — Environment variables

Expand All @@ -46,10 +50,11 @@ with KalshiClient.from_env() as client:
```

If `KALSHI_KEY_ID` / `KALSHI_PRIVATE_KEY_PATH` are unset, `from_env()` returns
an **unauthenticated** client. Public endpoints still work; private endpoints
raise `AuthRequiredError`.
an **unauthenticated** client. Public endpoints still work. See
[Environment variables](environment-variables.md) for the full table and
precedence rules.

## Option 3 — In-memory PEM
## Option 3 — In-memory PEM (env var)

If you store the private key in a secret manager (Vault, AWS Secrets Manager,
GCP Secret Manager, …), set `KALSHI_PRIVATE_KEY` to the PEM contents:
Expand All @@ -61,7 +66,24 @@ MIIEv...
```

Then `KalshiClient.from_env()` will load the key directly without touching the
filesystem.
filesystem. `KALSHI_PRIVATE_KEY` takes precedence over `KALSHI_PRIVATE_KEY_PATH`
when both are set.

## Option 4 — In-memory PEM (constructor)

You can also pass the PEM string straight to the constructor:

```python
from kalshi import KalshiClient

pem = secret_manager.get("kalshi/private_key") # str returning the PEM body

with KalshiClient(key_id="...", private_key=pem) as client:
...
```

The constructor accepts both `private_key_path=` and `private_key=`; supply
exactly one.

## Demo vs. production

Expand Down Expand Up @@ -101,7 +123,7 @@ asyncio.run(main())

## Public / unauthenticated usage

You don't need credentials for public market data:
You don't need credentials for most public market data:

```python
from kalshi import KalshiClient
Expand All @@ -111,9 +133,16 @@ with KalshiClient(demo=True) as client:
markets = client.markets.list(status="open", limit=5)
```

Any private endpoint call on an unauthenticated client raises
`AuthRequiredError` (a subclass of `KalshiAuthError`) immediately, before
hitting the network.
A handful of "public-looking" endpoints still require auth at the server
(`markets.orderbook`, `markets.bulk_orderbooks`,
`series.forecast_percentile_history`, `exchange.user_data_timestamp`).
Calling those on an unauthenticated client raises
[`AuthRequiredError`](errors.md) preflight — no network round-trip.

If you instead call a private endpoint with the wrong scope or expired
credentials, the server returns 401/403 and the transport maps it to
[`KalshiAuthError`](errors.md). `AuthRequiredError` is a subclass of
`KalshiAuthError`, so catching the parent covers both.

## Direct `KalshiAuth` usage

Expand All @@ -126,8 +155,47 @@ from kalshi import KalshiAuth
# From a key file
auth = KalshiAuth.from_key_path("your-key-id", "~/.kalshi/private_key.pem")

# From the environment (returns None if unset; use from_env() to raise instead)
# From a PEM string already in memory
auth = KalshiAuth.from_pem("your-key-id", pem_string)

# From the environment — raises if vars are missing
auth = KalshiAuth.from_env()

# From the environment — returns None on missing creds, but still raises
# KalshiAuthError if vars are set but malformed
maybe_auth = KalshiAuth.try_from_env()
```

### Key format constraints

`from_pem` and `from_key_path` are strict about format. If your key fails to
load, check:

- **Must be RSA.** EC, Ed25519, DSA keys are rejected.
- **Must be PKCS#8** (`-----BEGIN PRIVATE KEY-----`). Legacy PKCS#1
(`-----BEGIN RSA PRIVATE KEY-----`) is supported by the underlying
`cryptography` library on a best-effort basis.
- **Must be unencrypted.** Passphrase-protected keys raise `KalshiAuthError`
with a hint. Strip the passphrase with `openssl pkey`:

```bash
openssl pkey -in encrypted.pem -out unencrypted.pem
```

### Manual signing

`KalshiAuth.sign_request(method, path, timestamp_ms=None)` is part of the
public API for callers building custom transports. The path is the URL path
only — query string stripped, trailing slash stripped (except for the literal
`/`), with percent-encoded sequences normalized to uppercase hex per RFC 3986.

```python
from kalshi import KalshiAuth

auth = KalshiAuth.from_env()
headers = auth.sign_request("GET", "/trade-api/v2/exchange/status")
# headers = {"KALSHI-ACCESS-KEY": ..., "KALSHI-ACCESS-SIGNATURE": ...,
# "KALSHI-ACCESS-TIMESTAMP": ...}
```

See the [API reference](reference.md) for the full surface.
141 changes: 141 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Concepts

Short glossary of the Kalshi domain objects you'll hit. Each link goes to the
resource page that operates on them.

## Series, event, market

The three-level ticker hierarchy:

- **Series** — a recurring family (e.g. `KXPRES` covers presidential elections).
Series tickers look like `KXPRES`. See [Series](resources/series.md).
- **Event** — one instance of a series (e.g. `KXPRES-24`, the 2024 election).
See [Events](resources/events.md).
- **Market** — a single YES/NO contract under an event (e.g. `KXPRES-24-DJT`,
"will Trump win"). See [Markets](resources/markets.md).

Every market belongs to exactly one event; every event belongs to exactly one
series.

## YES, NO, side, action

A Kalshi market trades two complementary contracts that always sum to $1:
**YES** and **NO**. Each trade has a `side` (which contract) and an `action`:

- `side="yes"`, `action="buy"` — buying YES.
- `side="yes"`, `action="sell"` — selling YES.
- `side="no"`, `action="buy"` — buying NO (equivalent to selling YES against
the orderbook, but accounted separately).

These are `Literal` types — see [Types & literals](types.md).

## Prices

Prices live in `[0.00, 1.00]` and represent dollars (a YES at $0.65 implies a
65% market-implied probability). Always pass them as strings or `Decimal`:

```python
order = client.orders.create(..., yes_price="0.65")
order = client.orders.create(..., yes_price=Decimal("0.65"))
```

Float is a footgun; the SDK accepts it but normalizes through `str()` to avoid
`0.65 → 0.6499999…`. See [`DollarDecimal`](types.md).

The Kalshi API returns prices as JSON strings with a `_dollars` suffix
(`yes_bid_dollars: "0.5600"`). The SDK maps these to short field names
(`yes_bid: Decimal("0.5600")`). Both directions round-trip.

## Cents vs dollars

A few fields are **integer cents**, not dollars:

- `Balance.balance` / `portfolio_value` — cents.
- `CreateOrderRequest.buy_max_cost` — cents.
- `ApplySubaccountTransferRequest.amount_cents` — cents.

These are typed `int`. Passing a `Decimal` or `float` raises `ValueError` at
construction. The rule: anything with `_cents` / `buy_max_cost` is cents;
anything with `_dollars` or `yes_price` / `no_price` is `Decimal` dollars.

## Order, fill, position, settlement

- **Order** — your intent. Has a status (`resting`, `canceled`, `executed`).
- **Fill** — an actual execution against an order. One order can produce many
fills.
- **Position** — your aggregate exposure on a market (signed by side).
- **Settlement** — what the exchange paid out when the market resolved.

See [Orders](resources/orders.md), [Portfolio](resources/portfolio.md).

## RFQ and Quote

**RFQ** ("Request For Quote") — a private "Can someone make me a market on this
contract at this size?" message. **Quote** — a counterparty's answer ("I'll sell
you YES at $0.62 / buy from you at $0.60"). The requester picks a side and
accepts; the maker confirms.

This is Kalshi's bilateral block-trade rail, alongside the public order book.
See [Communications](resources/communications.md).

## Multivariate event collection

A template for combo bets: "Will it rain in NYC AND the Yankees win Saturday?"
The collection holds the building-block markets; calling `create_market` with a
list of leg selections mints a derived YES/NO contract.

See [Multivariate](resources/multivariate.md).

## Milestone

A real-world reference event a market is anchored to — a baseball game, an
economic release, an election. Live data (scores, clocks, weather) is keyed by
milestone id.

See [Milestones](resources/milestones.md), [Live data](resources/live-data.md).

## Structured target

The entity a market is "about" — a team, player, candidate, company. Two
markets pointing at the same Yankees roster share a structured target id, so
you can group markets by underlying entity.

See [Structured targets](resources/structured-targets.md).

## Incentive program

Time-boxed reward campaigns (maker rebates, volume bonuses) on specific
markets or series.

See [Incentive programs](resources/incentive-programs.md).

## Subaccount

A logical wallet partition under your main account. Used to isolate strategies
or risk pools. Subaccount `0` is your primary account; `1`–`32` are numbered
extras. Most resource methods accept a `subaccount=` kwarg to route the call.

See [Subaccounts](resources/subaccounts.md).

## Order group

A rolling contracts-limit bucket that several orders can share. Trips when the
group hits its cap; can be `reset()` or `trigger()`ed manually.

See [Order groups](resources/order-groups.md).

## FCM

**Futures Commission Merchant.** Kalshi-side broker designation. FCM members
get a separate `/fcm/*` surface that takes a `subtrader_id` discriminator and
returns the same shapes as `portfolio.*`. Non-FCM accounts get 401/403.

See [FCM](resources/fcm.md).

## API key, scope

Authentication identity. Has `read` and `write` scopes; `write` requires
`read`. Can be created via the web UI or via
`client.api_keys.generate()` / `client.api_keys.create()`.

See [API keys](resources/api-keys.md).
Loading
Loading