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
}Searching by keyword
| Field | Type | Default | Description |
|---|---|---|---|
| query | string | string[] | — | Words to search for. Each term is split into words on spaces and hyphens; words shorter than 3 letters are ignored. Matching is whole-word and case-insensitive. |
| query_mode | "any" | "all" | "any" | With "any", one matching word is enough. With "all", every word must appear somewhere in the article. |
| search_in | "all" | "title" | "all" | Where query words are looked for: title, summary and body text, or the title only. |
| exclude_query | string | string[] | — | Drop any article that mentions one of these words in its title, summary or body. Split into words the same way as query. |
Search is by word, not by phrase. "us-iran escalation" looks for the words iran and escalation (us is too short to search on), and by default an article containing either one matches. Whole-word matching means war does not match software or warning.
For tighter results, set query_mode to "all", search_in to "title", or both. When query is a list, "all" requires every word from every term in the list.
{ "query": "iran nuclear talks", "query_mode": "all" }
{ "query": "bitcoin", "search_in": "title", "exclude_query": ["etf", "price"] }Narrowing results
| Field | Type | Default | Description |
|---|---|---|---|
| category | string | 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. |
| source | string | string[] | — | Only return articles from these channels. Use the exact name from GET /channels; case is ignored. |
| exclude_source | string | string[] | — | Leave out articles from these channels. Same naming rules as source. |
| language | string | — | Two-letter language code of the channel, such as "en", "es", "fr", "de", "pt", "ar" or "he". |
| has_image | boolean | false | When 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
| Field | Type | Default | Description |
|---|---|---|---|
| date | YYYY-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_from | YYYY-MM-DD | — | Only articles published on or after this day. |
| date_to | YYYY-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
| Field | Type | Default | Description |
|---|---|---|---|
| sort | "balanced" | "newest" | "balanced" | "balanced" shows the latest match from each channel before any channel repeats. "newest" is plain newest-first. |
| limit | integer | 10 | How many articles to return. Maximum 200. |
| offset | integer | 0 | How many articles to skip. Use with has_more for paging. |
| fetch_extras | boolean | true | Whether 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 field | Type | Description |
|---|---|---|
| title | string | Headline. |
| url | string | Link to the article. Unique per article. |
| source | string | Channel name, as listed by GET /channels. |
| categories | string[] | Categories this article was classified into. |
| language | string | Two-letter language code of the channel. |
| is_rtl | boolean | True for right-to-left languages such as Arabic and Hebrew. |
| summary | string | Short description from the feed (up to 400 characters). May be empty. |
| summary_remaining | number | How many more characters the feed's description had beyond summary. 0 when nothing was cut. |
| content | string | The start of the body text (up to 400 characters). May be empty. |
| content_remaining | number | How many more characters the body had beyond content. 0 when nothing was cut. |
| image | string | null | Image URL, or null when none was found. |
| published_at | string | null | Publish time as a UTC ISO 8601 timestamp, or null when the channel gave none. |
| ai | object | null | AI 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 inYYYY-MM-DDform, ordate_fromwas afterdate_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 includesplan, the limit andused.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.