PROGRAMMABLE RSS FEEDS · API-FIRST

The web, delivered where you already look.

Newsletters from your blog. Slack pings when a page changes. Webhooks when a dependency ships. emit is the small, cheap pipe between any source and any inbox — every stage a real RSS feed, so you can tap in or out anywhere.

Get an API key → Read the docs no card to start · $10 min top-up
  Atom 1.0 + JSON Feed 1.1 DKIM + DMARC OpenAPI 3.1 pay per use
I HAVE
I WANT
blog → rss → email

Keep writing on your own site. Every post becomes a clean email to your subscribers — around a thousand sends for $1.60.

$1.60 ≈ 1,000 emails
$1.60 per 1,000 emails
$0 per-subscriber fees
$10 minimum top-up · no card to start
3 calls to wire up the pipe
— go do this

Stop checking. Start subscribing.

Every pipe below is real and buildable in the next five minutes — one API call or a couple of clicks. Each one ends in an RSS feed URL, an email, a Slack message, or a JSON POST to your own code.

All use cases →
blogemail

Publish once. We handle the sending.

Every new post becomes a clean, readable email to your list, from your own domain. Around a thousand sends for $1.60.

Start this pipe →
pageslack

Stop refreshing. Get pinged.

Watch any URL — pricing pages, competitors, notices. Your team hears about changes in Slack, even if the page never had a feed.

Start this pipe →
changelogwebhook

Releases into your pipeline.

New versions of your dependencies fire a webhook — open an issue, kick off CI, tell the channel. Clean JSON, no poller to write.

Start this pipe →
— what's in the box

A sending pipe. Not a CMS.

Emit does the four boring things between "you published a post" and "someone opened the email," and nothing else.

$

No subscriber tax

10,000 readers you email twice a month shouldn't cost the same as a daily blast. You're billed per email sent — full stop.

1 credit · 1 email · prepaid
@

Your domain, real deliverability

Send from a verified domain over Resend or Amazon SES with DKIM, bounce & complaint handling, list-unsubscribe headers, and one-click opt-out baked in.

posts@yourdomain.dev · SPF · DMARC
{ }

Everything is an API

A typed OpenAPI 3.1 spec, bearer keys, idempotent writes, and signed webhooks for key events. No dashboard you're forced through — wire it in and forget it.

compact REST API · < 200 LOC client
— three calls to wire it up

Three calls to a live 200 OK.

Full reference →
~/blog · zsh ⌘ + K
  new post triggers 
01 Create an account POST /v1/accounts
# returns an emit_live_… key, shown once
curl -X POST api.rssemit.com/v1/accounts \
  -d '{"name":"My Blog",
       "email":"me@blog.dev"}'
02 Connect a feed POST /v1/feeds
# polled every 5m · conditional GET
curl -X POST api.rssemit.com/v1/feeds \
  -H "Authorization: Bearer $KEY" \
  -d '{"url":"…/rss.xml",
       "from_email":"posts@blog.dev"}'
03 Top up & go POST /v1/billing/topup
# $20 ≈ 12,500 emails. autopilot.
curl -X POST api.rssemit.com/v1/billing/topup \
  -H "Authorization: Bearer $KEY" \
  -d '{"amount_usd":20}'
# one-time: verify your email + sending
# domain (DNS), then new posts mail
# themselves
FOR YOU + YOUR AGENT

Built so your coding agent can wire it up alone.

We ship a typed OpenAPI spec, a one-shot MCP server, and a Claude Code skill that handle domain verification, DNS records, subscriber import, and the first broadcast — in a single conversation.

Claude Code skill Codex CLI Cursor MCP server
~/blog · claude code
> add a newsletter to my blog using emit
Reading CLAUDE.md, found feed at /rss.xml.
Verifying domain blog.dev
emit.create_account → emit_live_••• issued
emit.verify_domain → 3 DNS records added to vercel.app
emit.connect_feed → posts@blog.dev · polling 5m
emit.import_subscribers → 1,284 from buttondown.csv
Done. Your next post will email 1,284 subscribers. Credits for 3.9k emails on file.
>
— pick your stack

One endpoint. Whatever language.

No SDKs to chase across versions. Bearer auth, JSON in, JSON out.

POST /v1/broadcasts copy
# Send a one-off broadcast to all confirmed subscribers
curl -X POST https://api.rssemit.com/v1/broadcasts \
  -H "Authorization: Bearer $EMIT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "subject":   "Shipping Emit v1.4",
        "body_html": "<h1>We shipped.</h1>",
        "feed_id":   "8Hk2q9f1-…"
      }'

# → { "id": "8Hk2q9f1…", "status": "queued", "recipient_count": 4218 }
// node 20 · zero deps
const r = await fetch("https://api.rssemit.com/v1/broadcasts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.EMIT_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject:   "Shipping Emit v1.4",
    body_html: "<h1>We shipped.</h1>",
    feed_id:   "8Hk2q9f1-…",
  }),
});

const { id, recipient_count } = await r.json();
console.log(`queued ${recipient_count} to ${id}`);
# python 3.11 · stdlib only
import os, json, urllib.request as u

req = u.Request(
    "https://api.rssemit.com/v1/broadcasts",
    method="POST",
    headers={
        "Authorization": f"Bearer {os.environ['EMIT_KEY']}",
        "Content-Type": "application/json",
    },
    data=json.dumps({
        "subject":   "Shipping Emit v1.4",
        "body_html": "<h1>We shipped.</h1>",
        "feed_id":   "8Hk2q9f1-…",
    }).encode(),
)

res = json.loads(u.urlopen(req).read())
print(f"queued {res['recipient_count']} to {res['id']}")
// go 1.22
body, _ := json.Marshal(map[string]any{
    "subject":   "Shipping Emit v1.4",
    "body_html": "<h1>We shipped.</h1>",
    "feed_id":   "8Hk2q9f1-…",
})

req, _ := http.NewRequest("POST",
    "https://api.rssemit.com/v1/broadcasts",
    bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer " + os.Getenv("EMIT_KEY"))
req.Header.Set("Content-Type", "application/json")

res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
# ruby 3.3
require "net/http"; require "json"

uri = URI("https://api.rssemit.com/v1/broadcasts")
req = Net::HTTP::Post.new(uri, {
  "Authorization" => "Bearer #{ENV['EMIT_KEY']}",
  "Content-Type"  => "application/json",
})
req.body = {
  subject:   "Shipping Emit v1.4",
  body_html: "<h1>We shipped.</h1>",
  feed_id:   "8Hk2q9f1-…",
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["id"]
$1.60 per 1,000 credits — an email costs one. Making feeds is free; you pay only when work runs. Full pricing →
— what gets sent

A readable email, not a marketing artifact.

Default template is plaintext-first, HTML-friendly, dark-mode safe, and respects your post's typography. Override it per feed with your own raw HTML via the feed's template_html.

preview posts@blog.dev → reader@hey.com
html · 41 kb
blog.dev · issue #042 · may 26

How conditional GET keeps polling cheap

We poll 14,000 RSS feeds every five minutes. Here's the ETag trick that drops 92% of those requests to a 304 — and why it matters for your bill.

The naive answer is "cache the feed body and diff it." That works until you're hitting 14k publishers per cycle, your egress bill stops making sense, and half of those publishers are running WordPress instances that hate you personally.

Continue reading →
you're receiving this because you subscribed at blog.dev · unsubscribe
source template.html
override per-feed
<!-- raw HTML, rendered with Jinja. Set
     per feed via template_html. Variables:
     title, body, url, unsubscribe_url -->

<h1>{{ title }}</h1>

<div>{{ body }}</div>

<a href="{{ url }}">
  Continue reading →
</a>

<hr>
<p>
  <a href="{{ unsubscribe_url }}">
    Unsubscribe
  </a>
</p>
— faq

Questions you'd ask before paying.

Pricing questions — credits, the free tier, bounces — are answered on the pricing page. If yours isn't either place, will@heltonlabs.com — a human reads it.

Can I get a feed as JSON instead of email?
Yes — that's a JSON broadcast. Point any feed URL, including one you don't own, at your endpoint and each new item arrives as application/json with an X-Emit-Event: sink.item header; the body is {"event","sink_id","name","item"}. Set the schedule to daily or weekly and emit batches them into a single sink.digest POST with an items array. No poller, no XML parsing, no cron — see RSS to webhook.
How is the RSS feed polled?
Every five minutes by default, with conditional GET (If-None-Match + If-Modified-Since). You can switch to on_publish mode and POST to /v1/feeds/{id}/ping from a webhook for instant delivery.
Can my coding agent set everything up?
That's the design goal. Emit ships a single-file OpenAPI 3.1 spec, an MCP server, and a Claude Code skill, so add a newsletter to my blog using emit in Claude Code or Codex can do DNS records, domain verification, subscriber import, and the first broadcast without you leaving the terminal.
What domains do you send from?
Yours. You verify a domain (we generate the DKIM, SPF, and DMARC records), and every send goes from: posts@yourdomain.dev. No <@rssemit.com> ever appears in your subscribers' inboxes.
Where does my data live?
Postgres in us-east-1. We never sell subscriber data — there's nothing in it for us, and it would tank the only thing we're selling.

Built for people who'd rather curl than click.

The dashboard exists. We just hope you never need it.