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 is in the publish_to frontmatter list but the automation route is manual: the pipeline prints the article body in a code block after each run, and I paste it into the Beehiiv web editor. 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. I found that only one is populated per response, and which one varies — sometimes web_url, sometimes url — without a clear rule for which gets set.
The production code uses a fallback chain:
const url = data.data.web_url ?? data.data.url ?? "(beehiiv post created, URL unknown)";
The "(beehiiv post created, URL unknown)" fallback has never actually been needed — one of the two fields is always present. But I don't know which one will be set on any given call, so both checks stay.
This isn't a blocker, just an annoyance that adds a branch I can't simplify away. Both fields should exist in the response. The fact that they don't always 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 still listed in publish_to because the intent is to cross-post there. The automation catches the Enterprise wall and falls back gracefully: it renders the article body as HTML, writes it to stdout, and the article routine prints it as a copyable block. That part works reliably.
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. The Enterprise gate is the only real blocker. Until then, one platform in the pipeline is semi-manual, 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)