I Added a Second Broker API, and Every Incident Since Has Come From It
A class of bugs that barely showed up on KIS (Korea Investment & Securities) for weeks started firing almost weekly the moment NH Securities' Open API joined the stack
This is the English version of a post originally written in Korean for my algorithmic trading system devlog(new tab).
I've been migrating my main trading account from KIS to NH Securities. Along the way, something odd stood out: a whole class of incidents that had barely shown up on the KIS side for weeks started firing almost weekly the moment NH's API joined the stack.
Two APIs doing the same job — so why did the trouble cluster almost entirely on one side? This post is my attempt to lay that out.
Why switch brokers at all
Everything originally ran on KIS (Korea Investment & Securities). Once the decision was made to move the main account to NH, I started running an NH paper-trading sleeve in parallel as a validation step before the real-money cutover. At first I treated it as "just wiring up one more API."
The first outside review came back almost immediately: cutting over the next day was not viable. The order-placement path didn't exist yet, the target account had my own pre-existing holdings mixed in (so the bot risked touching stock I hadn't asked it to touch), and above all there was zero validated track record on the NH side. So the plan became: paper trading in parallel → small real-money pilot → staged budget migration → eventual KIS retirement.
The trouble hit hardest during that "build up a track record" phase.
What broke, only on NH, over three weeks
Listing every single one would get tedious, so here are a few that stood out for being different kinds of problems.
A fill price quietly came back 1000x too small. NH's execution-history endpoint had been returning the average fill price field scaled down by a factor of 1000, and I hadn't noticed. The first time it showed up, I wrote it off as a one-off. It turned out to have been happening on every single fill since that day. That corrupted value flowed straight into the ledger, understating how much cash had gone out on each buy, and three days later a safety guard picked up the anomaly and halted trading automatically. No money actually went out wrong, but the ledger needed manual reconstruction.
A stock I'd clearly bought didn't show up in the holdings query. The fill history showed the buy correctly, but querying current holdings made it look like the position didn't exist at all. It turned out the holdings endpoint paginates its response, and the client was only reading the first page. Once holdings crossed a page boundary, anything past it silently vanished. From the ledger's point of view, it looked like "the ledger has it, the broker doesn't" — which is exactly the signal a safety guard is built to halt on.
The same pagination bug behaved differently between the live and sandbox accounts. After fixing the above, digging deeper turned up something more structural: on the live account, a truncated page honestly reports "there's more," but on the paper/sandbox account, the exact same truncation falsely reports "that's everything." The two looked like the same documented API, but they were actually running on different backends. That meant validating something on paper carried no guarantee it would hold on live money — which forced a rewrite of how validation itself was supposed to work.
An endpoint got blocked with zero notice. A price-lookup call that had worked fine for days suddenly started returning "not available for paper trading accounts." Nothing in the docs changed, no notice went out. A quiet fallback to a second price source got patched in on short notice.
An auth token got invalidated before its stated expiry. The locally cached token still had plenty of time left according to its own expiry field, but the server had already invalidated it — and that quietly halted trading for the rest of that afternoon. The fix now discards the cache and re-issues immediately on any "invalid token" response.
The exact same bug resurfaced in a different endpoint. The pagination issue from the holdings query showed up again, this time in the account-summary endpoint — with a meaner twist: intermediate pages had their summary totals zeroed out, so reading only the first page made the account look like it held zero value. That this one hit the live account made it sting more.
The same false alarm repeated nine times over two weeks. "The ledger has it, the broker query doesn't" kept recurring, and for a while I chalked it up to "NH's servers must just be flaky." Looking closer, the real cause was on our side: the holdings-query function could fail partway through paginating and still return whatever partial results it had collected as if it had succeeded — no exception thrown. That partial gap got misread as "the broker genuinely doesn't have this position." The first hypothesis — "their server is unreliable" — turned out to be our own code misreading the API's contract.
Why this only happened on NH, not KIS
A few structural differences stacked up.
- The starting point for documentation was different. KIS had proper developer docs. NH's official documentation wasn't sufficient, so the spec had to be reverse-engineered by observing real requests and responses. That left a much larger surface of unverified behavior, and each unverified corner eventually turned into a real incident.
- The assumption that "sandbox is a scaled-down live environment" didn't hold. That was mostly true for KIS. On NH, live and sandbox behaved like effectively different systems. Something confirmed safe on paper didn't always carry over to live.
- Pagination signals were hidden in response headers. Instead of the response body, a specific header value indicated "there's another page." That's easy to miss once, and once missed, the same mistake repeated across multiple endpoints.
- Field reliability varied endpoint by endpoint. A field like fill price, which looks like it should obviously be correct, turned out to be computed wrong in one specific place — something no amount of reading documentation would catch; only measuring the actual values caught it.
What I'd tell anyone else migrating brokers (or any external financial API)
Turning this into a checklist, in case it saves someone else the same three weeks.
- Don't trust the docs — measure everything. Request limits, response codes, even what counts as success vs. failure — verify every one of these against the documented value. Our request limits alone didn't match what the docs claimed.
- Don't assume sandbox and live are "two modes of the same system." They can look like the same API on paper while running on completely different backends underneath. Passing validation on sandbox is not a substitute for validating on live.
- Check whether pagination lives in response headers before you trust a single page as "complete." Reading only the body and assuming you're done produces a silent gap that looks like missing data rather than truncated data. And once you find this pattern in one endpoint, assume every other endpoint on the same backend needs the same suspicion, not just the one that already broke.
- Audit whether your query functions can turn "partial failure" into "apparent success." Whether a function throws on a mid-pagination failure or just returns whatever it managed to collect determines how every downstream safety check behaves. We assumed the wrong contract here and burned two weeks chasing "the server must be flaky" as a result.
- Be suspicious of every field's scale and units, not just its presence. Especially for anything price- or quantity-related where money is on the line, it's worth building a separate cross-check path against another derivable value (like fill amount divided by quantity).
- Assume API behavior can change with zero notice, and have a fallback ready. Free-tier and sandbox-only features in particular can get restricted at any time.
- Don't trust that the server will honor your local token's stated expiry. It's safer to build in, from day one, a path that discards the cache and re-issues immediately the moment you see any "invalid token" class of error.
- Treat an alert as something to close out the same day it fires. The single costliest incident here wasn't the bug itself — it was leaving the alert unaddressed for three days while the corruption kept compounding. The bug alone could have been a one-day incident. Neglect is what turned it into a three-day recovery.
Generalizing this
Expect a new API integration to fail far more often than an existing one doing the same job — that's normal, not a red flag about the vendor. The size of the unverified surface is just different. Jumping to "this API must be inherently unstable" during that early period is the wrong first move; "we still don't fully understand this API's contract" is the better default hypothesis. In fact, our longest-running recurring incident turned out to be our own calling code, not the other side's server.
Fixing a bug class in one place doesn't mean it's fixed everywhere. The pagination bug got fixed in one endpoint, and the same bug came right back in a different endpoint sharing the same backend. Once you find a structural flaw in one place, checking "does anything else use this same pattern?" right then is far cheaper than discovering each instance one incident at a time.
When money is involved, leaving an alert unaddressed is itself a cost. The damage at the moment an alert fires is usually small. What compounds it is letting the alert sit while the corrupted value keeps feeding into other calculations. Responding to alerts quickly was the single habit that decided whether an incident stayed a one-day problem or grew into a multi-day one.