For developers
You could build this yourself. Here's the part that bites.
You’re not afraid of the Admin API. You’ve generated the JWT, you know it’s HS256 off the hex secret with a five-minute expiry, and you’ve gotten a PUT /posts/{id}/ to come back 200. The instinct, once you’re that far, is the obvious one: I’ll just script this. And for a one-direction, one-time job, you should — that’s the right call, and nothing here is meant to talk you out of it.
This page is about the gap between that first working request and a two-way sync you can actually trust, because that gap is wider than the docs make it look — and most of it is invisible until you’re already relying on it. Specter is the maintained version of that sync, hosted: connect your site, run changes, review the diff, publish. Subscribe now for 500 free credits.
The part the docs don’t make loud
Ghost is Lexical-native. The lexical field is the source of truth for a post’s body; html is generated from it. Send HTML back with ?source=html and Ghost converts it to Lexical to store it — and that conversion is lossy. Push HTML, pull it back, and you don’t reliably get what you started with: cards flatten, structure drifts, and the post you “edited” via the API is quietly not the post you meant. A naive markdown→HTML→?source=html pipeline looks fine on the first post and corrupts the tenth in a way you don’t notice until a reader does. Getting it right means treating the editable content as a projection and reconciling it back through the format the CMS actually stores. Specter does that, and tells you up front where a card can’t survive the trip — why not just use the API directly is the full accounting.
What “two-way” actually costs to build
Say you get the fidelity right. You still have a pusher, not a sync. A real two-way sync has to answer: which side changed since last time, and what happens when both did. That’s diffing against stored state, a content hash so you don’t push a post whose only change was the CMS re-rendering its own HTML, a debounced watcher so a save doesn’t fire mid-write, and a conflict policy for when local and remote have both moved — and the only sane version of that last one is a prompt, not last-write-wins, because last-write-wins on someone’s published post is how you lose a day of their writing. None of these are hard alone. Together they’re a small, stateful, long-running program you now own — that breaks on the next major version, that you debug at the worst possible moment, that lives in your head instead of a repo someone maintains.
Run it hosted, or keep the scripts
On the webapp, that whole problem is taken off your hands: connect Ghost, WordPress, Shopify, or Webflow, run a recipe or an edit, review exactly which records would be created, updated, or flagged as a conflict, and publish what you approve. Browsing and reviewing cost nothing; only AI runs spend credits.
If what you don’t want taken away is the scripting, that’s exactly what the desktop and open-source edition is for. The sync engine underneath is also a CLI — pull, push, sync, watch, status, test — so you can wire a pull into cron, run a sync at the end of a build, and keep every post as a plain .md file that drops straight into git. The maintained, fiddly parts (JWT, Lexical/mobiledoc conversion, diffing, conflicts, dry-run) are handled either way; you write the orchestration that’s specific to you. There’s also a hosted API in private beta if you’d rather call the engine than run it.
The dry-run is the piece that earns trust: before anything writes, you see the blast radius made legible, instead of squinting at API responses after the fact. When you’re driving real writes against a real blog, that’s the difference between a tool you script confidently and one you babysit.