If you’re reading this, there’s a decent chance you searched for iexcloud.io API documentation, a pricing page, or a code sample, and landed here instead. That’s not an accident. IEX Cloud, one of the most widely used financial data APIs for small and mid-sized developers, was discontinued on August 31, 2024. The docs are gone, the dashboard is gone, and every iexapis.com request in production has been returning errors for well over a year.

For the full IEX Cloud shutdown official announcement, see this page.
If your app, backtest pipeline, or side project still has IEX Cloud calls sitting in it, either quietly failing or already ripped out and replaced with a patchwork of free-tier scrapers, this guide is the migration path. It maps every major IEX Cloud capability (quotes, historical prices, company data, fundamentals, dividends, earnings) to its equivalent in Infoway API, with working code for each one.
What IEX Cloud Users Actually Relied On
Before mapping endpoints, it’s worth being precise about what people actually built on IEX Cloud, because “stock data API” covered a lot of ground:
| Capability | What it was used for |
|---|---|
| Quote | Latest price, previous close, change %, day range, volume, market cap, P/E — the single most-hit endpoint |
| Historical prices / Chart | OHLCV bars for backtesting, charting libraries, ML feature pipelines |
| Company | Name, sector, industry, description, employee count, website |
| Key Stats | 52-week high/low, average volume, shares outstanding, beta, dividend yield |
| Dividends | Ex-date, record date, payment date, amount |
| Earnings / Financials | EPS actual vs. estimate, income statement, balance sheet, cash flow |
| News | Headlines tied to a symbol |
Almost every migration falls into one of these seven buckets. We’ll go through each one, in the order most integrations hit them.
Getting Set Up
Register at infoway.io to get an API key, new accounts include a free trial tier, no credit card required. Authentication is a single header on every REST request:
apiKey: YOUR_API_KEYPython SDK (recommended):
pip install infoway-sdkOr use plain requests (every example below shows both).
The One Change That Touches Everything: Symbol Format
IEX Cloud used bare tickers: AAPL, TSLA, MSFT. Infoway appends a market suffix to every symbol, because, unlike IEX Cloud, it isn’t US-only:
| Market | Format | Example |
|---|---|---|
| US Equities | {TICKER}.US | AAPL.US |
| Hong Kong | {5-digit code}.HK | 00700.HK |
| A-Share (China) | {code}.SH / .SZ | 600519.SH |
| Japan | {4-digit code}.JP | 7203.JP |
| South Korea | {6-digit code}.KS | 005930.KS |
| India | {symbol}.IN | RELIANCE.IN |
For a straight IEX Cloud migration you’ll only ever touch .US, but it’s worth knowing the suffix exists, it’s also the reason you get Hong Kong, China, Japan, Korea, India, forex, and crypto access from the same account, something IEX Cloud never offered.
If your codebase stores bare tickers in a database or config file, a one-line helper avoids touching every call site:
def to_infoway_symbol(ticker: str, market: str = "US") -> str:
"""Convert a bare ticker (IEX Cloud style) to Infoway's suffixed format."""
return f"{ticker.upper()}.{market}"
to_infoway_symbol("AAPL") # "AAPL.US"Scenario 1: Migrating Quotes (Latest Price)
The IEX Cloud /stock/{symbol}/quote endpoint was the single most-called endpoint in almost every integration. Its closest equivalent is the trade endpoint, it returns the latest print, not the full blended quote object IEX Cloud returned, so if your code also reads previousClose, week52High, or marketCap off the same response, those come from a second call (Scenario 3 below).
Endpoint:
<code>GET https://data.infoway.io/stock/batch_trade/{codes}</code>Using the Python SDK:
from infoway import InfowayClient
client = InfowayClient(api_key="YOUR_API_KEY")
trades = client.stock.get_trade("AAPL.US,TSLA.US,MSFT.US")
for t in trades:
print(f"{t['s']:10s} ${t['p']:>10s} vol={t['v']:>10s}")Using raw HTTP:
import requests
symbols = "AAPL.US,TSLA.US,MSFT.US"
url = f"https://data.infoway.io/stock/batch_trade/{symbols}"
headers = {"apiKey": "YOUR_API_KEY"}
resp = requests.get(url, headers=headers)
data = resp.json()["data"]
for tick in data:
print(f"{tick['s']:10s} ${tick['p']:>10s} vol={tick['v']:>10s}")Response fields:
| Field | Description |
|---|---|
s | Symbol |
t | Timestamp (Unix milliseconds) |
p | Last trade price |
v | Volume |
vw | Trade value |
td | Direction: 0 = default, 1 = buy, 2 = sell |
If your old code also needs the bid/ask spread (IEX Cloud exposed iexBidPrice / iexAskPrice from its own limited book), pull the depth endpoint alongside the trade:
depth = client.stock.get_depth("AAPL.US")[0]
best_bid = depth["b"][0][0] # price array, index 0 = best
best_ask = depth["a"][0][0]Scenario 2: Migrating Historical Prices / Chart Data
IEX Cloud’s /stock/{symbol}/chart/{range} endpoint is replaced by the candlestick (kline) endpoint. It’s more configurable — 12 timeframes instead of IEX Cloud’s fixed range strings (1m, 3m, 1y, max, etc.) — and pagination is explicit rather than range-based.
Endpoint:
<code>POST https://data.infoway.io/stock/v2/batch_kline</code>klineType | Timeframe | klineType | Timeframe |
|---|---|---|---|
| 1 | 1-minute | 7 | 4-hour |
| 2 | 5-minute | 8 | Daily |
| 3 | 15-minute | 9 | Weekly |
| 4 | 30-minute | 10 | Monthly |
| 5 | 1-hour | 11 | Quarterly |
| 6 | 2-hour | 12 | Yearly |
Using the Python SDK — the direct equivalent of chart/1y:
from infoway import InfowayClient, KlineType
client = InfowayClient(api_key="YOUR_API_KEY")
df = client.stock.get_kline("AAPL.US", kline_type=KlineType.DAY, count=250)
print(df.tail())Using raw HTTP, with pagination for full history (IEX Cloud’s chart/max equivalent — a single request maxes out at 500 candles, so walk backward using timestamp):
import requests, json
API_KEY = "YOUR_API_KEY"
ENDPOINT = "https://data.infoway.io/stock/v2/batch_kline"
HEADERS = {"Content-Type": "application/json", "apiKey": API_KEY}
def download_full_history(symbol: str, kline_type: int = 8) -> list[dict]:
all_candles, cursor = [], None
while True:
payload = {"codes": symbol, "klineType": kline_type, "klineNum": 500}
if cursor:
payload["timestamp"] = cursor
resp = requests.post(ENDPOINT, headers=HEADERS, data=json.dumps(payload), timeout=30)
resp.raise_for_status()
items = resp.json().get("data", [])
candles = next((it["respList"] for it in items if it["s"] == symbol), [])
if not candles:
break
all_candles.extend(candles)
oldest_ts = int(candles[-1]["t"])
if cursor and oldest_ts >= cursor:
break
cursor = oldest_ts
all_candles.sort(key=lambda c: int(c["t"]))
return all_candles
candles = download_full_history("AAPL.US")
print(f"AAPL.US: {len(candles)} daily bars")Minute-level history goes back 3 years; daily and above have no lookback limit, comparable to or better than what most IEX Cloud plans allowed for
chart/maxon lower tiers.
Scenario 3: Migrating Company Overview & Key Stats
This is where IEX Cloud blended several concerns into one /stats object. Infoway splits it into purpose-built endpoints, which is more calls but cleaner data:
| IEX Cloud field | Infoway source |
|---|---|
companyName, sector, industry, description, employees, website | stock_info.get_company() |
marketcap, sharesOutstanding, peRatio | basic.get_symbol_info() + stock_info.get_valuation() |
week52high, week52low, avg30Volume | stock_info.get_valuation() / candlestick history |
dividendYield | basic.get_symbol_info() (dividend_yield field) |
Company overview:
from infoway import InfowayClient
client = InfowayClient(api_key="YOUR_API_KEY")
company = client.stock_info.get_company("AAPL.US")
print(company["company_name"], "-", company["industry"])
print(company["description"][:200])Key stats and valuation (raw HTTP, since these live under /common/basic/):
import requests
headers = {"apiKey": "YOUR_API_KEY"}
info = requests.get(
"https://data.infoway.io/common/basic/symbols/info",
headers=headers,
params={"type": "STOCK_US", "symbols": "AAPL.US"},
).json()["data"][0]
print(f"Market cap components: total_shares={info['total_shares']}, "
f"eps_ttm={info['eps_ttm']}, dividend_yield={info['dividend_yield']}")
valuation = requests.get(
"https://data.infoway.io/common/v2/basic/stock/valuation/AAPL.US",
headers=headers,
).json()["data"]
latest_pe = valuation["pe_list"][-1]
print(f"Latest P/E: {latest_pe['pe']}")The common/v2/basic/stock/valuation endpoint returns full PE/PB time series, not just a snapshot — useful if your old code was reconstructing valuation history by polling IEX Cloud’s /stats endpoint daily and storing the result yourself.
Scenario 4: Migrating Dividends and Earnings
IEX Cloud’s /stock/{symbol}/dividends/{range} and /stock/{symbol}/earnings map to two dedicated endpoints:
Dividend history (ex-date, record date, payment date, amount):
import requests
headers = {"apiKey": "YOUR_API_KEY"}
resp = requests.get(
"https://data.infoway.io/common/basic/financial/dividend_payout",
headers=headers,
params={"symbol": "AAPL.US", "type": "STOCK_US"},
).json()
for d in resp["data"]:
print(f"ex-date={d['exDate']} amount=${d['amount']} type={d['type']}")Earnings (EPS/revenue actual vs. estimate — the “beat/miss” data most dashboards show):
resp = requests.get(
"https://data.infoway.io/common/basic/financial/earnings",
headers=headers,
params={"symbol": "AAPL.US", "type": "STOCK_US", "period_type": "fq"},
).json()
for e in resp["data"]:
beat = "beat" if e["epsPercentage"] > 0 else "miss"
print(f"{e['periodKey']}: EPS {e['epsActual']} vs est {e['epsEstimate']} ({beat})")Scenario 5: Migrating Financial Statements
IEX Cloud’s /stock/{symbol}/financials returned a flattened income statement. Infoway gives you all four core statements as separate, more granular endpoints — a genuine upgrade if your old code was scraping partial data out of IEX Cloud’s free tier:
| Statement | Endpoint |
|---|---|
| Income statement | /common/basic/financial/income_statement |
| Balance sheet | /common/basic/financial/balance_sheet |
| Cash flow | /common/basic/financial/cash_flow |
| Revenue breakdown (by segment/region) | /common/basic/financial/revenue |
import requests
headers = {"apiKey": "YOUR_API_KEY"}
resp = requests.get(
"https://data.infoway.io/common/basic/financial/income_statement",
headers=headers,
params={"symbol": "AAPL.US", "type": "STOCK_US", "period_type": "fq"},
).json()
for row in resp["data"]:
print(f"{row['periodDate']} {row['itemName']:20s} {row['itemValue']:,}")Each line item comes with an itemId (e.g. total_revenue, net_income, total_assets) so you can filter for the specific figures your model needs instead of parsing a flat blob.
Scenario 6: From Polling to Real Push — WebSocket
Depending on your plan, IEX Cloud’s “real-time” data was often SSE-based or required repeated polling for anything beyond delayed quotes. Infoway’s WebSocket gives you genuine server push for trades, depth, and candles — this is worth adopting even if your old code never used IEX Cloud’s streaming, because it removes the polling loop entirely.
Endpoint: wss://data.infoway.io/ws?business=stock&apikey=YOUR_API_KEY
import asyncio, json, uuid, logging
import websockets
from websockets.exceptions import ConnectionClosed
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
logger = logging.getLogger("stock-stream")
SYMBOLS = "AAPL.US,TSLA.US,MSFT.US"
class StockStreamClient:
def __init__(self, api_key: str):
self.url = f"wss://data.infoway.io/ws?business=stock&apikey={api_key}"
self.ws = None
self.running = True
async def _send(self, msg: dict):
await self.ws.send(json.dumps(msg))
async def _subscribe(self):
trace = lambda: str(uuid.uuid4())
await self._send({"code": 10000, "trace": trace(), "data": {"codes": SYMBOLS}}) # trades
await asyncio.sleep(1)
await self._send({"code": 10003, "trace": trace(), "data": {"codes": SYMBOLS}}) # depth
await asyncio.sleep(1)
await self._send({"code": 10006, "trace": trace(),
"data": {"arr": [{"type": 1, "codes": SYMBOLS}]}}) # 1-min candles
async def _heartbeat(self):
while True:
await asyncio.sleep(30)
if self.ws is None:
break
await self._send({"code": 10010, "trace": str(uuid.uuid4())})
def _dispatch(self, raw: str):
msg = json.loads(raw)
code, data = msg.get("code"), msg.get("data", {})
if code == 10002:
logger.info("TRADE %-10s $%-10s vol=%s", data.get("s"), data.get("p"), data.get("v"))
elif code == 10005:
bid, ask = data["b"][0][0], data["a"][0][0]
logger.info("DEPTH %-10s bid=%s ask=%s", data.get("s"), bid, ask)
elif code == 10008:
logger.info("CANDLE %-10s c=%s vol=%s", data.get("s"), data.get("c"), data.get("v"))
async def start(self):
backoff = 5
while self.running:
try:
async with websockets.connect(self.url) as ws:
self.ws = ws
await self._subscribe()
hb = asyncio.create_task(self._heartbeat())
async for message in ws:
self._dispatch(message)
hb.cancel()
backoff = 5
except ConnectionClosed as e:
logger.warning("Connection closed: %s — reconnecting in %ss", e, backoff)
except Exception as e:
logger.error("Error: %s", e)
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 60)
async def main():
client = StockStreamClient(api_key="YOUR_API_KEY")
await client.start()
if __name__ == "__main__":
asyncio.run(main())Rate Limits: What Changes at Each Tier
IEX Cloud priced by “message” credits consumed per field returned, a model almost nobody found intuitive. Infoway’s limits are flat request-rate limits by plan, which is easier to reason about when estimating whether your existing polling loop will fit:
| Plan | Requests/second | Requests/minute | Requests/day |
|---|---|---|---|
| Free | 1 | 60 | 86,400 |
| Basic | 2 | 120 | 172,800 |
| Advanced | 10 | 600 | 864,000 |
| Professional | 20 | 1,200 | 1,728,000 |
WebSocket connections are limited by total subscribed symbols rather than message volume:
| Plan | Max subscribed symbols |
|---|---|
| Free | 10 |
| Basic | 200 |
| Advanced | 800 (across up to 2 connections) |
| Professional | 5,000 (across up to 9 connections) |
If you’re migrating a script that used to fire one IEX Cloud request per symbol per poll, batch it instead, every REST endpoint above accepts comma-separated symbols in a single call (up to 100 for trade/depth, 500 for symbol info), which cuts your request count by orders of magnitude and keeps you comfortably inside the Free tier for testing.
FAQ
Why did IEX Cloud shut down?
IEX Group, the company behind IEX Cloud, announced in March 2024 that it would discontinue the product, with full shutdown on August 31, 2024. The company cited a strategic shift away from the developer data business. No successor product was offered — IEX Cloud customers were left to find alternative providers themselves, which is the situation this guide addresses.
Is there a free tier to test the migration before committing?
Yes. New Infoway accounts include a free trial with no credit card required, at the Free-tier rate limits shown above — enough to validate that your endpoint mapping and response parsing work correctly before switching production traffic over.
Do I need to rewrite every symbol reference in my codebase?
Only the places where you construct the request — appending .US to a bare ticker is a one-line change (see the helper function above). If you store instrument identifiers in a database, either migrate the stored values once or wrap the conversion at your data-access layer so the rest of the codebase is untouched.
Does Infoway only cover US stocks like IEX Cloud did?
No — that’s one of the bigger differences. The same account and the same request patterns also cover Hong Kong, mainland China (A-shares), Japan, South Korea, India, forex, and crypto. If your product only ever needed US equities, you won’t notice; if you’ve been wanting to add another market without integrating a second vendor, it’s already available.
What’s the closest thing to IEX Cloud’s free “IEX Cloud Sandbox” test environment?
There isn’t a separate sandbox with randomized data — the Free trial tier hits live, real data, just at a lower rate limit. For automated tests that need to run frequently without touching your rate limit budget, cache a handful of realistic responses locally and use them as fixtures.
My code depends on IEX Cloud’s real-time “last trade” price for options or crypto too — does that carry over?
Options are not currently covered. Crypto is — the same batch_trade / batch_kline / WebSocket pattern applies to crypto pairs via the /crypto/ endpoints instead of /stock/, using pair symbols like BTCUSDT instead of .US-suffixed tickers.
How long does a typical migration take?
For a simple quote + historical price integration, most of a day: an hour to map endpoints against this guide, a few hours to update request/response parsing, and time for regression testing against known values. Integrations that also pulled full financial statements or dividend history take longer only because there’s more surface area to test, not because any single endpoint is harder to use.