Create a feed
Register an RSS or Atom URL (need one? Start from the
developer feed catalog). Emit polls it on a schedule (or
on a webhook ping), tracks which items it has already seen, and emails any new
<item>
to your confirmed subscribers from your verified domain.
Each call creates a new feed and returns 201 Created. The same URL can be
registered more than once on an account.
curl -X POST https://api.rssemit.com/v1/feeds \ -H "Authorization: Bearer $EMIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://blog.dev/rss.xml", "from_email": "posts@blog.dev", "schedule": "on_publish" }'
{ "id": "3f8a1c2e-9b4d-4e7a-bf12-6c0d9a2e7f31", "url": "https://blog.dev/rss.xml", "title": null, "schedule": "on_publish", "from_email": "posts@blog.dev", "active": true, "last_polled_at": null, "last_status": null, "last_error": null, "digest_hour": 9, "digest_weekday": 0, "digest_tz": "UTC", "created_at": "2026-05-25T20:30:29Z" }
Body parameters #
| Field | Type | Description |
|---|---|---|
| url required | string · url | An HTTP(S) URL returning RSS 2.0 or Atom 1.0. |
| from_email required | string · email | Sender address. Its domain must be verified — see POST /v1/domains. The local part can be anything you own. |
| schedule optional | enum |
One of on_publish, daily, or weekly.
Default: on_publish (email each new item as it appears).
daily / weekly batch new items into a digest.
|
| title optional | string | A display name for the feed. Defaults to the feed's own title once polled. |
| template_html optional | string · html | A raw HTML string that overrides the default email layout for this feed. Omit to use Emit's maintained default template. |
| digest_hour optional | integer |
Hour of day (0–23) to send the digest. Only used for
daily / weekly schedules. Default: 9.
|
| digest_weekday optional | integer |
Day of week (0 = Monday … 6 = Sunday) for the
weekly digest. Default: 0.
|
| digest_tz optional | string · tz |
IANA timezone (e.g. America/New_York) that digest_hour /
digest_weekday are interpreted in. Default: UTC.
|
ETag +
Last-Modified returned by your feed and replays them on every poll. If your
origin responds 304, the request is free and we don't re-parse — 92%
of polls land here in practice. Make sure your CDN passes these headers.
Returns #
A feed object. The id is stable and safe to store — we never
recycle it, even after a delete.
Status codes
Smart feeds (watchers): anything → feed #
A smart feed — a watcher in the API — is the ingest half of the pipe:
sources + filter → feed. Describe an interest in plain language and emit
discovers matching sources, monitors them on your cadence, scores every new item against
the prompt, and serves the survivors at stable /w/{token}/atom.xml and
/w/{token}/feed.json
URLs. Or skip discovery and bring your own sources — feed URLs, or plain pages we watch by
link diffing — with an optional filter on top.
Before creating a feed, call POST /v1/watchers/sample with
{"url":"https://example.com/feed.xml"} and your bearer key. Returns
kind (feed or page), resolved_url, title, and up to
ten items with title, URL and published timestamp (null when unknown). Pages
use same-site link extraction. Sampling writes no source or item rows, establishes no
baseline and charges no credits; it uses direct fetching without paid rendering. Limited
to ten calls per account per minute (429 with Retry-After); unsafe, invalid or unreachable
URLs return 422. URLs are limited to 2,048 characters. MCP agents can call
sample_source with the same URL argument. Existing-feed preview instead
re-evaluates stored candidates.
curl -X POST https://api.rssemit.com/v1/watchers \ -H "Authorization: Bearer $EMIT_API_KEY" \ -d '{ "prompt": "security advisories affecting postgres or redis", "cadence": "6h", "threshold":"balanced" }' # filter feeds you already have (no discovery): curl -X POST https://api.rssemit.com/v1/watchers \ -H "Authorization: Bearer $EMIT_API_KEY" \ -d '{ "prompt": "only items about breaking changes or deprecations", "sources": ["https://blog-a.dev/rss.xml", "https://blog-b.dev/atom.xml"], "discover": false }' # broadcast the feed as a daily email digest: curl -X POST https://api.rssemit.com/v1/watchers/$ID/pipe \ -H "Authorization: Bearer $EMIT_API_KEY" \ -d '{"from_email": "digest@yourdomain.dev", "schedule": "daily"}'
Worth knowing: a source's first check records a silent baseline (no
back-catalog blasts); filtered-out items stay auditable via
GET /v1/watchers/{id}/items?include_suppressed=true with a score and one-line
reason each; POST …/items/{id}/feedback (more/less)
tunes the threshold; syndicated duplicates collapse on canonical URL; and a
summarize toggle writes an LLM summary onto each emitted item. New items also
fan out to your
webhook endpoints as
watcher.item events and to any
Slack, Discord, Teams, Telegram, Google Chat, and JSON-webhook sinks
subscribed to the feed. Filtering meters the same credit balance as sending: 1 credit per
5 judged items, 1 per summary, 25 credits per explicit discovery re-run, 2 credits per
JS-rendered page check (when enabled); creation, polling, and feeds out are free. Sink
deliveries get 250 free per account per calendar month (UTC), then 1 credit each.
Replacing a leaked feed URL
A feed's /w/{token}/atom.xml and /w/{token}/feed.json URLs are
bearer access: anyone holding one can read the feed. Rotating the token replaces both URLs
and keeps the Watcher, its sources, feedback and Reader history.
Every existing reader and downstream consumer of the old URLs stops working
(they receive 404 immediately), and nothing is rewritten for you. That includes
newsletters created with /pipe, which keep the old Atom URL until you update
them. Move your consumers to the URLs the call returns, then rotate. Existing tokens never
change unless you call it. MCP clients use rotate_feed_token.
curl -X POST https://api.rssemit.com/v1/watchers/$ID/rotate-feed-token \ -H "Authorization: Bearer $EMIT_API_KEY"
Push a feed to chat or your own code #
A sink subscribes to any feed URL — a feed emit built, your blog's
RSS, a feed you don't own — and pushes its new items somewhere.
kind: "slack", "discord", "teams" and
"google_chat" post to a channel through an incoming webhook (for Teams, the
URL of a Workflows “When a Teams webhook request is received” trigger; each
message is sent as an Adaptive Card); kind: "webhook" POSTs to any HTTP
endpoint as JSON.
For kind: "telegram", create a bot with
BotFather and add it to your group or channel with
permission to post. Use
https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>
as the destination URL. The chat can be a numeric ID (including negative group IDs) or an
@channel username. Treat this URL as a secret. Messages are plain text with
link previews disabled; templates and digest schedules work as for the other chat
destinations. The shared limit is 1,900 characters. See the
Telegram Bot API.
For kind: "google_chat", create an incoming webhook for the space under Apps
& integrations → Webhooks in Google Chat and use its URL,
https://chat.googleapis.com/v1/spaces/<space>/messages?key=<key>&token=<token>, as the destination. The host, path and both key and
token parameters are required. Treat this URL as a secret: the key and token
are redacted from stored errors and logs. Messages are plain text; templates and digest
schedules work as for the other chat destinations. The shared limit is 1,900 characters.
See the
Google Chat incoming webhooks guide.
curl -X POST https://api.rssemit.com/v1/sinks \ -H "Authorization: Bearer $EMIT_API_KEY" \ -d '{ "kind": "webhook", "url": "https://yourapp.dev/feed-events", "feed_url": "https://anyblog.com/rss.xml" }'
A webhook sink sends application/json with an
X-Emit-Event header of sink.item and a body of
{"event","sink_id","name","item"}, where item carries
guid, title, url, summary, and
published_at. Because the body is a fixed shape it takes no message template
— that's the one thing the chat kinds have and it doesn't.
Set schedule to daily or weekly (with
digest_hour, digest_weekday, digest_tz) and items
are held and sent as a single sink.digest POST carrying an
items array, instead of one request per item. The default is
on_publish.
Worth knowing: connecting a sink records a silent baseline, so it never
replays the feed's back-catalog; items are deduplicated by guid; any 2xx counts as
delivered and anything else is retried on the next cycle rather than dropped; and
POST /v1/sinks/{id}/test sends one real delivery and returns the exact body
it posted, so you can build your handler against it. Each account gets 250 free sink
deliveries per calendar month (UTC), then each delivery costs 1 credit; a digest counts as
one delivery and test sends are free. When the allowance is used and the balance
can't cover more, the sink pauses and holds items (none are dropped) until credits
are added; the allowance resets on the 1st. See
RSS to webhook for the full walkthrough.
Recover a failed signed account event
Account-event endpoints (/v1/webhook-endpoints) are separate from unsigned
JSON sinks: they receive HMAC-signed watcher.item,
subscriber.confirmed, and broadcast.sent events with a durable
delivery log and six attempts over about ten hours. Inspect
GET /, then re-queue a failed row with
POST /
(MCP: retry_event_delivery). This returns the pending delivery with a fresh
retry budget; the worker sends its original body, event identity, and signature. Pending
and delivered rows return 409 without changes; unknown or foreign endpoints
and deliveries return 404. Restore your receiver before retrying.
MCP server #
Emit hosts a remote MCP server at https://api.rssemit.com/mcp — there's
nothing to install. Point any MCP-aware client (Claude Code, Cursor, Codex) at the URL
with your API key. See
the agent setup guide and complete tool reference.
{ "mcpServers": { "emit": { "type": "http", "url": "https://api.rssemit.com/mcp", "headers": { "Authorization": "Bearer emit_live_…" } } } }
Authorization header — the same auth, validation, and rate
limits as the REST API. Your subscribers' data is never proxied through a third
party, and there's no package to keep updated.
Claude Code skill #
The skill is a single static file at https://rssemit.com/skill.md. Save it
into ~/.claude/skills/ and Claude Code will know how to wire Emit into any
blog repo — it reads your CLAUDE.md, finds your RSS feed, verifies your
sending domain (DKIM/SPF/DMARC), imports subscribers, and sends the first broadcast.
mkdir -p ~/.claude/skills/emit curl -fsSL https://rssemit.com/skill.md \ -o ~/.claude/skills/emit/SKILL.md # Then, in any repo: claude "add a newsletter to this blog using emit"