Summary
The V1 batch order endpoints have two serious correctness defects that make them unsafe for production:
(1) batch_create crashes on partial-success responses
Spec BatchCreateOrdersIndividualResponse declares both order and error as nullable. A failed leg comes back as {"client_order_id": "x", "order": null, "error": {...}}. The SDK does:
return [Order.model_validate(o.get("order", o)) for o in raw_orders]
dict.get("order", o) returns None when the key is present with value None (the default is only used on missing keys). So Order.model_validate(None) raises pydantic.ValidationError and tears down the entire batch result the moment any single leg is rejected.
Even on the happy path the SDK discards client_order_id and error, so callers cannot pair returned orders with the original requests or detect a degraded fill ordering. Documented batch examples in docs/resources/orders.md show iterating for o in created — that loop blows up the moment one leg is rejected.
(2) batch_cancel discards the entire response body
Spec BatchCancelOrdersResponse.orders[] carries per-entry order_id, reduced_by_fp (required, Decimal — load-bearing for risk reconciliation), order, and a nullable error. The SDK's batch_cancel is typed -> None and never reads the body returned from _delete_with_body:
self._delete_with_body("/portfolio/orders/batched", json=body) # return discarded
A trader who batch-cancels 20 orders has no way to tell which ones actually canceled, which were no-ops (already-filled), or how many contracts each cancel reduced.
V2 already does this correctly: batch_cancel_v2 parses and returns BatchCancelOrdersV2Response. V1 was simply left behind.
Location
kalshi/resources/orders.py:454-464 (sync batch_create), :902-912 (async)
kalshi/resources/orders.py:477-500 (sync batch_cancel), :925-948 (async)
specs/openapi.yaml:6876-6889 (BatchCreateOrdersIndividualResponse)
specs/openapi.yaml:6936-6959 (BatchCancelOrdersIndividualResponse)
Evidence
# kalshi/resources/orders.py:462-464
data = self._post("/portfolio/orders/batched", json=body)
raw_orders = data.get("orders", [])
return [Order.model_validate(o.get("order", o)) for o in raw_orders] # Order.model_validate(None) on failed leg
# kalshi/resources/orders.py:498-500
self._require_auth()
body = _build_batch_cancel_body(request, orders)
self._delete_with_body("/portfolio/orders/batched", json=body) # body discarded
Recommended fix
Add typed response models (mirror the V2 shape):
class BatchCreateOrdersResponseEntry(BaseModel):
order: Order | None
error: ErrorResponse | None
client_order_id: str | None
class BatchCreateOrdersResponse(BaseModel):
orders: list[BatchCreateOrdersResponseEntry]
class BatchCancelOrdersResponseEntry(BaseModel):
order_id: str
reduced_by_fp: FixedPointCount
order: Order | None
error: ErrorResponse | None
class BatchCancelOrdersResponse(BaseModel):
orders: list[BatchCancelOrdersResponseEntry]
Change batch_create to return BatchCreateOrdersResponse and batch_cancel to return BatchCancelOrdersResponse. Wire data = self._delete_with_body(...) into batch_cancel.
Breaking change for batch_create (return type changes from list[Order] to BatchCreateOrdersResponse) and batch_cancel (return type from None to BatchCancelOrdersResponse). Land both in the same release; either bump to v3.0 or ship under a deprecation cycle with V2-style *_v2 variants for V1 endpoints.
Add regression tests:
test_batch_create_partial_failure_returns_per_leg_results — mock response with 2 ok / 1 error legs; assert structure preserved.
test_batch_cancel_returns_reduced_by_fp_per_order — mock response, assert Decimal precision preserved.
Severity & category
high / correctness, spec-drift, breaking
Summary
The V1 batch order endpoints have two serious correctness defects that make them unsafe for production:
(1)
batch_createcrashes on partial-success responsesSpec
BatchCreateOrdersIndividualResponsedeclares bothorderanderroras nullable. A failed leg comes back as{"client_order_id": "x", "order": null, "error": {...}}. The SDK does:dict.get("order", o)returnsNonewhen the key is present with valueNone(the default is only used on missing keys). SoOrder.model_validate(None)raisespydantic.ValidationErrorand tears down the entire batch result the moment any single leg is rejected.Even on the happy path the SDK discards
client_order_idanderror, so callers cannot pair returned orders with the original requests or detect a degraded fill ordering. Documented batch examples indocs/resources/orders.mdshow iteratingfor o in created— that loop blows up the moment one leg is rejected.(2)
batch_canceldiscards the entire response bodySpec
BatchCancelOrdersResponse.orders[]carries per-entryorder_id,reduced_by_fp(required, Decimal — load-bearing for risk reconciliation),order, and a nullableerror. The SDK'sbatch_cancelis typed-> Noneand never reads the body returned from_delete_with_body:A trader who batch-cancels 20 orders has no way to tell which ones actually canceled, which were no-ops (already-filled), or how many contracts each cancel reduced.
V2 already does this correctly:
batch_cancel_v2parses and returnsBatchCancelOrdersV2Response. V1 was simply left behind.Location
kalshi/resources/orders.py:454-464(syncbatch_create),:902-912(async)kalshi/resources/orders.py:477-500(syncbatch_cancel),:925-948(async)specs/openapi.yaml:6876-6889(BatchCreateOrdersIndividualResponse)specs/openapi.yaml:6936-6959(BatchCancelOrdersIndividualResponse)Evidence
Recommended fix
Add typed response models (mirror the V2 shape):
Change
batch_createto returnBatchCreateOrdersResponseandbatch_cancelto returnBatchCancelOrdersResponse. Wiredata = self._delete_with_body(...)intobatch_cancel.Breaking change for
batch_create(return type changes fromlist[Order]toBatchCreateOrdersResponse) andbatch_cancel(return type fromNonetoBatchCancelOrdersResponse). Land both in the same release; either bump to v3.0 or ship under a deprecation cycle with V2-style*_v2variants for V1 endpoints.Add regression tests:
test_batch_create_partial_failure_returns_per_leg_results— mock response with 2 ok / 1 error legs; assert structure preserved.test_batch_cancel_returns_reduced_by_fp_per_order— mock response, assertDecimalprecision preserved.Severity & category
high / correctness, spec-drift, breaking