---
name: beehiiv
description: Subscribe contacts to a Beehiiv newsletter publication via the Beehiiv v2 API, from forms, scripts, or automations. Use when pushing email addresses into Beehiiv, adding subscribers programmatically, or wiring a signup form to a publication. Not for publishing to Ghost, Substack, or Listmonk: use ghost-publisher, substack-publisher, or listmonk-publisher. Not for writing the newsletter content: use copywriting or emails.
license: MIT
metadata:
  author: TechTide AI (Alex Cinovoj)
  provenance: original
  category: Content & Publishing
---

# Beehiiv Subscriber Push

One job: put an email address into a Beehiiv publication, with UTM attribution, and confirm it landed. Treat every subscribe call as an external send: real inboxes get welcome emails.

## Configuration

Two secrets, both from environment, never hardcoded:

- `BEEHIIV_API_KEY`: from the Beehiiv dashboard, Settings > API.
- `BEEHIIV_PUBLICATION_ID`: the `pub_...` identifier for the target publication.

API base: `https://api.beehiiv.com/v2`.

## Subscribe call

```
POST https://api.beehiiv.com/v2/publications/$BEEHIIV_PUBLICATION_ID/subscriptions
Authorization: Bearer $BEEHIIV_API_KEY
Content-Type: application/json
```

Body:

```json
{
  "email": "subscriber@example.com",
  "utm_source": "<SOURCE>",
  "utm_medium": "<MEDIUM>",
  "send_welcome_email": true,
  "reactivate_existing": true
}
```

Responses: `201` with a subscription object on success. `409` means already subscribed, treat as success for idempotent flows. `401` means bad or missing API key. `404` means wrong publication ID.

Shell one-liner:

```bash
curl -s -w '\n%{http_code}\n' \
  -X POST "https://api.beehiiv.com/v2/publications/$BEEHIIV_PUBLICATION_ID/subscriptions" \
  -H "Authorization: Bearer $BEEHIIV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"subscriber@example.com","utm_source":"<SOURCE>","utm_medium":"<MEDIUM>","send_welcome_email":true,"reactivate_existing":true}'
```

In Node or Bun, use native `fetch` with the same endpoint and read both secrets from `process.env`. No packages needed.

## Workflow

1. Confirm both env vars are set. Missing either: stop and report which one.
2. Validate the email format before sending. Reject obvious garbage locally; do not burn API calls on it.
3. Set `utm_source` and `utm_medium` on every call. Unattributed subscribers are unmeasurable subscribers.
4. Send the POST. Log status code and the returned subscription ID.
5. Map the result: 201 subscribed, 409 already present, anything else is a failure to surface, not swallow.
6. Batch inputs: dedupe first, then send sequentially with a short delay. Respect rate limits; back off on 429.

## Good vs bad

**Bad:** Loop over a CSV firing subscribes with no dedupe, no status handling, and `send_welcome_email: true` on a re-import. Existing subscribers get re-welcomed, failures vanish, attribution is blank.

**Good:** Dedupe the list, subscribe with source and medium set, treat 409 as already-in, collect failures into a retry list, and report counts: N new, M existing, K failed with reasons.

## Verification

Send one test subscription with an address you control. Expect HTTP 201 and a JSON body containing an `id` starting with `sub_`, then the address visible in the Beehiiv dashboard audience list within a minute. If you get 401, re-check the API key. If 404, the publication ID is wrong. If 201 but the dashboard shows nothing, confirm you are looking at the same publication the ID points to.

## Completion checklist

- [ ] Secrets read from environment, not hardcoded anywhere
- [ ] UTM source and medium set on every subscription
- [ ] 201, 409, and error statuses each handled explicitly
- [ ] Test subscription verified in the dashboard
- [ ] Batch runs report new / existing / failed counts

Any box unchecked: not done. Fix or say so.

## Footguns

- **Welcome email blasts on re-import.** `send_welcome_email: true` plus `reactivate_existing: true` on an existing list re-mails people who already got the welcome. Fix: set `send_welcome_email: false` for imports and migrations; keep it true only for genuine new signups.
- **Treating 409 as an error.** Retrying a 409 forever, or reporting it as a failure, corrupts run reports. Fix: 409 is a terminal, successful-enough state. Count it as "existing".
- **Committing the publication ID or key.** Both identify a real account. Fix: env vars only, and keep them out of logs and error messages.
