Webhooks
Register an endpoint
curl -X POST https://usefeeds.com/v1/webhooks \
-H "Authorization: Bearer $FEEDS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/feeds","events":["item.created"]}'
The URL must use HTTPS on a public host. The response contains secret (whsec_…), shown only once. Webhooks are free.
Events
Every post delivered to any of your trackers sends one item.created event to each endpoint that subscribes to it:
{
"id": "evt_12_4211",
"type": "item.created",
"createdAt": "2026-10-20T10:05:00.000Z",
"data": {
"trackerId": 87,
"item": {
"id": 4211,
"externalId": "1850000000000000000",
"author": "NASA",
"title": "First words of the post…",
"contentHtml": "<p>Full text of the post</p>",
"url": "https://x.com/NASA/status/1850000000000000000",
"publishedAt": "2026-10-20T10:00:00.000Z",
"media": []
}
}
}
POST /v1/webhooks/WEBHOOK_ID/ping sends a ping event, useful to test your endpoint.
Each request is a POST with these headers:
| Header | Value |
|---|---|
content-type |
application/json |
feeds-event-type |
The event type, e.g. item.created |
feeds-signature |
t=<unix seconds>,v1=<hex signature> |
user-agent |
feeds-webhooks/1 |
Verify the signature
The signature is the HMAC-SHA256 of <t>.<raw body> with your secret, in hex. Compute it over the raw bytes you received, before parsing the JSON, compare in constant time, and reject requests whose t is more than five minutes from your clock.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyFeedsSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
Python
import hashlib, hmac, time
def verify_feeds_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Go
func VerifyFeedsSignature(rawBody []byte, header, secret string, tolerance time.Duration) bool {
var t int64
var v1 string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(part, "=")
switch k {
case "t":
t, _ = strconv.ParseInt(v, 10, 64)
case "v1":
v1 = v
}
}
if d := time.Since(time.Unix(t, 0)); d > tolerance || d < -tolerance {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", t)
mac.Write(rawBody)
given, err := hex.DecodeString(v1)
return err == nil && hmac.Equal(given, mac.Sum(nil))
}
Respond and retry
Answer with any 2xx within 10 seconds; do the work after you answer. We do not follow redirects. Any other result is retried after 1, 5, 15, 60, 180, 360, and 720 minutes, about a day in all, and then the event is marked failed.
Delivery is at least once: the same event can arrive twice. Use id to ignore duplicates.
The console and GET /v1/webhooks/WEBHOOK_ID/events show each event with its attempts, last status, and last error. POST /v1/webhooks/WEBHOOK_ID/events/EVENT_ID/retry sends an event again.