Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
RPC Troubleshooting12 min read

Hyperliquid API Error Handling: Decoding Order Rejections, Margin, and Oracle Checks

Decode Hyperliquid /info and /exchange order rejections: margin, risk limits, open interest caps, oracle price checks, and post-only guardrails.

TL;DR

Hyperliquid's REST API separates read-only /info from signed /exchange actions. When an order fails, the response includes an order status of 'rejected' with a human-readable reason and numeric codes, or an 'error' object. This article explains the mechanism behind margin, risk, open interest, oracle price, and execution-mode rejections, and provides a reproducible client to capture and decode them.

Direct Answer: Where Order Rejections Live in the Hyperliquid API

When you submit an order to Hyperliquid's signed /exchange endpoint, the API does not throw an HTTP error for business-level failures. Instead, the response contains an order statuses array. Each element corresponds to an order you sent; a failed order has status: "rejected" and a statuses_reason string such as InsufficientMargin or PriceOutOfBand. For wrap failures (e.g., malformed request or a trading ban), the response is a JSON object with a response string like "Order price is above the maximum allowed price for this asset" or an error object. This is documented in the Hyperliquid docs and is distinct from HTTP 429 rate-limit errors covered in our Hyperliquid API rate limits guide.

The key insight: you must inspect the order status and reason, not just the HTTP status code. A 200 response can still contain a rejected order. This article decodes the rejection families you will encounter in production and shows you how to handle them programmatically.

Mechanism: How Hyperliquid Validates Orders Before Acceptance

Hyperliquid's matching engine performs a series of checks before an order can rest on the book or fill. These checks are enforced server-side and are separate from client-side validation. The engine evaluates your account equity, current positions, leverage, open interest, and the oracle price. If any check fails, the order is rejected with a specific reason.

The read-only /info endpoint provides the context you need to pre-validate: /info/meta returns asset metadata including maxLeverage, szDecimals, and maxSz; /info/clearinghouseState gives your account equity and margin summary; /info/assetCtxs provides the current oracle price and open interest. The signed /exchange endpoint then applies the final checks atomically.

Hyperliquid's documentation lists the error codes and reasons in the errors section. Independent integrations like the Hyperliquid Python SDK and ccxt map these to exceptions, but understanding the raw strings is essential for robust automation.

Rejection Families and Their Meaning

Production clients see a handful of rejection families. Each maps to a specific reason string or response text. The table below summarizes them; the exact strings are documented in the Hyperliquid docs and may evolve.

Margin and equity checksInsufficientMargin or InsufficientAccountValue indicate your account lacks enough equity or margin to support the order's notional. This often happens when you increase a position without adding funds, or when unrealized losses reduce equity.

Risk and leverage checksRiskLimits or MaxPositionSize mean the order would exceed your risk limit or the maximum position size for the asset. Leverage too high appears when you request more leverage than the asset's maxLeverage from /info/meta.

Open interest caps – The reason Cannot increase position when open interest is at cap (or similar) appears when the asset's open interest has reached its cap. This is a market-wide protection, not an account-level issue.

Oracle price checksPriceOutOfBand or the response string "Order price is above the maximum allowed price for this asset" indicates your limit price is too far from the current oracle price. Hyperliquid enforces a maximum offset to prevent bad prints.

Execution-mode guardrailsPostOnlyWouldTakeLiquidity means your post-only order would have matched an existing order, so it was rejected to preserve maker-only intent. Reduce-only violations occur when an order would increase a position instead of reducing it.

A crucial practical distinction exists between an order that is rejected outright and one that is accepted but later fails. When Hyperliquid's API returns a rejection, the order-status entry in the response array indicates that the order was never placed; any existing resting or working state for that order remains unchanged. In contrast, a successful submission results in a 'resting' or 'filled' status, and for multi-leg or partial fills, the array may contain multiple entries per leg. Therefore, a caller must not treat the array as all-or-nothing: a single rejection does not imply that other legs were not executed, and a partial fill does not mean the entire order was rejected. This granularity is essential for accurate bookkeeping and for deciding whether to retry, cancel, or adjust remaining legs.

  • Always check the statuses_reason field for per-order failures.
  • For wrap errors, parse the response string or error object.
  • Do not rely on HTTP status codes; a 200 can contain a rejected order.

Reproducible Client: Capture and Decode a Rejection

The following Python script uses the official Hyperliquid SDK to submit an order with an intentionally impossible price (e.g., 10% above the oracle) to trigger a PriceOutOfBand rejection. It prints the order status, reason, and numeric code. Replace the private key with a testnet key and use the testnet API URL https://api.hyperliquid-testnet.xyz as described in the Hyperliquid docs.

This script is safe because it uses a testnet and an impossible price, so no real funds are at risk. Never run such tests on mainnet with real balances.

import json
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from eth_account import Account

# Testnet configuration - replace with your testnet private key
account = Account.from_key('0x...')  # your testnet private key
base_url = 'https://api.hyperliquid-testnet.xyz'
info = Info(base_url, skip_ws=True)
exchange = Exchange(account, base_url)

# Fetch asset metadata and oracle price
meta = info.meta()
asset = 'BTC'
asset_index = next(i for i, a in enumerate(meta['universe']) if a['name'] == asset)
oracle_price = float(info.all_mids()[asset])

# Intentionally impossible price: 10% above oracle
bad_price = round(oracle_price * 1.10, 1)

# Build and submit order
order = {
    "coin": asset,
    "is_buy": True,
    "sz": 0.001,
    "limit_px": bad_price,
    "order_type": {"limit": {"tif": "Gtc"}},
    "reduce_only": False
}
result = exchange.order(order)
print(json.dumps(result, indent=2))

# Expected output (testnet, may vary):
# {
#   "status": "ok",
#   "response": {
#     "type": "order",
#     "data": {
#       "statuses": [
#         {
#           "resting": {"oid": 123456},
#           "status": "rejected",
#           "statuses_reason": "PriceOutOfBand"
#         }
#       ]
#     }
#   }
# }

Results Table: Map Rejection Reasons to Actions

Use the table below as a starting point. The exact reason strings are documented in the Hyperliquid docs; verify against your SDK version. Fill in the 'Observed on your setup' column when you run the client.

  • | Reason/Response | Family | Recommended Action | Observed on your setup |
  • |---|---|---|---|
  • | InsufficientMargin | Margin | Reduce size or add funds; check equity via /info/clearinghouseState | |
  • | RiskLimits | Risk | Reduce position size or wait for risk limit reset | |
  • | Cannot increase position when open interest is at cap | Open interest | Cancel and wait; consider a different asset | |
  • | PriceOutOfBand | Oracle | Clamp price to within the allowed band around oracle | |
  • | PostOnlyWouldTakeLiquidity | Execution mode | Cancel and resubmit as a marketable order or adjust price | |
  • | ReduceOnly would increase position | Execution mode | Check your reduce_only flag and current position | |

Failure/Fix Checklist: Handling Rejections in Production

When your bot receives a rejection, follow this checklist to decide the next action. The goal is to avoid blind retries that could amplify losses or trigger repeated rejections.

1. Parse the rejection reason. Extract statuses_reason from each order status. For wrap errors, parse the response string.

2. Pre-check margin and notional. Before submitting, query /info/clearinghouseState to confirm available margin. Compare the order's notional against your equity and the asset's maxLeverage from /info/meta.

3. Clamp to the oracle band. If you get PriceOutOfBand, fetch the current oracle price from /info/assetCtxs and set your limit price within the allowed offset (e.g., 5% for most assets, but check the docs).

4. Check post-only and reduce-only flags. If you receive PostOnlyWouldTakeLiquidity, either cancel and resubmit as a regular limit order or adjust your price to the other side of the spread. For reduce-only violations, verify your current position and the order side.

5. Resubmit with a new price or cancel. For price-related rejections, resubmit with a corrected price. For margin or risk rejections, do not retry until you adjust your account or order size.

6. Log and alert. Record the full rejection payload for debugging. Use structured logging to track rejection frequency by reason.

For production robustness, consider a conservative retry-and-raise strategy that avoids unnecessary risk. First, clamp your client-side notional to the current mark-price band before submission, as documented in Hyperliquid's API guidelines, to reduce the likelihood of PriceOutOfBand rejections. If a PriceOutOfBand rejection still occurs, cancel any resting orders and resubmit with an updated price within the band. For all other hard rejections—such as those related to margin, risk, or open interest—do not silently retry; instead, map them to a local alert or error log for manual review. This approach ensures that transient issues are handled automatically while persistent problems are escalated, and it aligns with Hyperliquid's documented behavior. Verify these recommendations against the official documentation and your own testing to tailor them to your specific use case.

Limitations and Tradeoffs

Hyperliquid's rejection reasons are human-readable but not guaranteed to be stable across protocol upgrades. The docs note that error codes may change; always handle unknown reasons gracefully.

The oracle price band is not a fixed percentage for all assets; it can vary. Check the meta and assetCtxs for each asset, and do not hardcode a universal offset.

Open interest caps are dynamic and can change as positions are opened and closed. A rejection due to open interest cap may be temporary; a retry after a short delay might succeed, but avoid aggressive retries.

This guide focuses on business-level rejections. For HTTP-level issues like timeouts and rate limits, see our Hyperliquid RPC timeouts and Hyperliquid API rate limits guides.

Next Steps: Build a Robust Integration

Now that you understand the rejection surface, you can harden your trading bot. Start by implementing the pre-check logic described above, then add a rejection handler that maps each reason to an action. Use the testnet to simulate various scenarios—insufficient margin, post-only would take, and price out of band—to verify your handler.

For a complete Hyperliquid setup, review our Hyperliquid network overview and the Hyperliquid endpoints (RPC Assistant) to choose a reliable endpoint. If you are using WebSocket streams for real-time updates, see our Hyperliquid WebSocket subscriptions and reconnection guide. For historical data needs, check Querying Hyperliquid historical market data.

If you are building on OnFinality, our API service provides managed access to Hyperliquid and other networks. See Pricing for plans. For more troubleshooting guides, visit the OnFinality Learn hub.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started