Documentation

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 or hmac.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 401 or 403. 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:// or https:// 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-Key you'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 id you'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.