Webhooks
Request and payload reference
This is the technical reference for the HTTP requests Feed Control sends to your webhook URL: the headers, every field in the JSON payload, how to check that a request is genuine, what your endpoint should answer, and how retries work. It's for developers writing a webhook handler.
The request
| Property | Value |
|---|---|
| Method | POST |
| URL | Your Webhook URL, exactly as entered, including any query string |
| Body | One feed item as a JSON object, encoded as UTF-8 |
| Items per request | One. Each new item is a separate request. |
| Timeout | 30 seconds for the whole request, of which up to 10 seconds to connect |
| HTTPS | Certificates are verified. TLS 1.2 or later is required. |
| Redirects | Not followed. A 3xx response counts as a failed delivery. |
| Destination | Must resolve to a public internet address. Private and internal addresses are refused. |
The body is compact JSON. Forward slashes and non-ASCII characters are escaped (for example https:\/\/example.com and Café). Any JSON parser decodes these for you.
Headers
| Header | Value | When |
|---|---|---|
Content-Type |
application/json |
Always |
Idempotency-Key |
feedcontrol-event- followed by the delivery's ID, for example feedcontrol-event-48213 |
Always. Every attempt of the same delivery has the same value. |
X-FeedControl-Test |
true |
Only on deliveries sent with Send test webhook |
User-Agent |
Currently GuzzleHttp/7, the default of the HTTP library Feed Control uses |
Always. It may change, so don't use it to recognise Feed Control. |
Standard headers such as Host and Content-Length are also sent. There's no signature header.
The number in Idempotency-Key is the delivery's ID in Event Logs: feedcontrol-event-48213 is shown as Event Log #48213, at /nu#/event-logs/48213. Log the key in your handler and you can match any request to its delivery record in Feed Control.
Authenticating requests
Feed Control doesn't sign webhook requests: there's no signature header and no signing secret. To check that a request comes from your Feed Control account, put a secret token in your webhook URL and check it in your handler.
- Use a long random token, for example the output of
openssl rand -hex 32, and add it to the URL:https://hooks.example.com/feedcontrol?token=<your token>. - Compare it in constant time, so the comparison doesn't leak how much of the token was right:
hash_equals()in PHP,crypto.subtle.timingSafeEqual()in Cloudflare Workers,crypto.timingSafeEqual()in Node.js orhmac.compare_digest()in Python. - Use HTTPS, so the token isn't sent in clear text. Keep your server's access logs private, as they usually record the full URL.
- Reject bad tokens with
401or403. Feed Control shows these as Not authorised in the delivery history, so a broken token check is easy to spot. - Don't reject requests for being old. No timestamp is signed, retries legitimately arrive hours after the item was found, and you can retry a delivery by hand for as long as it's kept (14 days).
To change the token without missing deliveries, see Setting up a webhook.
Treat the payload as untrusted input even after the token check: content and the other text fields come from third-party websites. If you display content, use content_clean_html or sanitise it yourself.
Payload
The body is a JSON object for one item, with the feed it came from nested under feed. All dates are ISO 8601 strings in UTC with microseconds, such as 2026-10-23T09:14:05.000000Z.
Don't rely on the order of keys, and ignore keys you don't recognise.
Item fields
| Field | Type | Description |
|---|---|---|
id |
integer | The item's ID in Feed Control. Unique across all feeds and never reused. |
created_at |
string | When Feed Control first found the item. |
title |
string | The item's title, with any text removed by the feed's Remove text from title filter. Up to about 255 characters. Can be empty. |
author |
string | The author from the source feed, or from the article if full-text extraction found one. Empty string if unknown. |
guid |
string | The item's GUID from the source feed. If the source has none, a SHA-256 hash of the item URL (or of the title, if there's no URL). Unique within a feed, but not across feeds. |
content |
string | The item's content: from the source feed, or the full article text if Fetch full text is on. Usually HTML, sometimes plain text. Can be empty. |
description |
string | A short summary or excerpt of the item. Can be empty. |
date |
string or null | The item's publication date from the source feed, or from the article if full-text extraction found one. null if unknown. |
url |
string or null | The link to the article. |
image |
string or null | The URL of an image for the item, from the source feed or the article. null if none was found. |
visibility |
string | The item's visibility in Feed Control. Always visible for newly found items. A test send of an existing item can show keep-visible, hidden or filtered. |
content_clean_html |
string | A cleaned-up version of content that's safe to display: basic formatting (paragraphs, headings, lists, links, images, tables) is kept, while scripts, styles and other attributes are removed. Same as content if content has no HTML. Empty if content is empty. |
content_plain_text |
string | A plain-text version of content, with paragraphs separated by blank lines. Empty if content is empty. |
feed |
object | The feed the item came from. See Feed fields. |
Feed fields
| Field | Type | Description |
|---|---|---|
id |
integer | The feed's ID in Feed Control. Use it to tell feeds apart if you connect several. |
created_at |
string | When the feed was added to Feed Control. |
updated_at |
string | When the feed's record was last updated, for example when you edited its settings. |
name |
string | The name you gave the feed in Feed Control. Up to 100 characters. |
title |
string | The feed's title, as found in the source feed or as you edited it. Used as the title of any generated RSS or JSON feed. |
url |
string | The source feed's URL. |
feed_fetched_at |
string or null | When the source feed was last checked for new items. |
update_frequency |
integer | How often the feed is checked, in minutes. 60 means every hour. |
description |
string | The feed's description, as found in the source feed or as you edited it. Can be empty. |
fetch_fulltext |
boolean | Whether Fetch full text is on for the feed. |
use_headless |
boolean | Whether Use headless browser is on for the feed. |
generated_rss_url |
string or null | The URL of the feed's generated RSS output, if RSS generation is on. Otherwise null. |
generated_json_url |
string or null | The URL of the feed's generated JSON output, if JSON generation is on. Otherwise null. |
The webhook can arrive before Feed Control has updated the generated RSS or JSON file. If your handler downloads that file, it might not include the item yet. See Generating RSS or JSON.
Example
A request for a new item from a feed with full-text extraction and RSS generation switched on:
POST /feedcontrol?token=3f9c…e21a HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
Idempotency-Key: feedcontrol-event-48213
User-Agent: GuzzleHttp/7
The body, formatted here for reading:
{
"id": 9183442,
"created_at": "2026-10-23T09:14:05.000000Z",
"title": "City council approves new cycle lanes",
"author": "Jane Smith",
"guid": "https://news.example.com/?p=48213",
"content": "<p>The council voted on Tuesday to approve <a href=\"https://news.example.com/cycle-plan\">the city's cycle plan</a>, adding 12 km of protected lanes.</p><p>Work starts in the spring.</p>",
"description": "The council voted on Tuesday to approve the city's cycle plan.",
"date": "2026-10-23T08:30:00.000000Z",
"url": "https://news.example.com/2026/10/council-cycle-lanes",
"image": "https://news.example.com/images/cycle-lanes.jpg",
"visibility": "visible",
"content_clean_html": "<p>The council voted on Tuesday to approve <a href=\"https://news.example.com/cycle-plan\">the city's cycle plan</a>, adding 12 km of protected lanes.</p><p>Work starts in the spring.</p>",
"content_plain_text": "The council voted on Tuesday to approve the city's cycle plan, adding 12 km of protected lanes.\n\nWork starts in the spring.\n",
"feed": {
"id": 52817,
"created_at": "2026-09-01T12:00:00.000000Z",
"updated_at": "2026-10-20T16:45:10.000000Z",
"name": "Local news: transport",
"title": "Example News",
"url": "https://news.example.com/feed/",
"feed_fetched_at": "2026-10-23T09:14:02.000000Z",
"update_frequency": 60,
"description": "Latest stories from Example News",
"fetch_fulltext": true,
"use_headless": false,
"generated_rss_url": "https://cdn.feedcontrol.net/1042/52817-6530f2a1b9c4d.xml",
"generated_json_url": null
}
}
Feeds and bundles
Deliveries for a connected bundle look exactly the same as deliveries for a connected feed. The feed object is the feed in the bundle that the item came from; the payload doesn't name the bundle. If your handler needs to know which bundle (or which connection) a delivery belongs to, create a separate webhook integration for it with a marker in the URL, for example ?source=transport-bundle.
Test sends
Deliveries sent with Send test webhook have the X-FeedControl-Test: true header. The body is the newest item from one of the integration's connected feeds, in the format above. If none of the connected feeds has any items yet (or only bundles are connected), it's a sample item instead, with id 0, guid feedcontrol-test-item, title Test webhook from Feed Control and a feed object with id 0. See Testing and delivery history.
Responding
Answer with any 2xx status code, such as 200, 202 or 204, as soon as you've safely received the item. The response body is optional.
You have 30 seconds. If you need to do slow work, such as calling other APIs or processing images, store the item or put it on a queue first, respond, and do the rest afterwards.
Anything else counts as a failed attempt, and Feed Control labels it in the delivery history:
| What your endpoint does | Shown as |
|---|---|
No response within 30 seconds, or 408 |
Timed out |
| Host name doesn't resolve | Host not found |
| Connection refused, reset or dropped | Connection failed |
| Certificate or TLS handshake problem | TLS/SSL error |
3xx redirect |
Redirected |
401 or 403 |
Not authorised |
404 or 410 |
Not found |
413 |
Too large |
429 |
Rate limited |
Any other 4xx |
Rejected |
5xx |
Server error |
Feed Control keeps the first 16 KB of your response body and your response headers (except Set-Cookie), and shows them in the delivery history. A short message such as {"status":"ok"} or a clear error makes failures much easier to understand. Don't put anything secret in the response.
Bodies can be large: with Fetch full text on, content, content_clean_html and content_plain_text each hold the whole article, and nothing is truncated. Make sure your server accepts large request bodies.
Retries
If an attempt fails, Feed Control tries again on this schedule:
| Attempt | When |
|---|---|
| 1st | Straight away, when the item is found |
| 2nd | About 1 hour after the 1st attempt failed |
| 3rd | About 2 hours after the 2nd attempt failed |
Retries are picked up every 15 minutes, so each one can come up to 15 minutes later than shown. After the third failed attempt the delivery is marked as failed and isn't retried automatically. If you've switched on failure notifications, it's included in your daily summary email. See Failure notifications.
Some failures aren't retried, because trying again can't help:
- Blocked address: the URL points to a private or internal network address
- Invalid URL: the URL isn't an
http://orhttps://URL - Switched off: the integration or the connection was switched off by the time the retry was due
- test sends
Feed Control doesn't read Retry-After headers; it always uses the schedule above.
You can also retry a failed or pending delivery yourself from Event Logs, even after all three attempts have been used. Each manual retry is one more attempt. See Event logs.
Every retry, automatic or manual, sends exactly the same body as the first attempt, to the same URL, with the same Idempotency-Key. The body is a snapshot of the item when it was first sent, and the URL is the one the integration had then, even if you've changed it since.
Duplicates and ordering
Your endpoint can receive the same item more than once:
- Retries after a late or failed answer. If your endpoint processed the item but answered too slowly or with an error, Feed Control sends it again.
- Interrupted sends. Very rarely, if Feed Control is interrupted while sending, the same delivery is sent again a few minutes later.
- Manual retries from Event Logs.
- Several connections. If the item reaches the same endpoint through more than one connection (for example, a feed and a bundle containing it), each connection is a separate delivery with its own
Idempotency-Key.
To handle this:
- Record each
Idempotency-Keyyou've processed and skip repeats. This covers the first three cases. Keep keys for at least 14 days. - If you want each item only once, whatever the connection, also skip items whose
idyou've already stored.
Requests for several new items can arrive at the same time and in any order. If order matters, sort by date (or created_at when date is null).
Can't find what you need? Contact support.