Lnkbase Newsletter Docs

#REST API v2

Manage subscribers, posts, segments, automations, tiers and more from your own code. The complete, always-current endpoint reference lives at /docs/api on your instance, and the machine-readable spec at /api/v2/openapi.json.

#Authentication

Create a key in Settings → API keys & MCP (admins only). Keys are shown once; revoke them any time.

curl https://YOUR_APP/api/v2/publication \
  -H "Authorization: Bearer oh_live_XXXXXXXX"

Each key belongs to one publication. Use either form of base URL:

#Responses and errors

Success: {"data": ...}. Lists add next_cursor and has_more.

Errors: HTTP status plus

{"error": {"code": "not_found", "message": "Subscriber not found."}}

Codes: bad_request (400), unauthorized (401), forbidden (403), not_found (404), conflict/already_published/not_enrolled (409), invalid_email (422, includes suggestion for typos), rate_limited (429), internal_error (500).

#Pagination

Lists use keyset cursors so they stay fast at millions of rows:

curl "https://YOUR_APP/api/v2/publication/subscriptions?limit=100"
# → {"data":[...], "next_cursor":"MTcz...", "has_more":true}
curl "https://YOUR_APP/api/v2/publication/subscriptions?limit=100&cursor=MTcz..."

limit is 1–100 (segment member lists allow up to 500). Never build cursors yourself.

#Rate limits

180 requests per minute per key. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). Over the limit you get 429 with Retry-After.

#Subscribers

# create (validated; double opt-in follows publication settings)
curl -X POST https://YOUR_APP/api/v2/publication/subscriptions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"email":"reader@example.com","name":"Ada","utm_source":"partner","tags":["vip"],"custom_fields":{"plan":"pro"}}'

# get by id or email (includes tags and recent activity)
curl https://YOUR_APP/api/v2/publication/subscriptions/reader@example.com -H "Authorization: Bearer $KEY"

# update
curl -X PATCH .../subscriptions/SUB_ID -d '{"tier":"premium","custom_fields":{"plan":"team"},"add_tags":["customer"]}'

# unsubscribe or delete
curl -X PATCH .../subscriptions/SUB_ID -d '{"status":"unsubscribed"}'
curl -X DELETE .../subscriptions/SUB_ID

Options on create: send_welcome_email (default true), reactivate_existing, double_opt_override: "off" (skip confirmation for already-verified emails), referral_code, list_ids, tier.

Custom field values: GET/PUT /subscriptions/{id}/custom_fields. Definitions: GET/POST/PATCH/DELETE /custom_fields. Tags: GET/POST /subscriptions/{id}/tags, DELETE /subscriptions/{id}/tags/{tag}, GET /tags.

#Posts and the send API

# create a draft from Markdown (or "blocks", or "html"), optionally from a template
curl -X POST .../posts -d '{"title":"Weekly #42","markdown":"## Hello\n\nThis week…","template_id":"TPL_ID"}'

# append / prepend content
curl -X POST .../posts/POST_ID/content -d '{"position":"append","markdown":"### P.S.\nSee you next week"}'

# rendered HTML: email vs web, free vs premium
curl ".../posts/POST_ID/html?format=email&audience=free"

# test, then send now or schedule — with targeting and a subject A/B test
curl -X POST .../posts/POST_ID/test -d '{"emails":["me@example.com"]}'
curl -X POST .../posts/POST_ID/send -d '{"segment_ids":["SEG"],"ab_test":{"subjects":["A","B"],"sample_pct":20,"wait_minutes":60}}'
curl -X POST .../posts/POST_ID/send -d '{"scheduled_at":"2026-11-01T14:00:00Z"}'

Stats per post (GET /posts/{id}/stats): delivered, unique opens and clicks (bot clicks excluded and counted separately), rates, unsubscribes, web views, A/B variants, top links and a mailbox-provider breakdown.

When a post is created from a template, the template's current content is copied in and its style is linked — later template style changes apply to new sends.

#Everything else

AreaEndpoints
Segmentslist, create (rules JSON), get, GET /segments/{id}/subscribers, recalculate, delete
Listslist, add/remove subscriber
Automationslist, get (with per-email stats), create, edit (draft), publish, pause/resume — see below; POST /automations/{id}/journeys (enroll by id or email), list journeys
Tierslist, create, update (pause/resume), archive; GET /premium_subscriptions; grant/cancel premium on a subscriber
Referral programmilestones and totals, leaderboard, per-subscriber referrals
Polls & surveyslist, live results/aggregates, individual responses; POST /polls (incl. trivia), PATCH /polls/{id} (close/reopen), POST /surveys, PATCH /surveys/{id}
Webhookslist, create (returns signing secret), update/upgrade version (PATCH), delete, GET /webhooks/versions, delivery log with every attempt (?expand=attempts, GET /webhooks/{id}/deliveries/{did})
Commerceorders, products
TransactionalPOST /transactional/send, message log (GET /transactional/messages, /transactional/messages/{id}), templates (GET/POST /transactional/templates) — see below
Podcastsshows, episodes with downloads
Statssummary (with plain-English sentence), growth series, earnings
Templateslist, get

#Automations

Automations have a live version (what runs) and a draft version (what you edit), exactly like the visual builder. Creating or editing never touches running journeys until you publish.

API=https://YOUR_APP/api/v2/publication; AUTH="Authorization: Bearer $KEY"

# create (saved as a draft); "activate": true publishes it and turns it on
curl -X POST $API/automations -H "$AUTH" -H 'content-type: application/json' -d '{
  "name": "VIP welcome",
  "trigger": {"type": "tag_added", "config": {"tag": "vip"}},
  "steps": [
    {"type": "send_email", "config": {"subject": "Welcome, VIP", "markdown": "Thanks for **everything**."}},
    {"type": "delay", "config": {"amount": 2, "unit": "days"}},
    {"type": "add_tag", "config": {"tags": ["welcomed"]}}
  ]
}'

# edit — name/description/allow_reentry apply now; trigger/steps go to the draft
curl -X PATCH $API/automations/AUTOMATION_ID -H "$AUTH" -H 'content-type: application/json' -d '{"steps": [...]}'

# publish the draft (journeys in progress continue on the new version, matched by step id);
# {"activate": true} also turns a draft/paused automation on
curl -X POST $API/automations/AUTOMATION_ID/publish -H "$AUTH" -H 'content-type: application/json' -d '{"activate": true}'

curl -X POST $API/automations/AUTOMATION_ID/pause  -H "$AUTH"   # subscribers wait where they are
curl -X POST $API/automations/AUTOMATION_ID/resume -H "$AUTH"

GET /automations/{id} returns the live trigger/steps, has_draft, and the draft version when one exists. Trigger types and step types are listed in the OpenAPI document; unknown types return 400 bad_request.

#Transactional email

Send receipts, password resets and notifications through the publication's transactional sender (Mailchimp Transactional / Mandrill style). One recipient per call.

curl -X POST $API/transactional/send -H "$AUTH" -H 'content-type: application/json' -d '{
  "to": "reader@example.com",
  "template_id": "order-receipt",                 # or "subject" + "html" | "markdown" | "text"
  "merge_vars": {"order": {"id": "1001", "total": "$29.00"}},   # {{order.id}} in subject and body
  "reply_to": "support@example.com", "tags": ["receipt"],
  "idempotency_key": "order-1001",                # a repeat returns the original message (200, "duplicate": true)
  "track": true,                                  # open + click tracking (off by default)
  "attachments": [{"filename": "receipt.pdf", "content": "<base64>", "type": "application/pdf"}]
}'
# → 201 {"data": {"id": "tx_…", "status": "sent", "message_id": "…", …}}

Errors: 400 invalid_recipient | bad_request, 404 not_found (template), 402 suspended, 429 rate_limited (60/min per key) or 429 daily_cap (200 per 24 h on Free, 5,000 on paid plans). Transactional mail does not count toward the plan's monthly newsletter emails; newsletter unsubscribes don't block it, transactional bounces/complaints do. Details: Store sync, transactional email & Inbox.

#Segment rules

{"op":"and","rules":[{"field":"tag","cmp":"has","value":"vip"},{"field":"last_open_at","cmp":"within_days","value":30}]} — groups can nest. Fields include email, status, tier, source (incl. magic_link), utm_*, created_at, engagement, opens/clicks, tag, custom ({"key","value"}), opened_post, clicked_url, automation (started/not_started/completed), segment, list, poll, survey, purchased, event_registered, trial, subscription_status and condition_set, plus store purchases from Shopify / WooCommerce / Stripe: ext_purchased (product name, SKU or id; contains / not_contains), ext_platform, ext_total_spent, ext_orders_count, ext_last_order, cart_abandoned.

#Webhooks

Configure endpoints in Settings → Webhooks or via the API. Each delivery is a POST with JSON in the endpoint's payload version:

// 2026-11-01 (latest — new endpoints get this)
{"id":"…","object":"event","type":"subscription.created","version":"2026-11-01","created_at":"2026-11-01T12:00:00.000Z","publication_id":"…",
 "data":{"subscriber":{"id":"…","email":"…","custom_fields":{"role":"founder"},"utm":{"source":"x","medium":"social","campaign":null},"created_at":"2026-11-01T12:00:00.000Z"}}}

// 2026-10-01 (original)
{"id":"…","type":"subscription.created","version":"2026-10-01","created":1793534400,"publication_id":"…",
 "data":{"subscriber":{"id":"…","email":"…","custom":{"role":"founder"},"utm_source":"x","utm_medium":"social","utm_campaign":null,"created_at":1793534400000}}}

#Payload versions

Every endpoint is pinned to a version, so a receiver built against one shape keeps getting that shape. Events are rendered from the oldest shape through each version's upgrade step up to the endpoint's version. The changelog and a sample payload per version are in Settings → Webhooks and at GET /webhooks/versions.

VersionChanges
2026-11-01created (Unix seconds) → created_at (ISO 8601) plus object: "event"; every *_at inside data is ISO 8601; data.subscriber matches the REST subscriber (custom_fields, utm: {source, medium, campaign}); order.paid nests data.order with amount: {value_cents, currency}; podcast.episode.* renames episode.duration → duration_seconds; post.sent renames recipients → recipient_count
2026-10-01Original format

Upgrade an endpoint from its page in Settings → Webhooks (or PATCH /webhooks/{id} with {"version":"2026-11-01"}); you can switch back at any time. Each request also carries an OpenHive-Version header.

Verify the OpenHive-Signature: t=<unix>,v1=<hex> header: compute HMAC_SHA256(secret, "<t>.<raw body>") and compare to v1. Failed deliveries retry with backoff for up to ~2 days. Every attempt (status code, response body or network error, latency, manual or automatic) is stored and shown in the delivery log; failed deliveries can be retried manually.

Events: subscription.created|confirmed|deleted|upgraded|downgraded|paused|resumed, tier.created|deleted|paused|resumed, list.subscribed|unsubscribed, post.sent|scheduled|updated, survey.response_submitted, poll.vote, podcast.episode.published|updated|archived|deleted, podcast.private_feed.accessed, order.paid, event.registered, referral.milestone, community.post.created.