Feeds

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.