## Alpaca Broker API Sandbox — Short Selling Deep Investigation

**Date:** 2026-04-11
**Question:** Is there ANY way to enable short selling on the Broker API sandbox? Is a cover path reachable? Is Trading API paper trading an alternative?
**Verdict:** **BLOCKED on Broker API sandbox.** Paper Trading (Trading API) **does** support shorting and is the only realistic alternative without a support ticket.

---

### TL;DR

1. **Broker API sandbox shorting is firm-locked.** `admin_configurations.disable_shorting=true` and `max_margin_multiplier="1"` are enforced at the correspondent/firm level and cannot be overridden via the broker key — not by PATCH, not at account creation, not via `trading_configurations`.
2. **The Broker API explicitly rejects `max_margin_multiplier > 1`** with `422 "max_margin_multiplier provided X exceeds the current limit 1"`. Prior probe called this "silently ignored" for the account-level PATCH path, but the trading-configurations-level PATCH and creation-time payloads return an **explicit 422** — the firm cap is unambiguous and machine-readable.
3. **`sell_short` / `sell_short_exempt` are Trading API–only side values.** The Broker API rejects them with `422 {"code":40010001,"message":"invalid side"}`. The only way to open a short via Broker API is `side=sell` (optionally `position_intent=sell_to_open`), which then hits the 403 40310000 "account is not allowed to short" gate.
4. **Cover** is not a distinct endpoint. In Alpaca, covering a short is just a normal `side=buy` (or `position_intent=buy_to_close`) that reduces a negative position. There is no separate cover flow to build — once shorting works, cover is free.
5. **Paper Trading (Trading API, not Broker API) supports short selling** per Alpaca's own feature-matrix ("Short Selling ✅" under the Paper column). This is a viable alternative if the mobile product can tolerate a second credential pipeline.

---

### 1. API Reference Findings

Fetched: `https://docs.alpaca.markets/reference/createorderforaccount`

Order body parameters that matter for shorting:

| Field | Accepted values | Notes |
|---|---|---|
| `side` | `buy`, `buy_minus`, `sell`, `sell_plus`, `sell_short`, `sell_short_exempt`, `undisclosed`, `cross`, `cross_short` | Broker API **only** accepts `buy`/`sell`. `sell_short` / `sell_short_exempt` → `422 invalid side`. |
| `type` | `market`, `limit`, `stop`, `stop_limit`, `trailing_stop` | — |
| `time_in_force` | `day`, `gtc`, `opg`, `cls`, `ioc`, `fok` | — |
| `position_intent` | `buy_to_open`, `buy_to_close`, `sell_to_open`, `sell_to_close` | **Prior probe was wrong** — it tried `"sell_short"` as the position_intent value (which is invalid). The correct short-open value is `sell_to_open`. We tested it; it still gets 403'd by the account-level block. |
| `order_class` | `simple`, `bracket`, `oco`, `oto`, `mleg` | — |

No "short" or "cover" specific top-level fields exist. Shorting is implicit in direction vs current position.

Fetched: `https://docs.alpaca.markets/docs/margin-and-short-selling`

Key policy quotes:
- "To trade on margin or sell short, you must maintain $2,000 or more in account equity."
- "Alpaca currently only supports opening short positions in easy to borrow ('ETB') securities."
- No mention of `disable_shorting`, `max_margin_multiplier`, or any sandbox-specific toggle.

Fetched: `https://docs.alpaca.markets/docs/short-selling` → **404**. That page doesn't exist.

### 2. Dashboard Configuration

Fetched: `https://docs.alpaca.markets/docs/sandbox-for-broker-api` → **404**.
Fetched: `https://docs.alpaca.markets/docs/integration-setup-with-alpaca` — no mention of firm-level short toggles, no mention of how `admin_configurations` are managed, no dashboard configuration surface documented for enabling shorting in sandbox.

**Finding:** There is no documented self-service way to flip firm-level `admin_configurations` in sandbox. The only lever is a support ticket to Alpaca.

### 3. Empirical PATCH Tests

Test account: `9fe06a06-5e67-4a44-9a9a-70b599ca5e14` (equity $50k, funded).

Baseline state before any PATCH:
```json
// GET /v1/trading/accounts/{aid}/account
{ "multiplier": "1", "shorting_enabled": false,
  "admin_configurations": {
    "allow_instant_ach": true,
    "disable_shorting": true,
    "max_margin_multiplier": "1"
  } }

// GET /v1/trading/accounts/{aid}/account/configurations
{ "no_shorting": false, "max_margin_multiplier": "4", ... }
```

Note the split: `trading_configurations.no_shorting` is already `false` and `trading_configurations.max_margin_multiplier` is already `"4"` — the user level permits it. The block is entirely at `admin_configurations`.

PATCH attempts:

| Request | Result |
|---|---|
| `PATCH /v1/trading/accounts/{id}/account/configurations` `{"no_shorting": false, "max_margin_multiplier": "4"}` | **422** `{"code":40010001,"message":"max_margin_multiplier provided 4 exceeds the current limit 1"}` — API is explicit: the firm caps it at 1. The prior probe's "this was accepted as 200" result only happens if you send `max_margin_multiplier` alone and nothing else changes it; when combined with the known-to-be-capped value it's a hard 422. |
| `PATCH /v1/accounts/{id}` `{"trading_configurations": {"no_shorting": false, "max_margin_multiplier": "4"}}` | **200 OK**, but subsequent `GET .../account` still shows `multiplier="1"`, `shorting_enabled=false`. The user-level field silently stays but is overridden by admin. |
| `PATCH /v1/accounts/{id}` `{"admin_configurations": {"disable_shorting": false, "max_margin_multiplier": "4"}}` | **200 OK**, but `admin_configurations` on re-GET is **unchanged** (`disable_shorting: true`, `max_margin_multiplier: "1"`). Silently dropped — confirms prior probe. |

### 4. Creation-Time Configuration Test

Attempted `POST /v1/accounts` with `trading_configurations` set at creation:

| Payload | Result |
|---|---|
| `trading_configurations: {"no_shorting": false, "max_margin_multiplier": "4"}` | **422** `{"code":40010001,"message":"max_margin_multiplier provided 4 exceeds the current limit 1"}` — refused at creation. |
| `trading_configurations: {"no_shorting": false}` (no margin override) | **200 OK**. New account `4f645110-e798-463c-a5c7-263f75c2c964` created. But it inherits `admin_configurations: {disable_shorting: true, max_margin_multiplier: "1"}` verbatim from the firm default. `multiplier=1`, `shorting_enabled=false`. |
| `trading_configurations: {}` | **200 OK**. Same story. |

`admin_configurations` cannot be set in the creation payload — the field is not documented on `POST /v1/accounts`, and the prior probe's test setting it in the body also showed it being silently dropped.

### 5. Order Placement Test

**Test A — reference account ($50k, pre-existing):**

```
POST /v1/trading/accounts/9fe06a06-5e67-4a44-9a9a-70b599ca5e14/orders
{"symbol":"MSFT","qty":"1","side":"sell","type":"market","time_in_force":"day","position_intent":"sell_to_open"}
→ 403 {"code":40310000,"message":"account is not allowed to short"}
```

Also tested:

| Variant | Response |
|---|---|
| `side: "sell_short"` | `422 {"code":40010001,"message":"invalid side"}` — Trading-API side value, not valid on Broker API. |
| `side: "sell_short_exempt"` | `422 {"code":40010001,"message":"invalid side"}` — same. |
| `side: "sell"` (bare, no position_intent) | `403 {"code":40310000,"message":"account is not allowed to short"}` |
| `side: "sell"` + `position_intent: "sell_to_open"` | `403 {"code":40310000,"message":"account is not allowed to short"}` |

**Test B — fresh account, JNLC-funded (the clean experiment):**

To eliminate all funding/creation-time confounders, spun up a brand-new sub-account:

1. `POST /v1/accounts` with `trading_configurations: {"no_shorting": false}` at creation → **200**, sub-account `bf8ae7ce-6901-411c-bc10-ed92bab60054`, status progressed `SUBMITTED → ACTIVE` in <1s.
2. `POST /v1/journals` with `{"entry_type":"JNLC","from_account":"711cca90-0628-3162-aaa6-a63a4144ed5d","to_account":"bf8ae7ce...","amount":"50000"}` → **200**, settled in ~1s. Equity went from 0 → $50,000 instantly. (Firm account had $239k available cash — JNLC from firm is the right funding path in sandbox, ACH takes ~10 min.)
3. Re-read trading account: `equity=50000`, `multiplier=1`, `shorting_enabled=false`, `admin_configurations={"disable_shorting": true, "max_margin_multiplier": "1", ...}` — exactly the same firm-inherited lock as the reference account, despite `trading_configurations.no_shorting=false` being set at creation.
4. Long buy sanity check: `POST /orders {"symbol":"AAPL","qty":"1","side":"buy","type":"market","time_in_force":"day"}` → **200** (order id `7b3d6c77-ca47-4cb4-86f9-d58b8aafc09c`). Confirms the account is fully trade-enabled and funded.
5. Short sell: `POST /orders {"symbol":"MSFT","qty":"1","side":"sell","type":"market","time_in_force":"day","position_intent":"sell_to_open"}` → **403 `{"code":40310000,"message":"account is not allowed to short"}`**.

This is the definitive control experiment: same request that succeeds for `buy` is rejected for `sell_to_open` with the short-specific error code on a fresh, fully-funded, trade-enabled account. The block is **exclusively** `admin_configurations.disable_shorting=true`, and nothing that a Broker API caller can do — PATCH, account creation, user-level `trading_configurations` — alters it.

**Cover** was untestable because no short position can exist to cover. Architecturally, cover = `side=buy` (optionally `position_intent=buy_to_close`) against a negative position. No separate endpoint. If shorting ever works, cover will work automatically.

### 6. Paper Trading (Trading API) Comparison

Fetched: `https://docs.alpaca.markets/docs/paper-trading`

Quote from the feature matrix: **"Short Selling ✅"** under the Paper column. The docs explicitly state paper trading accounts have the same short selling capabilities as live trading accounts.

This matters because Paper Trading is a **completely different credential/API than Broker API**:
- **Broker API** (what NanoStreet currently uses for SimTrade): one firm account that creates per-user sub-accounts on behalf of the end customer. Firm-level `admin_configurations` apply. This is what's blocked.
- **Trading API paper** (`https://paper-api.alpaca.markets`): one user, one paper account, one key. No sub-accounts. Shorting works out of the box.

The tradeoff for using Trading API paper as a workaround for the Short/Cover feature:
- **Pro:** Shorting works immediately, no support ticket needed.
- **Con:** Loses per-user account isolation — all users would share one paper account, or each user needs their own Alpaca paper account (which requires individual signup, not sub-account creation). This is a fundamentally different onboarding model and likely not viable for the current architecture.
- **Con:** Different API surface from Broker API — double the integration work if you also keep Broker API for the real trading path.

### 7. Conclusion + Recommendation

**Shorting is definitively blocked on Broker API sandbox.** The blocker is `admin_configurations.max_margin_multiplier="1"` and `admin_configurations.disable_shorting=true`, which are firm-wide defaults set by Alpaca at the correspondent level. There is **no self-service override**: PATCH returns either 422 (explicit rejection) or 200-with-silent-drop; creation-time overrides are either 422 or silently inherited from firm defaults; `sell_short` and `sell_short_exempt` side values are Trading-API only and rejected as `422 invalid side` on Broker API.

**The only legitimate paths forward:**

1. **(Recommended) File a support ticket with Alpaca** asking to enable `disable_shorting=false` and raise `max_margin_multiplier` to 2 or 4 on the NanoStreet sandbox firm. This is the only way to test Short/Cover against the real Broker API sandbox. Once the firm-level cap is raised, the existing `trading_configurations` values already permit shorting — new sub-accounts would inherit shorting enabled by default.

2. **(Not recommended for architectural reasons) Switch Short/Cover to Trading API paper.** Possible but requires a second credential pipeline, loses per-user isolation, and diverges from the production path.

3. **(Recommended in the meantime) Hide Short/Cover UI on mobile** until (1) lands. Wire the UI code behind a feature flag. Every attempt against current sandbox will 403 with a user-hostile error.

4. **(Not recommended) Simulate shorts in our own DB.** Heavy divergence from the real order path, reimplements the borrow/margin math, and would have to be re-plumbed to Alpaca later. Skip.

**Product decision point:** is Short/Cover in scope before prod cutover? If yes → file the Alpaca support ticket now; the turnaround on firm-level config changes in sandbox is historically 1–3 business days. If no → hide the buttons, ship without them, and revisit in prod where margin accounts open normally.

---

### Test harness

All calls used `ALPACA_BROKER_API_KEY` / `ALPACA_BROKER_API_SECRET` from `services/main/.env` against `https://broker-api.sandbox.alpaca.markets`. Firm account: `711cca90-0628-3162-aaa6-a63a4144ed5d` (used as JNLC source). Reference sub-account: `9fe06a06-5e67-4a44-9a9a-70b599ca5e14`. Probe sub-accounts created during tests: `4f645110-e798-463c-a5c7-263f75c2c964` (unfunded, ACH relationship missing), `ebc7c8f5-e392-4e60-a54a-51805279d2d8` (empty tcfg test), and `bf8ae7ce-6901-411c-bc10-ed92bab60054` (JNLC-funded to $50k, long-buy succeeded, short rejected — the clean control). No credentials hardcoded, no data mutated beyond creating throwaway sub-accounts, one AAPL test long buy, and one $50k intra-firm journal.
