DynamiRSS

API documentation

DynamiRSS collects articles from a curated set of news channels and lets you search them with one request. Everything is JSON over HTTPS.

Base URL: https://api.rss.dynamiapps.com

Quick start

Create an account to get an API key, then send a search:

curl -X POST https://api.rss.dynamiapps.com/scan \
  -H "Authorization: Bearer $DYNAMIRSS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "climate", "limit": 10}'

Authentication

Send your key as a bearer token on every request. Your key is on your dashboard. Keep it on your server; anyone who has it can make requests as you.

Authorization: Bearer YOUR_API_KEY

/scan and /channels require a key. /categories and /health do not.

POST /scan

Searches the collected articles. The body is a JSON object, and every field is optional, but a request must include at least one of query, category, language, source, or a date field. The other fields only refine a search and cannot be used alone.

Filters combine as you would expect: an article has to pass all of them. The one exception is query together with category, which is controlled by category_mode.

{
  "query": ["interest rates", "inflation"],
  "category": "Business & Markets",
  "category_mode": "and",
  "language": "en",
  "exclude_source": "Reuters (YouTube)",
  "date_from": "2026-09-01",
  "has_image": true,
  "sort": "newest",
  "limit": 20
}

Narrowing results

FieldTypeDefaultDescription
categorystring | string[]—One category name or several. An article in any one of them matches. Names come from GET /categories and are case-insensitive.
category_mode"or" | "and""or"How category combines with query when both are sent. "or" returns articles that match either; "and" requires both.
sourcestring | string[]—Only return articles from these channels. Use the exact name from GET /channels; case is ignored.
exclude_sourcestring | string[]—Leave out articles from these channels. Same naming rules as source.
languagestring—Two-letter language code of the channel, such as "en", "es", "fr", "de", "pt", "ar" or "he".
has_imagebooleanfalseWhen true, only return articles that have an image.

A category match is based on what each article is actually about, not just which channel it came from, so a general news channel does not put every story into every category it covers.

{ "category": ["Cryptocurrency", "Artificial Intelligence"], "limit": 20 }
{ "source": ["BBC News - World", "Al Jazeera - All News"], "query": "election" }
{ "category": "Sports", "language": "es", "has_image": true }

Filtering by date

FieldTypeDefaultDescription
dateYYYY-MM-DD—Only articles published on this day. If sent together with date_from or date_to, date wins and the range is ignored.
date_fromYYYY-MM-DD—Only articles published on or after this day.
date_toYYYY-MM-DD—Only articles published on or before this day.

Days are UTC days, so an article published late in the evening in your timezone can fall on the next day. Articles with no publish time never match a date filter. A date field is enough on its own: { "date": "2026-09-08" } returns everything published that day.

Without any date field, a search covers the last 30 days. To search older articles, send date_from or date. How far back you can go depends on your plan: 30 days on Free, 1 year on Pro, and everything stored (up to 1 year) on Business. An earlier start date is moved up to your plan's limit rather than rejected, and the response's searched_from shows the start that was actually used.

{ "source": "BBC News - World", "date": "2026-09-08" }
{ "query": "earthquake", "date_from": "2026-09-01", "date_to": "2026-09-07" }

Sorting and paging

FieldTypeDefaultDescription
sort"balanced" | "newest""balanced""balanced" shows the latest match from each channel before any channel repeats. "newest" is plain newest-first.
limitinteger10How many articles to return. Maximum 200.
offsetinteger0How many articles to skip. Use with has_more for paging.
fetch_extrasbooleantrueWhether to fetch a missing image or body text from the article page before responding. Set to false for the fastest response.

The default "balanced"order keeps a single busy channel from filling the first page: it returns each channel's most recent match first, then each channel's second, and so on. Use "newest" for a plain timeline, which is usually what you want when filtering to one channel or one day.

To page, keep the same filters and raise offset by limit each time until has_more is false.

Response

{
  "query": "climate",
  "query_mode": "any",
  "search_in": "all",
  "category": null,
  "category_mode": "or",
  "language": null,
  "sort": "balanced",
  "source": null,
  "exclude_source": null,
  "exclude_query": null,
  "has_image": false,
  "date": null,
  "date_from": null,
  "date_to": null,
  "offset": 0,
  "limit": 10,
  "has_more": true,
  "cached": true,
  "searched_from": "2026-08-20T12:00:00+00:00",
  "total": 10,
  "articles": [
    {
      "title": "World leaders meet on climate targets",
      "url": "https://www.bbc.co.uk/news/...",
      "source": "BBC News - World",
      "categories": ["Climate & Environment", "Diplomacy"],
      "language": "en",
      "is_rtl": false,
      "summary": "A brief description of the article...",
      "content": "The start of the article body, up to 400 characters...",
      "image": "https://www.bbc.co.uk/images/photo.jpg",
      "published_at": "2026-09-08T12:00:00+00:00",
      "ai": {
        "summary": "World leaders met to discuss new emissions targets ahead of the summit.",
        "categories": ["Climate & Environment", "Diplomacy"],
        "framing": { "tone": "neutral", "note": "Plain, factual headline; leads with the official meeting." },
        "model": "claude-haiku-5-5"
      }
    }
  ]
}

The response repeats the filters that were applied, which is useful for checking how a request was understood. total is the number of articles in this response, not the number of matches overall; use has_more to know whether another page exists.

Article fieldTypeDescription
titlestringHeadline.
urlstringLink to the article. Unique per article.
sourcestringChannel name, as listed by GET /channels.
categoriesstring[]Categories this article was classified into.
languagestringTwo-letter language code of the channel.
is_rtlbooleanTrue for right-to-left languages such as Arabic and Hebrew.
summarystringShort description from the feed (up to 400 characters). May be empty.
summary_remainingnumberHow many more characters the feed's description had beyond summary. 0 when nothing was cut.
contentstringThe start of the body text (up to 400 characters). May be empty.
content_remainingnumberHow many more characters the body had beyond content. 0 when nothing was cut.
imagestring | nullImage URL, or null when none was found.
published_atstring | nullPublish time as a UTC ISO 8601 timestamp, or null when the channel gave none.
aiobject | nullAI analysis of the headline and opening: summary (one or two sentences, in the article's language), categories, framing (tone and a one-sentence note on how the story is presented) and model. null until the article has been analyzed, usually within minutes of being collected.

POST /brief

Writes a short AI briefing on a topic from the articles that match it. The body takes query (required) and any of the /scan filters, plus max_articles: how many matching articles the briefing reads (default 20, maximum 30). Each key point lists the articles it is based on, as 1-based positions in the returned articles. The briefing is written in the language of query.

Briefings are included in the Pro and Business plans and have their own monthly limit, separate from the request limit. A topic that matches no articles returns brief: null and is not counted.

curl -X POST https://api.rss.dynamiapps.com/brief \
  -H "Authorization: Bearer $DYNAMIRSS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "interest rates", "language": "en", "date_from": "2026-11-01"}'
{
  "query": "interest rates",
  "brief": {
    "headline": "Central banks hold rates as inflation cools",
    "overview": "Most central banks covered this week kept rates unchanged...",
    "key_points": [
      { "point": "The Fed held rates and signalled one more cut this year.", "article_ids": [1, 4] }
    ],
    "model": "claude-opus-5-5"
  },
  "articles": [ ... ],
  "usage": { "plan": "pro", "used": 12, "monthly_briefs": 100 }
}

GET /channels

Lists the news channels being collected. Use the name values with source and exclude_source. Optional query-string filters: category, language, and name (matches part of a channel name).

curl "https://api.rss.dynamiapps.com/channels?language=en&name=bbc" \
  -H "Authorization: Bearer $DYNAMIRSS_API_KEY"
{
  "total": 1,
  "channels": [
    { "name": "BBC News - World", "categories": ["General", "Diplomacy", "Europe"], "language": "en" }
  ]
}

GET /categories

Lists every category name you can pass as category. No API key needed.

curl https://api.rss.dynamiapps.com/categories
{ "total": 3, "categories": ["Artificial Intelligence", "Business & Markets", "Cryptocurrency"] }

Errors

Errors come back as JSON with an error message and one of these status codes:

  • 400: the request had no search criteria, or a date was not in YYYY-MM-DD form, or date_from was after date_to.
  • 401: the API key is missing or not valid.
  • 403: your plan doesn't include /brief.
  • 422: the AI declined to write a briefing on this topic. It isn't counted.
  • 429: your plan's monthly request limit (or, for /brief, briefing limit) is used up. The response also includes plan, the limit and used.
  • 500: something went wrong on our side.
  • 502 / 503: the AI briefing service is unavailable or not enabled. A failed briefing isn't counted.

An unrecognised value for query_mode, category_mode, search_in or sort is not an error; the default is used. Check the echoed fields in the response if results look off. A search that matches nothing returns 200 with an empty articles list.

WordPress

The DynamiRSS Blocks plugin adds a block that shows articles from this API with no code. Install it, paste your API key into Settings → DynamiRSS Blocks, and add the block to a page.

GET /health returns {"status": "ok"} and is handy for uptime checks.