When I built the cross-publish pipeline for this project, the target list was Dev.to, Hashnode, Bluesky, and Beehiiv. Three of those four work without surprises. Beehiiv provided three lessons I didn't expect.
The pipeline lives in packages/publish/src/beehiv.ts. It reached production, but not in the way I originally intended. Here's what actually happened.
The post creation API is Enterprise-only, even on $99/month
The Beehiiv API documentation lists a POST /publications/:id/posts endpoint. I implemented it, wired up the API key, ran a test — and got back a 401 with body SEND_API_NOT_ENTERPRISE_PLAN.
What surprised me: this isn't about the free tier. The Scale plan at $99/month also returns this error. The Create Post API is genuinely locked to Enterprise, which starts at several hundred dollars per month.
The code now catches the 401 and 403 responses and surfaces a specific error:
Beehiiv 401: post creation API requires Enterprise plan.
Even Max trial / Scale ($99/mo) returns SEND_API_NOT_ENTERPRISE_PLAN.
Manual posting via app.beehiiv.com editor is the only option for non-Enterprise tiers.
So Beehiiv isn't in the publish_to frontmatter list at all — the publish package excludes it from the default platform set, and you have to pass --platform beehiv explicitly to even attempt it. On a non-Enterprise key that attempt just throws the error above. If I want an article on Beehiiv, the web editor is the only route. Not elegant, but accurate — and the error message keeps future-me from re-implementing the same thing.
The other three platforms don't have this gating. Dev.to's API works on the free tier. Hashnode's GraphQL API works on the free tier. Bluesky's AT Protocol is entirely free. Beehiiv is the outlier.
The body_content field expects HTML, not markdown
Had the Enterprise issue not blocked me first, I would have hit this second.
Dev.to accepts a markdown body_markdown field — you send markdown, it renders on their side. Hashnode accepts contentMarkdown in the GraphQL mutation. I send the same raw markdown to both.
Beehiiv's Create Post endpoint uses body_content, which expects HTML. I had to add a markdownToHtml conversion step specifically for Beehiiv. The other platforms don't need it.
The Hashnode integration also takes HTML in some contexts — their embed API is HTML-based — but the article creation flow accepts markdown. Beehiiv requires the conversion for the primary path, not just edge cases. This means any change to how I convert markdown to HTML affects Beehiiv output specifically, which is a maintenance surface that doesn't exist for the other two platforms.
The conversion itself isn't hard. I use a lightweight renderer rather than a full parsing pipeline since the articles have predictable structure: headers, paragraphs, code blocks, the occasional table. But it's an extra step, tested separately from the Dev.to and Hashnode paths. See how I resolved cross-publish internal links for the related problem of how links behave differently across platforms.
The response URL field is ambiguous
Looking at other API responses in the publish pipeline gave me a false expectation about response shape. Dev.to returns a clear url field. Hashnode's mutation returns data.createPublicationStory.post.url. Both are predictable.
The Beehiiv Create Post response returns data with two URL fields: web_url and url. From the docs alone, I couldn't tell which one to use — and because the Enterprise gate blocked every Create Post call I made, I never got a successful response to check against.
The production code uses a fallback chain:
const url = data.data.web_url ?? data.data.url ?? "(beehiiv post created, URL unknown)";
I have to be honest here: this chain has never actually run. Every call I made died at the 401 before a response body came back, so the fallback is defensive code against a response shape I've only read about in the docs. Since I can't observe which field gets set on a real call, both checks stay.
This isn't a blocker, just an annoyance that adds a branch I can't verify or simplify away. Both fields being optional in the schema reflects what I'd guess is an API evolution where one field was added and the other wasn't fully deprecated yet.
What the pipeline looks like now
Beehiiv is not in publish_to — the publish package deliberately excludes it from the default platform list because of the Enterprise wall, and no article in this project lists it. There's no graceful fallback either: if you opt in with --platform beehiv on a non-Enterprise key, the call simply throws the error from section one and the run records it as a failure. Nothing renders the body to stdout or prints a copyable block; posting to Beehiiv would mean pasting into the web editor by hand, which I haven't been doing.
If I ever upgrade to Beehiiv Enterprise, the existing beehiv.ts implementation should work — the HTML conversion and fallback URL chain are already in place, though untested against a real successful response. The Enterprise gate is the only real blocker. Until then, Beehiiv simply isn't part of the automated run, and the code is honest about why.
This pattern of building toward automation and gracefully degrading when a platform blocks you is something I've run into more than once — the three things that changed when an API token expired post covers a similar forced-manual fallback from a different angle.
Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.
Top comments (0)