beehiiv

Lightweight, dependency-free, in-memory beehiiv API v2 fake for testing code that uses the language-agnostic beehiiv v2 REST API. Matches the real wire protocol: Bearer auth, JSON request/response bodies, { data: {...} } single-object envelopes, { data: [], limit, page, total_results, total_pages } list envelopes, and the canonical { status, statusText, errors: [{ message, code }] } error envelope.

Default port: 4835

Quick start

Start the server:

import { BeehiivServer } from "./services/beehiiv/src/server.js";

const server = new BeehiivServer(4835);
await server.start();
// ... run your app/tests ...
await server.stop();

Point a client at it (Bearer auth):

await fetch("http://127.0.0.1:4835/v2/publications/pub_parlel/subscriptions", {
  method: "POST",
  headers: { Authorization: "Bearer parlel-beehiiv-key", "Content-Type": "application/json" },
  body: JSON.stringify({ email: "subscriber@parlel.dev" }),
});
// => { data: { id, email, status: "active", subscription_tier: "free", ... } }

Created posts are captured and inspectable via /__parlel/*.

Access via MCP / preview URL

When run under the parlel pool, this service is reachable through the MCP gateway and a preview URL at http://127.0.0.1:4835. Use BEEHIIV_BASE_URL to point clients/agents at it. Captured posts live at GET /__parlel/messages.

Implemented operations

All /v2/* routes require Bearer auth. State is in-memory and ephemeral.

Publications

Subscriptions

Posts

Service & inspection operations (parlel extensions)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
Publications (list/get)✅ Supported
Subscriptions CRUD (create/upsert/list/get/by_email/update/delete)✅ Supported
Subscriptions required fields (subscription_premium_tier_names, utm_channel, utm_term, utm_content, referral_code)✅ Supported
Posts (create/list/get/update/delete)✅ Supported
Post required fields (authors, slug, web_url, audience, platform, subject_line, etc.)✅ Supported
Correct error envelope (status/statusText/errors[{message,code}])✅ Supported
Captured post inspection✅ Supported (parlel extension)
Cursor-based pagination (has_more/next_cursor)◐ Envelope fields returned but single-page only
Actual newsletter delivery / SMTP✓ By design — Captured in-memory for inspection — no real messages sent
Segments / automations / referral program / webhooks / custom fields API⟳ Roadmap
Real API-key validity / scope enforcement✓ By design — Any non-empty credential is accepted — no real secrets needed
Rate limiting (429)✓ By design — Never throttles — local tests run at full speed, zero cost

Error codes & shapes

Errors use the real beehiiv envelope:

{
  "status": 400,
  "statusText": "Bad Request",
  "errors": [{ "message": "A valid email is required.", "code": "bad_request" }]
}
StatusCodeWhen
401unauthorizedmissing/invalid Bearer auth
400bad_requestinvalid/missing email or title, malformed JSON body
404not_foundunknown publication / subscription / post / endpoint
405method_not_allowedunsupported HTTP method on a valid route

Manifest

See services/beehiiv/manifest.json:

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

BEEHIIV_API_KEY=parlel-beehiiv-key
BEEHIIV_PUBLICATION_ID=pub_parlel
BEEHIIV_BASE_URL=http://parlel-bridge:4835
<!-- parlel:testenv:end -->