For developers

You could build this yourself. Here's the part that bites.

By Axel Antas-Bergkvist Published May 21, 2026 Updated May 29, 2026

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.