Handling delistings, mergers, and ticker changes
A live trading universe is more turbulent than it looks. Every year, ~5% of listed US issuers undergo a name change, merger, spinoff, or delisting. Your application has to handle each case without crashing or, worse, silently treating one company as another.
This recipe covers the failure modes and the right defensive patterns.
Failure modes
| Event | API behavior | What you usually want to do |
|---|---|---|
Active issuer renamed (FB → META) | Old ticker returns 404 not_found after the effective date. New ticker works. | Update your symbol map. Migrate historical data under the old ticker. |
Merger (ATVI → MSFT) | Old ticker 404s. The merger is in corporate_actions. | Replace the holding with cash on the effective date. |
| Spinoff (Kellogg → Kellanova + WK Kellogg) | New ticker(s) appear. Old ticker continues if surviving entity keeps it. | Pro-rate basis across the resulting tickers. |
| Going private | 404 not_found permanently. | Mark the holding as cash-out at the take-private price. |
| Delisting from one exchange but still active elsewhere | Specific listing 404s. Other listings still work. | Switch to a different listing. |
| Ticker rename and re-use (rare; happens after a few years) | Same ticker may resolve to a different ISIN. | Always cross-check ISIN before treating “same ticker” as “same instrument”. |
Defensive pattern 1: Use ISIN as the primary key
Ticker is unstable. ISIN is stable. If you are persisting a watchlist or a portfolio, store the ISIN alongside the ticker:
CREATE TABLE holdings ( id BIGSERIAL PRIMARY KEY, user_id UUID NOT NULL, ticker TEXT NOT NULL, -- last-known ticker, may go stale isin TEXT, -- stable across renames quantity NUMERIC NOT NULL, cost_basis NUMERIC NOT NULL, added_at TIMESTAMPTZ DEFAULT now());When a ticker 404s, look up the ISIN via /v1/symbol_search?query=<ISIN>
and update the row if you find a new ticker for the same ISIN.
def resolve_or_replace(holding): try: r = httpx.get( "https://api.oneapi.finance/v1/quote", params={"symbol": holding["ticker"]}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10.0, ) if r.status_code == 200: return holding["ticker"], r.json()["quote"] except httpx.HTTPError: pass
if holding.get("isin"): s = httpx.get( "https://api.oneapi.finance/v1/symbol_search", params={"query": holding["isin"]}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10.0, ) s.raise_for_status() candidates = [c for c in s.json()["symbols"] if c["isin"] == holding["isin"]] if candidates: new_ticker = candidates[0]["symbol"] update_holding_ticker(holding["id"], new_ticker) return resolve_or_replace({**holding, "ticker": new_ticker})
mark_holding_inactive(holding["id"]) return None, NoneDefensive pattern 2: Distinguish “no data” from “no instrument”
A 404 not_found on /v1/quote could mean either:
- The instrument is unknown (delisted, never existed, typo).
- The instrument is known but the upstream has no recent quote (rare; can happen for very illiquid names).
Check /v1/profile to disambiguate:
def status_of(symbol: str) -> str: r = httpx.get( f"https://api.oneapi.finance/v1/profile", params={"symbol": symbol}, headers=HEADERS, timeout=10.0, ) if r.status_code == 404: return "delisted_or_unknown" r.raise_for_status() return "active"For a fully built-out solution, future endpoints will return a status field
on /v1/stocks rows (active, delisted, unknown) so you can detect this
without an extra call.
Defensive pattern 3: Snapshot historical data before it disappears
When a symbol is renamed or merged, the old ticker’s historical
/v1/time_series data continues to work for a while, but vendors rotate
older symbols out of their public universes after months or years. If you
care about historical performance, snapshot the time-series locally when
you first add the holding, and resume from where you left off on each
update.
This also has the happy side effect of cutting your API calls.
Defensive pattern 4: Handle 502 separately from 404
A 502 upstream_failure is transient — every upstream we tried just
happened to be down at this exact moment. Retry with backoff (see
errors). Do not delete or mark-inactive a holding
based on a 502.
A 404 not_found is structural — we genuinely could not resolve the
symbol. Investigate.
See also
- Instruments — symbol vs ISIN vs FIGI.
- Corporate actions — split/dividend semantics.
/v1/symbol_search— look up by ISIN or name.