← All Writing
May 22, 20267 min read

How I Self-Healed Schwab’s 7-Day OAuth Expiry

Schwab’s market-data API hands you a token that hard-expires every 7 days — by design, not by bug. Instead of setting a reminder to renew it by hand, I built a pipeline that renews itself before it dies and only emails me when it genuinely can’t.

YieldA market-data pipeline that survives Schwab’s 7-day token expiry without human babysitting — it renews the token a couple of days early, degrades gracefully if it can’t, and emails me only when a real person is actually needed
DifficultyIntermediate (OAuth token refresh, a scheduled freshness check, unattended browser login, fail-safe email alerting)
Total Cook TimeA few sessions of iteration — the first version reminded me to renew by hand; the final version renews itself and only speaks up on failure

Ingredients

The account itself is boring on purpose

Worth saying up front: the brokerage account behind this API holds under a dollar. It exists purely to unlock the free market-data feed — no meaningful balance, no trading through it. That framing mattered when I decided how much automation to trust with the login: there’s effectively nothing to steal, and the design still errs on the side of caution anyway.

The Problem: A Token That Dies Every Week

Several of my little projects lean on the Schwab Market Data API — the morning market email, an options monitor, a trading-thesis watchdog. They all read from the same live feed. To talk to that feed, you authenticate once and get back an OAuth token — a temporary password the API accepts instead of making you log in every single call.

There are actually two tokens in play, and the distinction is the whole story:

Here’s the sharp edge. On most OAuth systems, a refresh token lets you renew indefinitely — you use it to get a new access token, and often a fresh refresh token comes back with it, resetting the clock. Schwab doesn’t work that way. After 7 days the refresh token is simply dead, and no amount of refreshing revives it. The only way to get a new one is a real login — an actual human-style trip through Schwab’s sign-in and consent screens.

This is a constraint, not a bug

I want to be precise, because it’s easy to read this as “Schwab’s API is broken.” It isn’t. A 7-day hard expiry on the refresh token is a deliberate platform security choice — it caps how long a leaked token stays useful. My job wasn’t to fight the policy. It was to live inside it without a weekly chore.

The naive failure mode: I set everything up, it works beautifully for a week, and then on day 8 every downstream project silently starts getting rejected. No crash, no obvious signal — just stale-or-missing market data leaking into the morning email until I happened to notice something looked off. A pipeline that dies quietly is worse than one that dies loudly.

Version 1: A Reminder to Do It Myself

The first version was honest about its limits. A daily check looked at how old the refresh token was, and once it crossed a threshold, it emailed me: “Schwab token is aging, go re-authenticate.” Then I’d sit down, walk through the login by hand, and the clock reset.

It worked. It also quietly annoyed me every few days. A reminder that only ever tells you to go do a manual chore isn’t automation — it’s a nag with a cron schedule. Worse, it depended on me being reachable. Travel, a busy morning, a missed email, and the token would lapse before I got to it. The reliability of the whole data pipeline was pinned to my inbox habits, which is not a load-bearing thing to depend on.

So the goal changed. Not “remind me to renew.” Renew it for me, and only bother me if you actually can’t.

The Core Idea: Renew Early, Alert on Failure

The whole design collapses to two decisions made once a day by a scheduled check:

“No news is good news” is a feature

The most valuable property of this system is that a healthy week is completely silent. I used to get a renewal nag every few days. Now the token quietly rotates in the background and the only email I ever get about Schwab OAuth is one that actually requires my hands. An alert I can trust to mean “act now” is worth ten alerts I learn to ignore.

🔧 Developer section: the daily decision

The Hard Part: Renewing Without a Human — and the Spectrum of Ways to Do It

Here’s the tension: getting a new refresh token requires a real login, the kind a person does in a browser — but the whole goal is to not need a person. Squaring that means the server drives the login itself, in a headless (no-screen) browser, then catches the fresh token at the end. I’m staying deliberately high-level on the mechanics — this touches a real brokerage login, and a step-by-step would be the wrong thing to publish.

It helps to split the problem in two. Refreshing the short-lived access token (trading a refresh token for a new one every half hour) is standard, endorsed OAuth, spelled out in Schwab’s OAuth guide. The friction is entirely the 7-day hard expiry on the refresh token: once it dies, the only way back is a full login-and-consent flow. That day-7 re-login is what people solve differently, along a security spectrum:

The one rule that doesn’t move

Wherever you land, the API client secret, the refresh token, and — above all — your brokerage password stay out of hardcoded literals, out of committed git history, and out of anywhere prone to leaking. Load them at runtime from a locked-down file; never paste them inline where they can end up in a repo, a log, or a screenshot.

The honest gray area: automating the token refresh is fine and endorsed. Automating the login to mint a fresh refresh token every 7 days scripts a flow meant for a human. It’s widely done, but it leans on that sign-in staying scriptable — if Schwab ever added bot-detection or step-up MFA to the authorize screen, the automated approaches would break overnight while the manual-paste one kept working. A pragmatic solution living at the mercy of Schwab’s login hardening, and that’s worth naming plainly.

🔧 Developer section: safety rails on the auto-login

The lesson I’d underline: never hammer a login. On an earlier project I let automation retry a login it was confused about, and the retries got the account soft-locked. So this renewal makes exactly one clean attempt and stops the moment anything looks off:

I’m staying at the philosophy level on purpose — I won’t walk through exactly how mine drives the login. But if you’re building something similar and want to compare notes, reach out.

The Backstop: Degrade, Don’t Crash

Self-healing is the happy path. But I wanted the downstream projects to survive even awindow where the token is dead — say the renewal fails on day 5 and I don’t fix it until day 7. So every consumer of the Schwab feed has a fallback: if the live token isn’t working, it quietly falls back to free public data sources for prices instead of throwing an error and killing the whole job.

This is the same principle as the market email from an earlier post: when a data source is down, the email still sends — with a note in the affected section instead of a crash. A missing options chain shouldn’t take down the entire morning briefing. The token renewal makes the good data show up; the fallbacks make sure a bad day is a degraded email, not a dead one.

Designing the One Email That Matters

Since a failure email is now a rare, real event, I put care into making it actionable. When the self-heal can’t recover, the email tells me how old the token is, roughly why the automated attempt failed (a login challenge, a changed page, the browser being down), and the exact command to run to fix it by hand. It’s not “something went wrong” — it’s “here’s the state, here’s the button.”

One more subtlety: I didn’t want a stuck token to email me every single day. So the alert de-duplicates — once it’s warned me, it stays quiet for 24 hours before it’s allowed to warn again. A real problem gets one clear ping, not a daily pile-up that I’d start tuning out.

🔧 Developer section: what the failure email carries

How It Grew: From Nag to Self-Heal

The evolution is really a story about moving the human further and further out of the loop:

I kept the manual path deliberately. Self-healing systems shouldn’t delete their own escape hatch — when the automation gives up (correctly, per the safety rails), I want a fast, well-worn way to fix it myself. The failure email is basically a shortcut straight to that path.

What went fast

What needed patience

The best part of this one isn’t visible, which is exactly the point. There’s no new feature to look at, no page to open. There’s just a token that used to die every week and now quietly renews itself before it does — and a morning email that keeps arriving with real market data behind it, whether or not I remembered a thing.

← Back to all writing