beehiiv streamlines newsletter publishing and audience growth by creating and editing posts, managing subscriber lists and segments, automating email journeys, analyzing engagement and advertising revenue, managing paid recommendations, and publishing podcast episodes.
Encrypted at rest, isolated from the model
Resolved from an AES-256-GCM vault at the moment of the call and attached to the request — the model never sees the secrets.
Try asking
The full performance and payment report for one ad placement: earnings and payout status, payout model and rate, recipients and delivery rate, unique opens and open rate, payable clicks and click-to-open rate, and the send window. Get the report id from list_ad_reports.
The ads a publication can currently place in a post: 'available_offers' (unclaimed offers the publisher hasn't acted on) and 'pending_opportunities' (offers already claimed but not yet placed). Embed an item's id using an advertisementOpportunity node via save_post or edit_post_content. When an offer lists more than one entry in 'creative_variations', let the publisher pick which they want (with logo vs without) and set data-advertisement-kind to their choice. Optionally set data-custom-intro to plain text shown before the ad when the custom ad intro feature is available. Ids are internal placement handles - build the node with them and refer to ads by advertiser name when talking to the publisher. For the history of opportunities already acted on, use list_ad_opportunities.
A publication's ad opportunity history - opportunities already acted on (accepted, pending, rejected, expired, canceled, missed). Use it to check the status of a past opportunity or find an already-accepted one to embed. Ids are internal placement handles; refer to ads by advertiser name when talking to the publisher. For the ads currently available to place, use list_ad_offers.
What a publication earned from the beehiiv ad network, post by post. One row per ad that has run, with the post it appeared in, the advertiser, how much it earned, and whether that money is paid or still awaiting approval. Use it to answer which posts made money and how much, to compare advertisers, or to total earnings per advertiser over a past month for finance reconciliation. In disbursement_status, paid and manual both mean settled; every other value is still pending. Returns all reports by default, newest send first. Use get_ad_report for one placement's full performance breakdown, get_earnings for publication-wide revenue totals across all monetization, and list_ad_offers for ads still available to place.
Generate an AI image from a text prompt and save it to a publication's media library. Fire-and-forget: this enqueues a background job and returns immediately with a `generation_id` and status "pending". Kick it off, then continue with other independent work (other pages, content, assets), and call `get_image_generation_status` once you actually need the result. The finished asset's `id` serves as a post's `thumbnail_id`, and its `url` embeds in HTML content via `save_post` or `edit_post_content`. Generation uses Google Gemini, or OpenAI with model_preset "openai", and counts toward the workspace's AI credit quota, reported in the completed status response's `ai_usage` field.
Get a single asset from the media library with full details including usage information
Check the status of an AI image generation started by `generate_image`: "pending", "processing", "completed" (the response then carries full asset details) or "failed" (with an error message). Finished responses also carry an `ai_usage` object reporting AI credit consumption. Generation typically takes 15-45 seconds, so do other independent work after kicking it off and check back once, rather than polling in a loop. When you do need to wait, leave about 10 seconds between checks.
Search and browse a publication's media library. Supports text search, file type filtering, and folder filtering. Use an asset's `url` when placing it in post or page content, since the editor re-uploads images from any other host. `share_url` is a CDN link for sharing outside beehiiv.
Upload an audio file from a URL to a publication's media library. Returns the downloadable ID and metadata for embedding an audio widget via `save_post` or `edit_post_content`. Supported formats: MP3, WAV, OGG, FLAC, AAC, WebM audio.
Upload a file from a URL to a publication's media library. Returns the downloadable ID and metadata for embedding a fileAttachment widget via `save_post` or `edit_post_content`. Supported formats: PDF, EPUB.
Upload an image to a publication's media library from a URL or raw base64 bytes. Returns the asset ID and the hosted image URL. The returned `id` can be used as a post's `thumbnail_id`. The returned `url` can be embedded in HTML content via `save_post` or `edit_post_content`.
Update the metadata of an existing asset in a publication's media library. At least one editable field is required.
Add a single step to an existing automation's flow. Works on live (published) automations: the step is added to a staging workspace (a draft copy) and only affects the live automation once you publish. Reference the parent step via `parent_id`; live step IDs are accepted and mapped to their staging copy. Call `get_automation_schema` for per-step-type param shapes. For `send_email`, pass `subject_line` to auto-create a placeholder email, or `email_message_id` to attach an existing one. Configure a `webhook` step's destination with `update_automation_step` after creating it. When the response reports the automation live with unpublished changes, relay its `publish_instructions`.
Delete a single step from an existing automation's flow. Works on live (published) automations: the deletion is applied to a staging workspace (a draft copy) and only affects the live automation once you publish. The step's children are re-parented automatically. Pass the live or staging `automation_step_id` to delete. Live IDs are mapped to their staging copy. When the response reports the automation live with unpublished changes, relay its `publish_instructions`.
Delete a trigger from an automation. Always edits a staging workspace (a draft copy); the live automation is unchanged until a human publishes from the editor. Pass the live or staging `automation_trigger_id` to delete. Live IDs are mapped to their staging copy. An automation must keep at least one trigger. The deletion is saved to staging; publish the changes from the editor.
Edit an existing automation email's body incrementally by targeting individual blocks - cheaper and safer than resending the whole body with `save_automation_email`. Untouched blocks are left exactly as they are; only the blocks you touch are re-parsed. Read the body once with `get_automation_email_content` (format: `editor_html`), then target a block in `operations`. Top-level blocks are stamped with `data-node-hash="<hash>"`; the response returns refreshed hashes so you can chain more edits without re-reading. A nested node is a valid target too - to change one paragraph inside a `section`, target that paragraph's `data-id` rather than replacing the whole section and re-sending its siblings. Target the hash by default: it is rejected if someone else changed that block since you read it, which an id is not. An edit elsewhere leaves a block's hash intact, so you can chain. Use an id when you mean to write over whatever the block now holds. `content` is HTML matching the editor's parse rules - the same contract as `save_automation_email`. Call `learn_post_authoring` for it. Automation emails drop ad-related editor nodes such as `advertisementOpportunity` rather than authoring them. Edits land in the automation's staging (draft) workspace. When the response reports the automation live with unpublished changes, relay its `publish_instructions`. Atomic: if any target no longer resolves, nothing is applied and you are told to re-read.
Get an automation's full configuration including triggers, step flow, email metadata, and branch paths. Use workspace parameter to inspect live (published) or staging (draft) flow.
Get the rendered content of an automation step's email. 'editor_html' returns canonical HTML compatible with save_automation_email and, for small changes, edit_automation_email_content - each top-level block is stamped with data-node-hash for targeted patching. A 'staging' read falls back to 'live' when the step has no unpublished draft yet; the response 'workspace' field reports which was read. Call `get_automation_schema` for the full email workflow.
Get detailed information about a single automation journey, including all journey steps, branch decisions, and per-subscriber email engagement metrics (open rate, click rate).
Get the Automation reference: trigger event main_conditions shapes, step type params, branch paths, resources, and email authoring workflow.
Get performance metrics for an automation. Returns overall stats, per-email breakdown, and optionally per-step subscriber counts with branch decisions. Rates are percentages.
Compare an automation's staging workspace against its live (published) version. Returns a summary of what changed: added, removed, or modified steps and triggers. Use before publishing to review pending changes.
List the emails sent by an automation. Use this to discover send_email step IDs before calling save_automation_email or get_automation_email_content.
List subscriber journeys for a given automation with optional filtering by status, email, and time period. Returns journey details with cursor-based pagination. Use the next_cursor value to fetch subsequent pages.
List automations for a given publication with optional filtering by state and trigger event
Reorder automation steps in bulk. Pass `bulk_updates` keyed by live or staging automation_step_id. Each update can set `parent_id`, `branch_arm`, and `branch_path_id`; live IDs are mapped to their staging copies. When `parent_id` changes, the graph is automatically repaired: the moved step's old children are re-parented to its old parent, and the new parent's existing children are threaded under the moved step. Specify only the steps you are explicitly moving; displaced neighbours are updated automatically. Only works while the automation is not live. Changes are saved to staging; publish them from the editor.
Create or update an automation. Call `get_automation_schema` first for trigger event shapes, step type params, branch paths, and the resources map. - Create: omit `automation_id`. New automations start as draft; going live, pausing, and publishing stay human actions done in the editor. - Update metadata: pass `automation_id` with only the `automation` block. - Update a draft graph: pass `automation_id` with `triggers` and `steps`, which replaces the entire graph. Draft, never-published automations only. - Edit a published automation: its live graph cannot be replaced in place. Use `add_automation_step`, `update_automation_step`, `delete_automation_step`, or `reorder_automation_steps` for steps, and `save_automation_trigger` / `delete_automation_trigger` for triggers - these edit a staging copy, published from the editor. - Email content: send_email steps are created with placeholder content. Use `save_automation_email` afterwards to draft each step's body. When the response reports the automation live with unpublished changes, relay its `publish_instructions`.
Create or update the email for an automation's send_email step. Can be called multiple times - each call replaces the full content. Uses the same HTML syntax as post authoring - call `get_automation_schema` for the email workflow and `learn_post_authoring` for the full HTML reference. To start the email from a saved design instead of raw HTML, pass `post_template_id` (from `list_post_templates`) in place of `html_content` - this applies the template's body, theme, and sender settings. Provide at most one of the two. To change only the subject line, preview text or email settings, omit both and the body stays as it is. Edits always go to the automation's staging (draft) workspace. When the response reports the automation live with unpublished changes, relay its `publish_instructions`. For read-edit-save cycles, call `get_automation_email_content` with `format: "editor_html"` first, edit that HTML, then send it back here.
Create or update a trigger on an automation. Automations support up to 3 triggers (any of which can enrol a subscriber). Always edits a staging workspace (a draft copy); the live automation is unchanged until a human publishes from the editor. Omit `automation_trigger_id` to create, passing `event` and its `main_conditions`. Pass `automation_trigger_id` to update, where live IDs map to their staging copy and only the fields you send change. To replace all triggers on a draft automation at once, use `save_automation`. To remove a trigger, use `delete_automation_trigger`. The change is saved to staging; publish the changes from the editor.
Update a single step on an existing automation's flow. Works on live (published) automations: the change is applied to a staging workspace (a draft copy) and only affects the live automation once you publish. Pass the live or staging `automation_step_id` to edit. Live IDs are mapped to their staging copy. Only include the fields you want to change. Call `get_automation_schema` for per-step-type param shapes. When the response reports the automation live with unpublished changes, relay its `publish_instructions`.
Ban or unban a community member. Banning blocks them from posting, commenting, and DMs; pass banned=false to lift a ban. Defaults to banning. Idempotent - a member already in the requested state is left unchanged.
Invite people to a publication's community from one of three audiences - a set of subscriber segments, a list of email addresses, or recommended subscribers. Pick one with `source` and provide its matching field. Invites are created and emailed in the background.
Get a publication's community: branding, sync toggles, tier gating, Luma calendar, and PWA. Returns `exists: false` when the publication has not created a community yet.
Get a community channel with its display position, default flag, and access gating: `gated` is true when members need a premium tier or a matching custom field value, and `tiers` lists the premium tiers required. `tier_gated` is the old name for `gated`.
Get a community member's profile, roles, and banned/removed status.
Get a publication's community overview metrics for a period: new members, posts created, comments and replies, and member participation, each with the period total and the previous period's total for comparison. Pass `breakdown` to drill into a ranked list instead of the overview: new_members (recent joiners), popular_channels, popular_members, or top_posts. For popular_channels/popular_members set `by` to rank by "posts" (default) or "comments".
Get a community post with its threaded comments and a summary of any attached content (embedded Post, Poll, Survey, or Podcast presented as type + id). Comments are paginated.
Learn the HTML contract for authoring community posts and comments with save_community_post, schedule_community_post and save_community_post_comment. Returns every supported node and mark with example HTML, plus the resource types a post can embed. Community content is deliberately simpler than newsletter posts: paragraphs, headings, lists, quotes, basic marks, images, emoji, member mentions, and link cards.
List a publication's community channels in display order. Each channel is flagged as default or not, with `gated` true when members need a premium tier or a matching custom field value, `tiers` listing the premium tiers required, and `tier_gated` as the old name for `gated`.
List the messages in a direct-message conversation, oldest first. Readable only when the community owner participates in the conversation; otherwise returns not found.
List the community owner's direct-message conversations, most recently updated first. Only conversations the owner participates in are returned. Use list_community_dm_messages to read one.
List a publication's pending community invites (people invited who have not joined yet), newest first. Optionally filter by a name or email search.
List a publication's community members (most recently joined first). Optionally filter by role, status (active, banned, or removed), or a name/email search.
List the comments on a community post, oldest first, with their nested replies. Paginated. Use this when you only need the comment thread for a post you already have.
List a publication's community posts as the publisher sees them, pinned first then newest. Includes removed posts (each carries a `removed` flag). Optionally filter to a single channel.
List a publication's community moderation queue - reports on posts and comments, ordered for review (pending first, then most-reported, then newest). Optionally filter by status or reportable type. Use resolve_community_report to act on a report.
List a publication's most engaged subscribers (ranked by email open and click rate) as candidates to invite to the community. Feed the returned subscription_ids into create_community_invite with source "recommended".
Pin or unpin a community post in its channel. Defaults to pinning; pass pinned=false to unpin. Idempotent - a post already in the requested state is left unchanged.
Dismiss a community report, marking it and every other pending report on the same content as resolved. Does not remove content - use ban_community_member to ban the author.
Create or update a publication's community. If the publication has no community yet, one is created; otherwise the existing community is updated. Omitted channel names seed a default "General" channel unless feed-first is enabled, in which case no discussion channel is created. Controls branding (name, description, colors), tier gating, newsletter sync, DMs, and the PWA.
Create or update a channel in a publication's community. Omit community_channel_id to create a new channel; provide it to update an existing one. Gate a channel behind premium tiers with tier_ids (empty array makes it open to all members).
Turn a community's notification emails on or off and set the From name members see. Members get these emails when something in the community concerns them: a message, a comment on their post, a reply, a reaction, a new follower, or a badge. They send from the publication's sending domain and share its reputation. Members choose which types they receive, so this tool does not change types, and the From address is not editable.
Create or update a community post, authored as the publication owner. To create, provide a community_channel_id; to update, provide a community_post_id (only the owner's own posts can be edited). Body content is HTML (matching the community editor) and is parsed to the stored rich-text state. Optionally embed another resource as an attachment when creating.
Create or update a comment on a community post, authored as the publication owner. To create, provide a community_post_id (and parent_comment_id to reply to another comment); to update, provide a comment_id (only the owner's own comments, within the edit window). Body content is HTML matching the community editor and is parsed to the stored rich-text state.
Send a direct message to a community member, delivered as the publication owner. Reuses the existing 1:1 conversation or creates one. Because the message is delivered in the background, this confirms the send rather than returning the saved message. Fails if DMs are disabled for the community or the recipient, or if the recipient is banned.
Replace a community member's role. Pass a single role to grant (admin, manager, or moderator); an empty array clears the role, leaving them a plain member. A member may hold at most one community role. A Community Manager can only appoint or remove Moderators. Community Admin requires a workspace Admin seat, and a workspace Admin cannot be moved off Community Admin. Community Manager requires a workspace Member seat and the Community Manager feature flag on the publication.
Retrieve detailed information about a condition set, including its full definition
Get the condition set schema: available condition types, attributes, operators, and structure.
List condition sets for a publication, with optional filtering by status
Create or update a reusable condition set. Call `get_condition_set_schema` first to learn the available condition types, attributes, operators, and structure. Archived condition sets cannot be updated.
List and search content tags for a given publication
Create or update a content tag used to organise posts on the website. If content_tag_id is provided, updates the existing tag; otherwise creates a new one. When updating, only the provided fields are changed - omitted fields keep their current values.
Get AI crawler and bot analytics for a publication's website — total, served, and blocked requests, AI-crawler share, per-crawler breakdown, top crawled pages, and response status codes.
Read the full content of a documentation article, blog post, glossary term, or runbook by its ID
Search beehiiv's documentation for support articles, blog posts, glossary terms, and runbooks
List external RSS feeds configured for a publication. Each feed defines an inbound RSS source with display preferences (thumbnail position, CTA style, which fields to show).
Create or update an external RSS feed for a publication. If external_rss_feed_id is provided, updates the existing feed; otherwise creates a new one. When creating, url is required. When updating, only the provided fields are changed - omitted fields keep their current values.
Get a single group subscription: its tier, purchaser, seat counts, per-seat price, list price, the tier's current group discount (it can differ from the discount at purchase), billing dates and the custom deal it came from, if any. Use list_group_subscription_seats for its members.
Get a single custom group subscription deal: its negotiated seats and price, contacts, payment method, purchase link or invoice URL, and the group subscription it created once redeemed. purchase_link_url is set only for card deals; invoice deals use invoice_url instead.
List a publication's custom group subscription deals: negotiated seat counts and prices offered to one organization through a purchase link or invoice. A redeemed deal links to the group subscription it created (see get_group_subscription). These are not paid subscription offers (see list_premium_offers).
List the seats of one group subscription: who was invited, who filled a seat, and who left. subscription_id is set once a seat is filled; pass it to get_subscription for the member's subscriber record.
List a publication's group subscriptions: one purchaser buys a block of paid seats on a tier and invites members to fill them. Each result includes the tier, seat counts, price, billing status and the custom deal it came from, if any. display_status is the status shown in the dashboard; status is the stored billing state. Use list_group_subscription_seats for the members of one group. Individual paid subscribers are in list_subscriptions, and tier pricing is in list_tiers.
Create or update the subscriber preference-center entry for a newsletter list. Each list has at most one preference, shown in the subscriber preference center so subscribers can opt in/out of that list without unsubscribing entirely. If the list already has a preference, this updates it; otherwise it creates one.
Get the opt-in confirmation email for a publication. Returns the subject line, preview text, and content as editor HTML. Call save_opt_in_email to make changes.
Update the opt-in confirmation email for a publication. Pass html_content to replace the body, subject_line to change the subject, and/or preview_text for the preview. Omitted fields remain unchanged. The confirmation button is automatic and will be added if missing from the HTML. Call get_opt_in_email to read the saved body.
Get an order's full details, including the buyer (subscription id, email, status), line items, refund status, and fulfillment status.
List orders (purchases) for a publication with optional filtering and pagination. Each order includes the buyer (subscription id, email, status) that placed it, so you can answer questions like 'who bought this product?' or 'what has this subscriber bought?'
List paywall configurations for a given publication. Each paywall has a unique ID that can be used with save_post or edit_post_content to insert a specific paywall break via data-id.
Delete a podcast episode and its audio files. A published episode disappears from the show's RSS feed. This cannot be undone through the API.
Delete a podcast show and all of its episodes. The show's RSS feed stops serving. This cannot be undone through the API.
Retrieve detailed information about a podcast episode
Get performance metrics for a specific podcast episode — total downloads, unique listeners with period-over-period growth, plus top listening apps, countries, and cities by download volume. Returns all-time stats by default.
Retrieve the plain text transcript for a podcast episode
Retrieve detailed information about a podcast show including categories, platform links, and episode count
Get performance metrics for a podcast show — total downloads, unique listeners with period-over-period growth, plus top listening apps, countries, and cities by download volume. Defaults to all-time stats; pass start_date/end_date for a custom range.
List the Apple Podcasts categories and subcategories available for podcast shows. Use the returned ids with save_podcast_show's categories parameter.
List episodes for a podcast show with optional filtering by status
List podcast shows for a given publication with optional filtering by status
Create or update a podcast episode. If podcast_episode_id is provided, updates the existing episode; otherwise creates a new one. When creating, podcast_show_id and title are required. Setting status to 'scheduled' requires scheduled_at and an uploaded audio file (audio is uploaded through the dashboard). Changing scheduled_at on an already scheduled episode reschedules its publish job automatically. To unschedule an episode, set status back to 'draft'.
Create or update a podcast show. If podcast_show_id is provided, updates the existing show; otherwise creates a new one. When creating, publication_id, title, description, and author are required. Setting status to 'active' publishes the show's RSS feed and requires artwork, at least one category, and at least one published episode.
Get a poll's details including choices and response counts
List individual poll responses showing which subscribers voted for which choice
List polls for a given publication with optional filtering by status and type
Create or update a poll. If poll_id is provided, updates the existing poll; otherwise creates a new one. When creating, name, question, and choices (at least 2) are required. Published (locked) polls cannot be updated.
Duplicate an existing post within its publication. Creates a new draft copy with " (N)" appended to the title, carrying over the body content, theme, audience targeting, content tags, and authors. The copy always starts as a draft - promotion/publish remains a human action via the beehiiv UI. Returns the new post in the same shape as `get_post`. To rename or change the copy, follow up with `edit_post`.
Duplicate an existing post template into a new template in the same publication. The copy includes the template's content, theme, settings, and metadata, with "(N)" appended to its name. Discover template IDs with `list_post_templates`.
Change an existing post's metadata (title, subtitle, audience, settings, SEO, content tags). This is a partial patch - only the fields you send change (e.g. send just `title` to rename, nothing else is touched). Edits land in the post's draft - a human publishes in the beehiiv UI for changes to go live. For the full typed shapes (recipients, web/email/seo settings, content tags) call `learn_post_metadata`. To change the post's body, use `edit_post_content`. To create a new post, use `save_post`.
Edit an existing post's body incrementally by targeting individual blocks - cheaper and safer than rewriting the whole body. Untouched blocks are left exactly as they are; only the blocks you touch are re-parsed. Read the body once with `get_post_content` (format: `editor_html`), then target a block in `operations`. Top-level blocks are stamped with `data-node-hash="<hash>"`; the response returns refreshed hashes so you can chain more edits without re-reading. A nested node is a valid target too - to change one paragraph inside a `section`, target that paragraph's `data-id` rather than replacing the whole section and re-sending its siblings. Target the hash by default: it is rejected if someone else changed that block since you read it, which an id is not. An edit elsewhere leaves a block's hash intact, so you can chain. Use an id when you mean to write over whatever the block now holds. Each block takes one operation per call, so a target may appear only once in `operations`. To replace a block and add content around it, put the extra blocks in that replace's `content`; for anything else, make a second call with the refreshed hash. When the body already has structure - a template scaffold, a sign-off/footer, or a placeholder section - place new content with `insertBefore`/`insertAfter` the relevant block, or `replace` the placeholder, rather than `insert position:"end"`, which lands content after the sign-off. `content` is HTML matching the editor's parse rules; call `learn_post_authoring` for the node and mark contract. Ad network opportunities and automated ad placements can be inserted - use the contract returned for `advertisementOpportunity` or `automatedAdvertisementPlacement`. Other editor-managed nodes already in the post (polls, sponsorships, and so on) round-trip fine, but adding or repointing them here needs their contract to say they are authorable. Atomic: if any target no longer resolves, nothing is applied and you are told to re-read. Edits land in the draft only - a human publishes in the beehiiv UI.
Update a post template's metadata. This is a partial patch - only fields you send are changed. To replace the template's body content, use `edit_post_template_content`. To create a new template, use `save_post_template`. Templates migrated from publication settings or created during onboarding are read-only.
Edit a post template's body incrementally by targeting individual blocks, or replace the entire body with `html_content`. For incremental edits, read the template once with `get_post_template_content` (each top-level block carries a `data-node-hash`), then target blocks in `operations`. Only the blocks you touch are re-parsed; untouched blocks survive byte-identical. Use `insertBefore`/`insertAfter` to place new content: `insertBefore` a block hash finds that block's position and prepends content before it. For full replacement, provide `html_content` - it replaces the entire body. Omit `operations` for full replace. `operations` and `html_content` are mutually exclusive; providing both raises an error. For supported HTML syntax and per-node parseHTML rules, call `learn_post_authoring`. Templates migrated from publication settings or created during onboarding are read-only. To update template metadata (name, subject line, etc.), use `edit_post_template` instead. For theme-level styling, use `save_post_template_theme`.
Get a post's metadata and configuration. Read its body with get_post_content.
Get the content of a post in a specified format (text, html, or editor_html), optionally simulating how it appears for different subscriber types
Read the publication's custom email footer - the block of content that renders at the bottom of every post's email, above the copyright line, mailing address, and unsubscribe link. `content` is canonical HTML in the shape `save_post_footer` accepts, so you can read it, change it, and send it straight back. Each top-level block carries a `data-node-hash` to target with its `operations`. Empty `content` with `active: false` means no footer has been written yet. This reads the publication-wide footer by default. Pass `post_theme_id` to read what a given theme renders, which is its own footer when it carries one and the publication's otherwise.
Get performance metrics for a specific post — email delivery, open rate, click-through rate, bounce rate, unsubscribe rate, and web engagement.
Get performance metrics for multiple posts in one call — per-post email delivery, open rate, click-through rate, unsubscribes, and web engagement. Returns a per-post summary; for the full single-post breakdown (bounce, suppression, split test) use get_post_stats.
Read a post template's body as canonical editor HTML - the shape `edit_post_template_content` accepts, so what you read here can be edited and sent straight back. Each top-level block carries a `data-node-hash` to target with its `operations`.
Read the theme design tokens a post renders with - fonts, colors, spacing, borders and backgrounds across text, tables, buttons, quotes, images and layout. `tokens` is the same map `save_post_theme` accepts, so you can read it, change one key, and send it straight back. `source` tells you where the theme comes from: `post` when the post carries its own override, `publication_default` when it is still riding the publication's theme and every publication change moves it too. A post created without a template inherits silently, so check `source` before a send. This reads the post's own styling. `learn_post_authoring` reports the publication default and the token vocabulary.
Learn the canonical HTML contract used by `save_post` and `edit_post_content` for a specific publication. Returns a TOC of every node and mark with example HTML and attribute names, plus the publication's valid merge tags ({{tag_name}} patterns) - enough to start writing without follow-up calls. Pass `node: <name>` for that node's parseHTML rules and full attribute table. Plan-gated widgets carry an `available` flag reflecting this publication's plan. Authorable ad network nodes include an example and placement instructions. Non-authorable nodes return a `note` and a null `example`: forbidden nodes are rejected on save, while other editor-managed widgets are accepted but will not render as authored - author those in the beehiiv editor. For typed config (recipients, web/email/SEO settings, content tags) call `learn_post_metadata` instead.
Reference for the typed config accepted by `save_post` (on create) and `edit_post` (on edit) — recipients, web/email/seo settings, content tags, and other top-level options. Mirrors the public Create/Update Post endpoints. For HTML/content shape, call `learn_post_authoring` instead.
List subscribers who clicked links in a post, with each subscriber's acquisition channel and source. Omit url_hash to return every click across all the post's URLs in one pass (each row includes its own url); or pass a url_hash from list_post_clicks to scope to a single URL. Returns individual subscriber clicks with timestamps.
Get per-link click breakdown for a specific post. Returns individual URL click counts (email and web) rather than aggregate post-level stats.
List the public reader comments on a published post (the comment section readers see on the live page), oldest first, with their nested replies - not the internal team comment threads left in the post editor, use list_post_editor_comments for those. Paginated on top-level comments. Admin-removed comments are excluded. Each comment reports its author (subscriber or moderator) and state (active / deleted_by_commenter).
List the team comment threads left directly in the post editor (the "leave a comment for team members" collaboration feature) - not the public reader comment section, use list_post_comments for that. Each thread reports its resolved state and its comments, oldest first, with the commenting team member's name. Works on drafts. Returns no threads if the post has never been opened in the collaborative editor. To find what a thread is attached to, call get_post_content with format "editor_html" and search for the thread id: it appears as data-thread-id on every span wrapping the commented text, and on a div wrapping a commented block. One thread splits across several spans when the commented text crosses other formatting, so carry all of them back unchanged when you edit the block, or the comment detaches and the editor resolves it. Only the editor starts a thread, so an anchor for any other id is dropped on write.
List per-subscriber engagement for a post. Each row reports a subscriber's status, their total opens and clicks, and the subscriber's acquisition channel and source. Each subscriber gets one status, ranked unsubscribed > clicked > opened > delivered, so a clicker reads as clicked and never as opened, and anyone who unsubscribed from this post reads as unsubscribed whatever else they did. Pass statuses: ["opened", "clicked", "unsubscribed"] for everyone who engaged with the post, ["clicked"] for everyone who clicked any link, and ["delivered"] for everyone who was sent it and did nothing.
List post templates for a given publication
List posts for a given publication with optional filtering by status and content tags
Create a new draft post. Promotion/publish remains a human action via the beehiiv UI. To change an existing post's metadata (title, settings, audience) use `edit_post`; to change its body use `edit_post_content`. Call `learn_post_authoring` for the canonical HTML contract behind `html_content`. It is generated from the post-editor Tiptap schema, so it cannot drift from the editor, and patterns outside it may be silently dropped on parse. Call `learn_post_metadata` for the typed config - recipients, web/email/SEO settings, content tags. When `post_template_id` is provided the template's content prepends `html_content`, so your blocks land after the entire template. A default template is usually a complete scaffold - header, body placeholder sections, and a sign-off/footer - not just an intro, so appending `html_content` drops your content below the sign-off, which is rarely intended. For a structured template, omit `html_content`, then read the post with `get_post_content` (format: `editor_html`) and use `edit_post_content` to slot the body into place: replace the placeholder section, or insert before the sign-off. The template also seeds settings (theme, sender, footer) plus its content tags and guest authors, each of which the corresponding option field overrides.
Write the publication's custom email footer incrementally by targeting blocks, or replace it entirely. For incremental edits, read the footer once with `get_post_footer` (each top-level block carries a `data-node-hash`), then target blocks in `operations`. Only targeted blocks are re-parsed; untouched ones survive byte-identical. Use `insertBefore`/`insertAfter` to place content: `insertBefore` a block hash finds that block and prepends content before it. For full replacement, provide `html_content` - it replaces the entire footer. Omit `operations` for full replace. `operations` and `html_content` are mutually exclusive; providing both raises an error. `html_content` and operation `content` are HTML matching the editor's parse rules; call `learn_post_authoring` for the node/mark contract. Footers hold text, images, buttons, and links only - no polls, ads, sponsorships, or dynamic content. For a clickable logo use `<figure data-type="imageBlock" data-src="..." data-url="...">`, since `<a><img></a>` keeps the image and loses the link. Warnings name anything else the parse dropped. Content stays hidden until `active` is true. By default this writes the publication-wide footer. A theme's own footer overrides that for all its posts, so pass `post_theme_id` to edit a theme's footer instead. For theme-level styling use `save_post_theme` (the `footer_*` keys).
Create a new reusable post template with metadata and optional body content. To update an existing template's metadata, use `edit_post_template`. To replace its body, use `edit_post_template_content`. `html_content` is parsed into native editable blocks against the same contract as `save_post`. Call `learn_post_authoring` for that contract - a TOC of every node and mark, or `node: <name>` for one node's parseHTML rules and attribute table. It is generated from the post-editor Tiptap schema, so patterns outside it may be silently dropped on parse. The template's own defaults - fonts, text and link colours, table gridlines, button design - are theme-level; set them with `save_post_template_theme`.
Set a post template's theme design tokens - fonts, colors, spacing, borders, and backgrounds across text, tables, buttons, quotes, images, and layout (the styling the editor's style sidebar controls). Pass `theme_data` as a partial map of token keys to values; only the keys you send change. Writes a per-template override seeded from the publication default, so the tokens you don't touch are preserved. Tables have no per-cell styling - set `table_border_width: 0` here to remove gridlines. Call `learn_post_authoring` for the token vocabulary and current values (see its `theme` section). This is the post-template counterpart of `save_post_theme`.
Set the theme design tokens for a single post - fonts, colors, spacing, borders, and backgrounds across text, tables, buttons, quotes, images, and layout (the styling the editor's style sidebar controls). This changes only the post you pass; the publication's theme and every other post are left untouched. Pass `theme_data` as a partial map of token keys to values; only the keys you send change. Writes a per-post override, forked from the publication default on first edit, so the tokens you don't touch are preserved. Read the post's current tokens back with `get_post_theme`. Tables have no per-cell styling - set `table_border_width: 0` here to remove gridlines (inline `style` on a `<td>` is dropped on save). Call `learn_post_authoring` for the token vocabulary and current values (see its `theme` section). The `footer_*` tokens style the email footer; the footer's actual copy is separate - read and write it with `get_post_footer` / `save_post_footer`.
Set up an A/B test of email subject lines on a post that has not sent yet. The post's own subject line (`email_settings.email_subject_line` on `save_post` / `edit_post`) is the control; `subject_lines` are the alternatives tested against it, so each one must differ from it and from each other. This replaces the whole variant list: a subject line you omit is removed. Read the current list from `get_post` (`split_test.subject_lines`) and echo it back with your change. Only works while the test is still pending - once the post starts sending, the variants are locked. Results land in `get_post_stats`.
Search for stock/crypto tickers (cashtags) via StockTwits. Returns matching symbols that can be inserted as inline cashtag elements in post content via `save_post` or `edit_post_content`.
Get a single paid subscription offer, including its terms, the tiers and billing cadences it applies to, and how many times it has been redeemed. An empty tiers list means the offer applies to every tier, and an empty plans list means it applies to every billing cadence.
List a publication's paid subscription offers - percent discounts, free trials and paid trials that convert readers into paying subscribers. An offer with an empty tiers list applies to every tier, and an empty plans list applies to every billing cadence. These are not ad network offers (see list_ad_offers) or storefront product coupons (see list_product_coupons).
Replace the full set of tags assigned to a product. product_tag_ids is the complete desired tag list, not a delta - it replaces any existing assignments. Pass an empty array to clear all tags. IDs that are not tags of this publication are rejected.
Duplicate an existing product. Creates a draft copy with "(COPY)" appended to the name and a "-copy" slug, including its variant, pricing and redeemable.
Get a product's full details including variants, tags, and sales totals
Get product review moderation and display settings for a publication
List and filter publication discount codes using the same controls and fields as the products UI
List promotion codes, including deleted codes, for a publication-scoped product coupon
List and filter publication product reviews using the same controls and fields as the reviews UI
List and search publication product tags with assignment counts
List products for a given publication with optional filtering and sorting
Approve or reject a subscriber-submitted product review. Only the moderation status can be changed - reviews are never created or edited through this tool, since the review text and score are subscriber-authored content.
Create or update a product (a paid or free offering buyers can purchase). If product_id is provided, updates the existing product; otherwise creates a new one. When creating, name and asset_id are required (upload the image first with save_image), plus the url or file_url for the chosen product_type. Updates change any product field; the redirect url can only be changed for link products.
Create or update a product discount coupon. If product_coupon_id is provided, updates the existing coupon; otherwise creates a new one. Once created, a coupon's discount terms are synced to Stripe and can never change - update only supports renaming. To add redeemable codes to a coupon, use save_product_promotion_code.
Create a redeemable promotion code on a product coupon, or activate/deactivate an existing one. If product_promotion_code_id is provided, toggles the code's active state; otherwise creates a new code on the coupon given by product_coupon_id. Once created, a code's terms (amount, usage limits, restrictions) can never change - deactivate and create a replacement instead.
Update product review moderation and display settings for a publication. Lazy-creates the settings if this publication has none yet. Only the fields you pass are changed - omitted fields keep their current values.
Create or rename a product tag used to organize products. If product_tag_id is provided, renames the existing tag; otherwise creates a new one. Tag names are unique per publication (case-insensitive). To assign tags to a product, use assign_product_tags_to_product.
Get the publication settings that save_publication_settings accepts. Only settings currently available to this publication are returned, and the document is safe to round-trip unchanged.
List the beehiiv publication-category names accepted by save_publication_settings' general_info.tags field.
Partially update publication settings returned by get_publication_settings. Send only requested fields from its settings object. Omitted fields are unchanged; false and [] are real values; null clears only nullable fields.
Get an author's details by author ID
Get a publication's revenue broken down by feature - ad network, boosts, paid subscriptions, and digital products - plus the combined total, over a time period or an exact date range.
Get email engagement metrics (opens, clicks) for a publication over time, broken down by period.
Get a publication's profile and configuration. Members and admins also see subscriber tags. Admins also see each web domain, email domain, web redirect, and branded link with the status shown in domain settings.
Get a high-level overview of publication performance — active subscribers, engagement rates, subscriber growth, earnings, and top acquisition sources.
Get a publication's referral program details, including milestones and rewards
Get day-by-day subscriber growth for a publication: new subscribers, unsubscribes, resubscribes, and net change per day. Net is subscribed minus unsubscribed and excludes resubscribes. Only each subscriber's most recent resubscribe is counted, and a later unsubscribe removes it. Defaults to the last 28 days. For the total active subscriber count over time, use get_subscriber_history.
Get the publication's active subscriber count over time, matching the values shown in the dashboard's subscriber growth chart. Use this for historical or trend questions such as "how has my list grown year over year?" or "what did my subscriber count look like last quarter?"; for one window's totals and rates rather than a series, use get_publication_stats; for daily new subscribers and unsubscribes, use get_subscriber_growth. Each entry is the count as of that date, so the entry dated 2025-01-01 is the count at the end of 2024. The final entry is the count right now.
Get a tier's details by tier ID
List authors for a given publication
List all publications the authenticated user has access to
List tiers for a given publication with optional filtering by status
Create or update a guest author (publication-scoped byline). If guest_author_id is provided, updates the existing guest author; otherwise creates a new one. When creating, publication_id and first_name are required. The maximum number of guest authors per publication is set by the workspace plan. Cannot create or modify User-typed authors (those require a full signup flow).
Apply to a paid recommendation offer, asking that publication for permission to recommend them to your subscribers in exchange for a payout per subscriber you send them. Find offer_id with discover_publications - it is the `offer.id` on a publication that pays to be recommended. Applying normally puts nothing in front of your subscribers yet: the offer's owner reviews the application, and only an accepted one becomes a live recommendation. The exception is an offer whose `offer.auto_accepts_you` is true, which accepts on submission and starts recommending them to your subscribers straight away - confirm with the publisher before applying to one of those. Track an application with list_paid_recommendation_applications; once accepted it appears in list_recommendations with boosted: true. message is the pitch the reviewer reads, so say who your audience is and why they are a good match. The reviewer sees only your message and your publication, so applying without one gives them nothing to judge. Each publication can only submit a limited number of applications per month, and only one application per offer at a time.
Search the publications you could recommend, the same surface as the discover page in the app. Returns public-safe fields only. Use this to find a publication's ID before creating a recommendation. A publication that will pay you to recommend them carries an `offer`: - payout_per_lead_cents: what you earn per verified subscriber you send them, after the beehiiv cut. - eligible_countries: subscribers must be in one of these countries to be paid for. Empty means no restriction. - has_applied: you already have an open application to this offer. - auto_accepts_you: your publication already meets their auto-accept rules, so an application would go live immediately instead of waiting for review. Use type 'paid' to see only publications that pay, or 'free' for only those that do not. subscriber_count_at_least is a floor the app rounds down to, not an exact count.
Get engagement and spend insights for a publication's paid recommendation partners (publications you pay to recommend you), to identify which send high-quality, engaged subscribers and which underperform or should be canceled. Returns totals plus partners ranked by subscribers gained and by open rate (both top and under performers), and the single highest-growth and highest-paid partners. A partner needs at least 10 subscribers gained in the period to be ranked. Pass recommendation_id (the 'id' from list_recommendations with direction 'incoming') to also get that partner's figures as `partner`. Its status_counts split subscribers gained in the period: - verified: passed verification and were paid for. - pending: still in the verification window, including escrowed signups. - failed: did not pass verification and were not paid for. - pending_opt_in: have not confirmed a double opt-in subscription yet. - free: gained without a verification status. - deleted: acquired, then deleted. status_counts, open_rate and the engagement, blocked-by-country and total-subscriber figures need the recommendations network UI on the publication. Without it, `partner` carries the legacy all-time figures only. Rates are percentages from 0 to 100.
List AI-generated recommendation suggestions for a publication. Returns up to 4 high-quality publication suggestions that haven't been dismissed or already recommended.
List a publication's recommendations, both free and paid. Defaults to outgoing recommendations (publications you recommend to your subscribers). Use direction 'incoming' to see which publications recommend you. Each recommendation includes: - boosted: true for paid recommendations, false for free ones. - status: the perspective-aware state shown in the UI - 'active', 'paused_by_you', 'paused_by_them', 'insufficient_funds', 'pending', or 'removed'. - referred_subscription_count: subscribers driven (outgoing) or gained (incoming) through the recommendation. - For paid recommendations only: cost_per_lead_cents, plus earnings_cents (outgoing, what you have earned) or spend_cents (incoming, what you have spent). referred_subscription_count mixes every subscriber state. For the outgoing verified, pending and failed split, read `overview` with direction 'outgoing': the all-time totals shown above this list in the app, where pending includes escrowed and amounts are formatted currency strings. The incoming `overview` carries spend totals instead. For one incoming partner's split, use get_recommendation_partner_insights with its recommendation_id. Change a free recommendation with save_recommendation, a paid one with save_paid_recommendation. Find publications you could recommend, free or paid, with discover_publications. Applications that have not been accepted yet are not recommendations - they live in list_paid_recommendation_applications.
Accept or reject a pending application from a publication that wants to recommend you in exchange for a payout from your offer. Find application_id with list_paid_recommendation_applications using direction 'incoming', and read the applicant's audience quality with get_paid_recommendation_application first. Accepting starts a paid partnership: they begin recommending you to their subscribers straight away, and your offer's budget pays out for every verified subscriber they send. Confirm with the publisher before accepting on their behalf. Rejecting ends the application. The applicant is told, and rejected_reason is shown to them, so keep it civil and specific enough to be useful. Only pending applications can be reviewed. To pause or end a partnership you already accepted, use the paid recommendations dashboard in the app. save_paid_recommendation covers the other side only, the partnerships where you are the one recommending.
Pause, resume, or end an existing paid recommendation - a publication you are being paid to recommend to your subscribers. Find the recommendation_id with list_recommendations; paid ones have boosted: true. This tool only works on paid recommendations - use save_recommendation for free ones. status options, the same ones save_recommendation and the app use: - 'inactive' pauses it, hiding the recommendation from your subscribers and stopping earnings. Reversible. - 'active' resumes a recommendation you paused. - 'archived' ends the partnership permanently. It cannot be undone, so confirm with the publisher first and prefer 'inactive' if they may want it back. list_recommendations labels these the way the app does, so a paused one reads back as 'paused_by_you' and an archived one as 'removed'. Creating a paid recommendation is not available here: a paid recommendation only exists once a publication accepts your application. Find publications that pay with discover_publications and apply with apply_to_paid_recommendation.
Create, restore, update, or archive a free recommendation - a publication you recommend to your subscribers. Use discover_publications or list_recommendation_suggestions to find a recommended_publication_id first. The same (publication, recommended publication) pair is an upsert: an unknown pair creates a new recommendation, an existing one is updated. Omit reason to keep the current reason. Set status "inactive" to remove a recommendation (there is no delete - removed recommendations are kept inactive and can be restored). A publication can only have a limited number of active recommendations. To replace one when at the limit, set status "inactive" on an existing recommendation (see list_recommendations) before adding the new one. reason is shown to your subscribers, so write quality copy (max 500 characters). This tool only manages free recommendations. For paid ones (boosted: true in list_recommendations), use save_paid_recommendation to pause, resume, or end them.
List rewards configured on a publication's referral program. Use reward IDs with save_referral_milestone.
Create or update a referral program milestone. Milestones define thresholds (e.g. 5 referrals) that trigger a reward email. Each milestone requires a reward — use list_referral_rewards to find reward IDs. If milestone_id is provided, updates the existing milestone; otherwise creates a new one. num_referrals must be unique per referral program. html_content is the email body - HTML matching the editor's parse rules (call learn_post_authoring for the node/mark contract). The milestone editor supports a subset of the post editor: ads, polls and dynamic content are dropped, not authored here. If the reward's type is promo_code, the body must contain the {{reward_promo_code}} merge tag.
Update a publication's referral program settings. The referral program must already exist. Use get_referral_program to retrieve current settings. Only provided fields are updated — omitted fields keep their values. Pass enabled: false to disable the program, enabled: true to re-enable it.
Create or update a referral program reward. If reward_id is provided, updates the existing reward; otherwise creates a new one. When updating, only provided fields are changed. Rewards are referenced by milestones — use list_referral_rewards to see existing rewards. Note: promo code file uploads are not supported via MCP; use the beehiiv dashboard for those.
Add every subscriber in a segment to a newsletter list. Runs in the background against the segment's current members. Poll `get_subscription` (with include_newsletter_lists) or the list's `list_subscriptions` to confirm membership.
Retrieve detailed information about a segment
Get the Segment DSL reference: available fields, operators, syntax, and examples.
List segments for a publication
Trigger a recalculation of a dynamic or static segment to refresh its membership count and metrics. Recalculation runs in the background. Poll `get_segment` for `status` and `num_members` to track progress. Manual segments, and segments that are already recalculating, cannot be recalculated.
Create or update a subscriber segment. Call `get_segment_schema` first to learn available fields and syntax. If segment_id is provided, updates the existing segment; otherwise creates a new one. When creating, name and where are required. If conditions change, the segment is reprocessed.
List signup flows for a publication. Each flow defines an ordered set of URLs that new subscribers step through. Returns the materialized steps with external/modal metadata.
Create or update a publication's signup flow - an ordered set of URLs that new subscribers step through. Omit `signup_flow_id` to create, where the first flow on a publication is automatically marked default. Pass it to update, where only the fields you send change. urls must be ordered: any external URL (a URL outside the publication's domain) has to be last. Each url must be a valid HTTP(S) URL. Setting default: true makes this the publication's default flow and unsets the previous default.
Compose a single website page using the template-builder factory (tb) - deterministic, rule-validated output - and write it as that page's DRAFT content. The live published page is never changed; a human promotes via the beehiiv advanced builder. `category` must match the identified page's actual category, which guards against overwriting e.g. a `home` page with `archive`-shaped content. If tb's page-scoped rules report errors they come back so you can adjust the spec and retry.
Derive a complete BrandSpec from this publication's own brand - its web theme colors, fonts, name, and logo - via the template-builder factory. When the user wants pages composed in their existing brand, derive here first and pass the returned `brand_spec` through to `build_site_page` or `apply_site_package_draft`, adjusted or not. The derivation is deterministic and opinionated: the theme's five colors expand into the full 10-role palette with contrast-safe fallbacks (weak or missing colors are repaired), fonts map onto the editor's catalog, and three themes are generated. Palette, typography, and logo come from the publication; archetype, cards, motion, and copy land on neutral defaults - those are the user's creative choices, so adjust them conversationally before composing.
Change an existing website page's metadata - its name, visibility, paywall, SEO/social cards, and layout toggles. This is a partial patch: only the fields you send change (e.g. send just `name` to rename, nothing else is touched). Send at least one of `name`, `page_enabled`, `premium`, `metadata`, or `customization` - identifying the page alone leaves nothing to change. To read a page without changing it, call `get_page`. Edits land on the page record directly; body content is versioned separately. For the full typed shapes (the `metadata` and `customization` objects) call `learn_page_metadata`. On a website builder site a page turns on only once the publication has something for it to show - the same rule the editor applies before it offers "Show page" - so turning on Archive with no published post is refused rather than publishing an empty page.
Get the content gate settings for a page on a publication's website. Content gates hide content below the gate based on the visitor's subscription status. Reports the gate's type (inline or popup) and its audience settings. Identify the page with page_id, slug, or page_type; call list_pages to see them.
Get one of a website's site globals (navbar, footer, login_modal, signup_modal). Reads the draft you are editing, or pass site_version_id from list_site_versions to read what an earlier version held. Use save_site_global to write it back. Every node carries a `_hash`, and most carry an `attrs.id`: either addresses that node in a save_site_global `operations` edit, and an id survives a rewrite where a hash does not. A navbar's two trees are under `attrs.data.desktop` and `attrs.data.mobile`, not in `content`, and they mirror ids - so an operation on a navbar names its `viewport`. A website builder site draws its chrome from a preset, so there is no document: any valid `key` returns one `editable` list covering the navbar, footer and subscribe form together, read from the draft. With section customization it is grouped by region, as `{region, elements}` entries. Write each back with save_site_global `overrides`, under its `key` where it has one, else its `id`.
Get a website page's metadata and customization. Read the page's body with get_page_content.
On a website builder site, call it with just the page identifier: it returns an `editable` list of the nodes you may change, with the template value in `attrs` and the publisher's edit, if any, in `override` - what the page shows. A `scope: "site"` row is shared by every page. Only what would publish is listed, so a post feed with no posts is left out until it has content. Everything below describes an advanced builder site. Read a website page's body. `format: "html"`, the default, renders it under `html` for reading, each top-level block stamped with its `data-node-hash`; it omits card-layout attrs and a feed's rebuilt `data`. `format: "json"` returns native Tiptap JSON - the format `save_page_content` writes - so read with it before editing. Passing `node_id`, `node_hash` or `block_hash` reads json, whatever `format` says. A raw-HTML page returns its HTML verbatim in either format, the format `save_page_html` writes. The rest describes json. Every node, nested or not, is addressable by its `attrs.id` or its `_hash`, here and in `save_page_content`. Prefer the id for anything you intend to edit later: a hash is a digest of the node's bytes, so it retires the moment anything inside it changes - including one you did not make, since the editor re-saves the page whenever a human touches it. Use the hash when a write should fail if the block moved under you. Small docs return whole. Larger ones come back as an index of blocks (hash, id, type, chars); `type` and `chars` tell you which blocks the request concerns. A node still too large returns its own `children` index to drill into. A truncated response names the way to get the rest: `next_offset`, `unfetched`, `attr_chars`. A dynamic element (post feed, pricing, authors) keeps its per-card render template in `structureTiptapState`/`structureTiptapStateDraft`, returned as an `__elided__:...` marker. Pass the marker back untouched and `save_page_content` restores it; omitting the attr keeps it too. That template renders the cards, so sibling style attrs (`titleFontSize`, `titleTextColor`...) only reach what it leaves to inherit - a missing change lives inside it, so reveal it and edit it there. A post, products or podcast feed's `data` (`selectedPodcastShowEpisodes` on a podcast feed) is elided the same way: the server rebuilds it on every page load, so what it stores is its last render. Change what a feed shows through its group, filter and count attrs; reveal `data` only to edit a pinned selection.
Get a publication's website structure, content index, and configuration. Returns site metadata, SEO, social links, analytics pixels, settings, content (posts, authors, categories, products, podcast episodes, forms), fonts, and redirects. Two keys cover the site outside its pages: `theme` holds the fonts and colors every page, global and post list inherits, and `globals` names the navbar/footer/modal documents, which the page tools cannot reach. A publication on the legacy website builder has no site these tools can read.
For an advanced builder site. A website builder site has no node tree to write - it takes `overrides` on save_page_content and save_site_global, whose descriptions list every type and attr it stores. The raw node contract behind `save_page_content`, in native Tiptap JSON - the exact shape it writes, so every example pastes straight into an operation. It is generated from the dream-editor Tiptap schema dump, so it cannot drift from the editor. This is the escape hatch, and the reference for a patch path. Compose from the primitives and recipes instead (learn_site_design topic: 'composition', if not already in your context); they derive the mechanics from the publication's brand so you write the idea, not the expansion. Reach here for a design the builders cannot express, or to look up an attribute you are patching. No args returns a TOC of every node and mark with attribute names, plus the list of page_categories. Pass `node: <name>` for the full attribute table, or `category: <value>` (e.g. 'home', 'authors-show') for that page's required dynamic nodes, which auto-fill with live data, its record_bindings, the `attrs.mergeTag` values that bind ordinary nodes to the record a dynamic page renders, and a worked example derived from the editor's own default content. Drop a binding and the page keeps rendering, showing your placeholder copy on every record.
Reference for the page metadata accepted by `edit_page` - name, visibility, paywall, the SEO/social-card `metadata` object, and the layout `customization` object.
Design intelligence for building beautiful beehiiv site pages - read overview first, then topic deep-dives; content is generated from the template-builder's rulebook and knowledge base.
List one page's body history, newest first. Address the page by `page_id`, `slug` or `page_type`, the same way edit_page and save_page_content do. Every content write leaves a version behind, so this is how you find what a page looked like before a change - including changes from earlier turns. `restorable` is false for the draft. Pass an id from here to restore_version to put that body back. Bodies are not returned here because they are large - pass an id to get_page_content to read one first.
List a publication's website pages (home, subscribe, custom pages, etc.) with their slugs, metadata, and publish state. Use get_page_content to read a single page's body.
List a publication's website redirects (old_path → new_path, status_code, redirect_type, wildcard, active). Redirects map old site paths to new ones. To create, update, or delete a redirect, use save_site_redirect.
List the sections, blocks and pages this publication has saved as reusable templates from its own site - the user's own library, separate from the beehiiv template gallery. Use it when they name something of theirs ("my standard CTA", "that button I saved"), which is content they built rather than something to compose. Each entry carries id, name, description, level, category, and studio_default. studio_default is true for templates from The Atlas Collective, the package Studio onboarding preselects. The body stays out because you never need it: pass the id to save_page_content as an operation's site_template_id and it is spliced in server-side.
List a website's version history: the site-wide snapshots and saves that cover the navbar, footer, modals, routes and settings. Newest first. For a snapshot, `version` sometimes names what triggered it (e.g. `global_swap`, `theme_apply`, `agent_edit`); most carry nothing, and some carry a human label. `restorable` is false for the draft, since that is the state you are editing. Pass an id from here to get_site_global to read what that version's navbar, footer or modals held, or to restore_version to roll the site back. For a single page's history, use list_page_versions instead - page saves are not reachable from a site version.
List a publication's websites. The live one is the published site, the one visitors reach at the publication's domain; the rest are unpublished drafts nobody can see yet. Every other site and page tool takes the `id` from here as its site_id. `builder` is where a site's pages come from. Most sites are on `website`, the default: it assembles pages from a template as it renders, so what is stored is not what a visitor gets. `advanced` is opt-in and stores the pages it renders, so they can be read back and edited. A `legacy` publication has a site row that renders nothing a visitor ever sees. Each tool states which of these it works on.
Roll a website back to an earlier version. Pass exactly one of `site_version_id` (from list_site_versions) or `page_version_id` (from list_page_versions). A site version restores the site-wide state - navbar, footer, modals, routes, settings - plus the body of every page it covers. A page version restores that one page's body; page history is not reachable from a site version, so undo a page write with `page_version_id`. The current state is backed up first, so a restore is itself undoable. Edits land in the draft only - a human promotes via the beehiiv advanced builder.
Update the audience settings of an existing content gate on a publication's website page. Scope and limits: - Updates an existing gate only. save_page_content can insert a top-level contentGate node, but its body is yours to author and will not match a gate added in the website editor. - Changes who the gate applies to (show_when + tier) and toggles its popup. It does not edit the gated content, and it cannot author or restyle a popup's content. - Targets a single gate; it will not act on a page that has more than one content gate. Identify the page with page_id, slug, or page_type; call list_pages to see them.
On a website builder site the navbar, footer and subscribe form come from a preset, so there is no document to compose. Send `overrides` there - a patch keyed as get_site_global returns them - and ignore everything below, which describes an advanced builder site. Write one of a website's DRAFT site globals - the navbar, the footer, or the login/signup modals. Pick it with `key`, and provide exactly one of `operations`, a design, or `content`. For an addition, a tweak, or "another one of these", use `operations`: targeted edits against the nodes `get_site_global` returned. An operation carries a `design` of its own, so you keep the builders and keep the rest of the global. A design composes the whole global, so it describes every item, not just the new one. Let a builder emit the node tree either way - it cannot fail on a miscounted bracket and the payload is a fraction of the JSON. Builders and their arguments are learn_site_design (topic: 'builders'). Content mode is the escape hatch for a shape no builder covers; read `get_site_global` first so you edit the shape the renderer expects. The navbar takes `desktop` and `mobile`, each {left, middle, right}. Its two trees are independent and mirror each other's node ids, so a design places every item into both and an operation names the `viewport` it edits. The footer and the modals take a flat `design` list: a footer composes into its sections, a modal into its body, wrapped in the login/signup shell for you. A footer background image goes on its section's attrs (`backgroundImage`, `backgroundDarken`, `overlayColor`), not the doc root. A modal needs a `loginForm` (or `signup`) and a `modalSwitchLink` to pass its own rules. Saves run the template-builder rulebook and safe issues are autofixed. A violation rejects the save with the list to adjust and retry; findings the global already carried return as `pre_existing` and never block. Tell the user about any fix in `fixes_applied` that changed something they did not ask for. Edits land in the draft - a human promotes via the beehiiv advanced builder.
Create a new custom website page on a publication's site. The page is created as a draft with an empty body; promotion/publish stays a human action via the beehiiv UI. Compose the body with `save_page_content`, which is gated by the design rulebook. To change an existing page's metadata use `edit_page`. Only custom pages can be created - default and dynamic pages (home, subscribe, etc.) already exist. Provide `slug` (the page's URL path, unique on the site and not a reserved path) and `name` (its label). For a nested page, pass `parent_path` (e.g. ["blog"] places it at blog/<slug>). Pass `uses_raw_html: true` for a page whose body is hand-written HTML instead of composed blocks - the navbar and footer are hidden and you write it with `save_page_html` rather than `save_page_content`. This is one-way and cannot be changed afterwards, so only choose it for a design the builders cannot express; you give up the theming and responsiveness they derive from the publication's brand. Initial metadata (visibility, paywall, SEO/social cards, layout toggles) can be set in the same call via `page_enabled`, `premium`, `metadata`, and `customization`. Call `learn_page_metadata` for the full typed shapes.
On a website builder site a page is assembled from a preset, so there is no body to compose. Send `overrides` there - a patch keyed by the node ids `get_page_content` returns - and ignore everything below, which describes an advanced builder site. Write a website page's DRAFT body. Provide exactly one of `operations` (targeted edits against nodes you read) or `design` (a tree of builders the engine expands against the publication's brand and validates in one hop). Edits land in the draft only - a human promotes via the beehiiv advanced builder. Design from `learn_site_design` (topic: 'composition') if that reference is not already in your context. Raw-HTML pages are not composable from blocks; write those with `save_page_html`. Reach for the smallest operation that expresses the change. Validation charges you only for the violations your edit adds, measured against the page as it stood before, so what you do not resend cannot be held against you: "just change the gradient" is a one-key `set_attrs`, while a wide `replace` makes you author of every attribute in that subtree and each one you drop is a new violation that blocks the save. A `replace` rejected over attributes you never meant to touch is a signal to retry as `set_attrs` on the node you actually meant. An `__elided__:...` marker from `get_page_content` (format "json") resolves back to its stored value on save, so pass it through untouched rather than substituting a value of your own. When the user names a template as theirs ("my standard CTA"), it is a saved one and `list_site_templates` has the id. A `design` replaces the entire body, so it must describe every section on the page: a one-node design discards every other block rather than adding that node. For an addition, a tweak, or "another one of these", use `operations` - which keeps the builders, since an operation can carry a `design` of its own. Saves are validated against the template-builder design rulebook and safe issues are autofixed, so the saved content may differ from what you submitted. Structural rules that would break the page reject the save with a violation list to adjust and retry; style findings return as `suggestions`, and issues the page already carried as `pre_existing_issues` - raise those if they help the user, and leave the page as it is otherwise. The response returns `fixes_applied` and refreshed `block_hashes` a follow-up edit can target with no re-read. Tell the user about any fix they did not ask for.
Create or update a publication's website redirect, mapping an old site path to a new one. Omit `site_redirect_id` to create; pass it to update, where only the fields you send change. Both paths are on the publication's own site, because the site can only redirect within its own domain. Give new_path as a path that starts with /, such as /new-page or /blog?tag=news. old_path is normalised to a leading slash. A wildcard redirect is auto-detected when old_path contains `*`. Redirects are always permanent (HTTP 301).
Set a website's builder-level settings. `get_site` returns the current values under `settings`, and only the settings you pass change. `placeholder_data_enabled` fills data-bound elements - post feeds, products, authors, testimonials - with sample records, so a publisher can see their design before they have real content. Recommend it when list_posts, list_products or list_authors show nothing to fill a feed with, and turn it on once the publisher agrees. It is the setting behind the builder's own "Enable placeholder data" and "Show placeholders" buttons. This is preview state: the published site never reads it, no page content changes, and turning it off restores the real feeds. The published-site equivalent is a `post` element's `hasFakeData` attribute, which does ship stock posts to readers - reach for this setting instead whenever the goal is to see a design. Say when you turn it on, and that real posts replace the samples once published.
Set a website's theme tokens - the fonts, colors, borders and shadows the whole site renders from. Every page, the navbar, the footer, the modals and the post list read these, so a site-wide restyle belongs here: "change every font to Times New Roman" is one call setting `header_font`, `body_font` and `button_font`. Editing elements one by one misses every region that inherits the theme. Styling one element or one page stays a page edit. Pass `preset` to apply a curated theme by name. Each one is a whole look - palette, fonts and corner radius - whose text/surface pairings clear WCAG AA and whose fonts the builder already offers, so prefer a preset over inventing hex values when the ask is a mood ("make it neon", "something editorial") rather than a named color. Pass `theme_data` as a partial map of token keys to values; only the keys you send change. Send both to start from a preset and override part of it. `get_site` returns the current values under `theme`. The write carries into the draft pages and the globals as well, and snapshots the old theme so a human can undo it from the beehiiv advanced builder. The restyle is complete when this call returns, apart from anything holding a hand-set value of its own. The first write forks the publication's theme onto this site, leaving the publication's other sites untouched. A website-builder site takes this call too. There the write lands on the DRAFT theme and nothing reaches the live site until a human publishes. Its pages render from a preset, so there is nothing to carry the write into and no undo snapshot.
Get full details for a sponsorship agreement item, including advertiser info, placement details, and scheduled date. Use after list_sponsorship_agreement_items to get richer context before inserting a Direct Sponsorship widget into a post.
Get full details for a Direct Sponsorships product, including price, product type, booking message, and recurrence. Use after list_sponsorship_products, or before updating one with save_sponsorship_product.
List sponsorship agreement items (scheduled ad placements) for a publication. Each item represents a single sponsored placement with its advertiser, placement type, and scheduled date. Use this to discover upcoming sponsorships that can be inserted into posts as Direct Sponsorship widgets via save_post or edit_post_content.
List Direct Sponsorships products (the ad formats/placements a publisher sells to advertisers, e.g. 'Newsletter Primary Sponsor') for a publication. Matches the Products page under Direct Sponsorships in the dashboard. Use get_sponsorship_product for full detail and save_sponsorship_product to create or update one.
Create or update a Direct Sponsorships product (the ad formats/placements a publisher sells to advertisers, e.g. 'Newsletter Primary Sponsor'). Matches the Products page under Direct Sponsorships in the dashboard. Distinct from a sponsorship agreement (an actual deal). If product_id is provided, updates the existing product; otherwise creates a new one. When creating, name and price_cents are required.
Get a subscribe form's settings, theme design tokens (a flat token map matching save_subscribe_form_theme's input), and custom field bindings. Returns unpublished draft edits when present, otherwise the live theme.
Compare a subscribe form's unpublished draft against its live theme. Lists every token that differs (live vs draft value) - i.e. exactly what publishing the draft from the website editor would change. Returns no changes when there is no draft.
Reference for editing subscribe forms: the draft/publish workflow, allowed render and trigger values, trigger_config shapes, and the full theme token vocabulary grouped by section. Read this before calling save_subscribe_form or save_subscribe_form_theme.
List a publication's subscribe forms with summary metadata. Use get_subscribe_form for a form's full theme and custom fields.
Create a new subscribe form, or update an existing one's name, success behaviour, and display rules. Pass subscribe_form_id to update an existing form (publication_id is not needed and is ignored if also given); pass publication_id alone to create a new form. name applies immediately; display and success settings write to the DRAFT and go live when it is published from the website editor. Edit visual design with save_subscribe_form_theme; see learn_subscribe_form_authoring for trigger_config shapes.
Edit a subscribe form's theme design tokens. Always writes the DRAFT theme - changes are not live until the draft is published from the website editor. Pass theme_data as a partial map of token keys to values (unspecified tokens are left untouched). Use reset_to_defaults to revert tokens to their default. Call learn_subscribe_form_authoring for the token vocabulary.
Get a subscription's details by subscription ID
Get full details for multiple subscriptions in one call — email, status, tags, tiers, custom fields, and per-subscriber engagement stats. Returns a per-subscription entry plus a not_found list. For a single subscriber use get_subscription.
List custom fields for a given publication with optional filtering by kind
List one subscriber's email activity across every post in the publication, most recent first. Each row gives the post, the event, when it happened, and for a click the URL that was clicked. Use this to build an interest profile for one subscriber from the posts they open and the links they click; for one post's whole audience use list_post_subscriber_engagement instead. event_type takes one value, so omit it to get opens and clicks together in one pass.
List subscriptions for a publication with optional filtering and pagination.
List subscriber tags for a given publication
Create or update a publication-level custom field used to capture extra data on subscribers. If custom_field_id is provided, updates the existing field; otherwise creates a new one. When creating, name and kind are required. The kind cannot be changed after creation.
Create or rename a subscriber tag. If tag_id is provided, updates the existing tag; otherwise creates a new one. When creating, publication_id and name are required. Tag names are normalized to lowercase and must be unique per publication.
Get a survey's details including questions, options, and aggregate response data
List individual survey responses with per-question answers
List surveys for a given publication
Create or update a survey (form). If survey_id is provided, updates the existing survey; otherwise creates a new one. When creating, title is required. To publish, set status to "live" (requires at least one question with 2+ options for multiple choice). Use type to place the survey in a publication lifecycle slot (subscribe/unsubscribe/downgrade) or detach it (standalone).
Add or update a question on a survey. Use `list_custom_fields` to find valid custom_field_id values before adding questions. For multiple_choice or dropdown types, the custom field must be of kind "string" or "list". Pass `options` to set the answer choices for multiple_choice/dropdown questions, when adding or updating. Options are additive: listed values are added, existing ones are left untouched. Options for a list custom field are stored on the field, keeping it in sync. Set `multi_select` to true to let subscribers pick more than one answer. It is only valid for multiple_choice questions backed by a list custom field.
Create or update a paid tier's metadata (name, description, redirect URL, gifting). If tier_id is provided, updates the existing tier; otherwise creates a new one. When creating, name is required. Pricing is managed separately - use the beehiiv dashboard to add or change prices.
Get the high-level website analytics overview for a publication.
Get a ranked website analytics breakdown grouped by dimension. The `data` block is keyed by the underlying metric (e.g. `data.page_views` or `data.sessions`).
Get the schema for website analytics conditions, used to build the conditions parameter for website analytics tools.
Get identity and access context for the authenticated user
Get an agent skill with its instructions, such as brand guidelines.
Get details about the current workspace, including whether beehiiv has disabled its sending or disabled or frozen its paid recommendations
List the workspace's agent skills: saved instructions beehiiv's agent loads on demand, such as brand guidelines or house style. Workspace skills are shared with the team; private skills are visible only to their creator. Skills with visibility "repo" are shared by beehiiv for every workspace. Call get_skill for a skill's instructions.
Update the authenticated user's own author profile: name, bio, avatar and social links. This writes the profile of the person whose credentials this session uses, and nothing else. It cannot edit another team member's profile: that profile belongs to that person and is shared across every publication they write for, so there is no admin path to it. To edit a byline the publication owns, use save_guest_author instead. Only the fields you pass are changed. Call get_current_user first to see what is already set. Social links must be full HTTPS URLs, e.g. https://linkedin.com/in/janedoe. Older profiles may hold links saved without a scheme; add https:// before passing one back.
Create or update an agent skill: saved instructions beehiiv's agent loads when relevant, such as brand guidelines (palette, fonts, logo rules, voice, do and don't rules) kept in a local file. Omit id to create; pass an id from list_skills to update. Only the fields you pass are changed. Instructions are plain text or markdown, up to 10000 characters, and replace the whole document, so editor formatting becomes the markdown text get_skill returns. Workspace visibility needs a workspace admin.
One endpoint, the same key, whichever client you use.
~/Library/Application Support/Claude/claude_desktop_config.json (Mac) · %APPDATA%\Claude\claude_desktop_config.json (Windows)
Replace API_KEY with your own key.
Already have an "mcpServers" section in your config? Just add the server entry inside it.
Discovery, routing, credentials, tool scoping and execution logs all happen at the gateway→connections stay ACTIVE with no work from you
Beehiiv MCP runs through a gateway that holds the credentials, scopes the access and records every call.
Managed auth, hosted MCP servers, and every Beehiiv MCP tool your agent needs.
Free to start.