How reverse-engineering an undocumented API turned into six real bugs and a working WordPress plugin
Substack Notes has no scheduling feature. You write a Note, you hit publish, it goes out now. That’s your only option.
If you’re trying to show up consistently on Notes – which is one of the main ways to grow on Substack – you need to be at your keyboard every time. No queue. No calendar. No “publish Tuesday at 9am.”
I run my content system from WordPress. So the question was obvious: can I schedule Notes from there?
Substack’s answer: no. No public API. No integrations. No official way to automate Notes from anywhere.
So I built one.
What I actually built
The plugin is a dedicated Substack Notes scheduler that lives inside WordPress. You draft Notes in a custom post type – each one gets an auto-generated sequential title (Note #1, Note #2, etc.) so you don’t waste time naming throwaway short-form content. You pick a date and time, and the plugin publishes it to Substack Notes on schedule.
Under the hood, it handles the parts that make this tricky. The Substack session token is stored with AES-256-CBC encryption. Scheduled times go through timezone-aware UTC conversion so your 9am is actually 9am. A dashboard widget shows you what’s queued, what’s published, and what failed.
The part I’m most pleased with is the self-healing cron system. WordPress’s built-in cron only fires when someone visits your site – which means on a low-traffic site, your scheduled Note might just sit there. The plugin detects overdue Notes and triggers a loopback request to force the cron to run. If your Substack session expires, the scheduler pauses itself and sends you an email instead of silently failing.
No Node.js. No build step. No external dependencies. Ten PHP files following WordPress coding standards.
How the session worked
I built the entire plugin in a single Claude Code session. Not “I typed a prompt and got a plugin.” More like a working session with a collaborator who writes fast but needs real-time feedback.
The process was iterative. Start with the architecture – custom post type, settings page, API wrapper, scheduler. Get a working version. Test it on a local WordPress install. Hit a bug. Describe what happened. Get a fix. Test again.
This is how I use Claude Code for WordPress development in general. I understand the architecture and know what the plugin should do. Claude Code writes the implementation. When something breaks, I describe the symptom and we figure out the cause together. Sometimes it’s obvious. Sometimes it requires digging into third-party source code.
The whole session produced ten files across a clean plugin structure: main plugin file, API wrapper, custom post type handler, admin settings, scheduler, two view templates, CSS, JavaScript, and a readme. All with proper escaping, sanitization, and nonce verification.
What made this project interesting wasn’t the code generation. It was the six bugs we hit along the way.
Six bugs that actually mattered
Every one of these surfaced during real testing. Not hypothetical edge cases – actual failures on a local WordPress install.
1. Activation crash. The plugin crashed on activation. The activation hook tried to reference a class that hadn’t loaded yet because it was deferred to the plugins_loaded hook. Fix: load the includes immediately instead of on a hook. Classic WordPress gotcha.
2. Cookie sanitization breaking authentication. WordPress’s sanitize_textarea_field() was silently destroying the Substack session cookie. URL-encoded characters like %3A and %2B were getting mangled. The authentication kept failing and nothing in the error logs explained why. Fix: replace the standard sanitizer with a custom regex whitelist that preserves encoded characters.
3. Wrong API endpoint. The initial implementation assumed Substack had a /api/v1/me endpoint for verifying authentication. It doesn’t – just returns a 404. Claude Code went and read the source code of the substack-api npm library on GitHub and found the correct endpoint: GET /api/v1/handle/options. This was the moment where the AI did something I wouldn’t have done as fast – systematically reading through an open-source library to reverse-engineer the right call.
4. Missing custom post type. The post type simply didn’t exist after activation. The registration was hooked to init at priority 0, but the function adding that hook already ran at init priority 10 – so priority 0 had already fired. Fix: call register_post_type() directly instead of relying on hook timing. Another WordPress-specific sequencing issue.
5. Timezone offset. Scheduled Notes were publishing an hour off. WordPress’s get_gmt_from_date() function behaved inconsistently depending on how the site’s timezone was configured. Fix: bypass the helper entirely and use explicit DateTime objects with wp_timezone() for all conversions.
6. WP Cron not firing. On a local dev site with no traffic, WordPress’s lazy cron never triggered. Scheduled Notes just sat there. Fix: the self-healing system I mentioned earlier – an admin-side heartbeat that detects overdue Notes and fires a non-blocking loopback request to wp-cron.php.
The pattern across all six: the first version of the code was structurally sound. The bugs came from WordPress-specific behavior, undocumented API quirks, and the gap between “should work” and “works on this actual install.”
What this tells you about building with AI
If you’ve never built something with Claude Code, you might picture a prompt-in, code-out workflow. Type what you want, get a finished product.
The reality is closer to pair programming. The AI is fast at generating structure, refactoring, and following patterns. But the hard parts of this project – the cookie sanitization issue, the undocumented API, the cron timing problem – required context that only existed on my screen. I had to describe what I was seeing. Claude Code had to reason about causes it couldn’t directly observe.
Where it was genuinely impressive: reading the npm library source code to find the right endpoint, and systematically refactoring across ten files when we changed the architecture. Where it needed me: knowing that the bug was in sanitization (not authentication), recognizing the WordPress hook priority issue, and deciding that self-healing cron was worth building rather than just documenting the limitation.
This is the actual workflow. Not magic. Not useless. A fast collaborator that needs a human who understands the domain.
The stack and the result
Pure PHP 7.4+. WordPress-native APIs throughout – Settings API, Custom Post Types, WP Cron, wp_remote_request(), transients, nonces. Substack’s internal API reverse-engineered for auth verification and Note creation. OpenSSL for credential encryption. Ten files, clean architecture, no external dependencies.
The plugin works. I use it to schedule Notes from the same WordPress install where I write everything else.
It’s not something I’d put on the WordPress plugin repository – it relies on Substack’s internal API, which could change without notice. But as a personal tool built in a single work session, it does exactly what I needed. And the development process taught me more about working with Claude Code than any tutorial would.
The best AI projects aren’t the ones where everything works on the first try. They’re the ones where the bugs are interesting enough to be worth solving together.







