EveryFeedEveryFeedeveryfeed.ai

Secure webhooks

Receive signed post outcomes with bounded retries, delivery history, and replay controls.

EveryFeed webhooks send a signed JSON request when a root post is published or fails to publish. Organization admins manage endpoints from Settings → Webhooks. Each endpoint picks which events it receives, can be limited to selected channels (instead of all connections), and can be paused with its active toggle — inactive endpoints receive nothing.

Add an endpoint

Create a public HTTPS receiver that accepts POST requests and returns a 2xx response within 10 seconds.

In Settings → Webhooks, add its URL, select the events it should receive, and save the endpoint.

Copy the signing secret immediately and store it in a secret manager. The full efwhsec_… value is shown only when the endpoint is created or its secret is rotated.

Only public HTTPS destinations are accepted. EveryFeed blocks credentials in URLs and hosts that resolve to private, loopback, link-local, or reserved addresses. The destination is resolved and pinned again for each request.

Use Send test on an endpoint to fire a signed sample delivery — the envelope carries test: true, the request carries X-EveryFeed-Test: true, and tests are rate-limited to five per minute per endpoint.

Rotation applies to new deliveries

After a rotation, new deliveries sign with the new secret immediately — but deliveries already queued or retrying, and replays of older deliveries, keep their original secret version. Keep the previous secret available until they drain, and pick the right one by X-EveryFeed-Secret-Version.

Events and payloads

EventSent when
post.publishedA root post is successfully published.
post.failedPublishing a root post fails.

Replies in a thread do not emit separate webhook events. Every request uses a versioned envelope:

{
  "apiVersion": "2026-07-15",
  "createdAt": "2026-07-15T20:15:00.000Z",
  "data": {
    "connectionId": "4ccfdb9e-3e37-4d98-b89d-703cc487e514",
    "platformPostId": "urn:li:share:123",
    "platformPostIds": ["urn:li:share:123"],
    "postId": "be89294f-4714-4208-b68b-75c66df22e35",
    "providerIdentifier": "linkedin",
    "releaseUrl": "https://www.linkedin.com/feed/update/urn:li:share:123",
    "releaseUrls": ["https://www.linkedin.com/feed/update/urn:li:share:123"],
    "state": "PUBLISHED"
  },
  "id": "2b49e175-4b85-45fa-9f07-252c09e5ea42",
  "organizationId": "eb13d637-ab8a-47f7-bbb3-300c08b60fb1",
  "type": "post.published"
}

platformPostId and releaseUrl can be null; the plural array fields carry every id when a publish produces several. A warning string appears when the post published with a warning, and test deliveries add test: true to the envelope.

post.failed data contains connectionId, postId, providerIdentifier, state: "ERROR", and the publishing error message under error.

Verify the signature

EveryFeed signs the exact request bytes with HMAC-SHA256:

v1=hex(HMAC_SHA256(secret, timestamp + "." + rawBody))

Read these headers before processing the event:

  • X-EveryFeed-Signature
  • X-EveryFeed-Timestamp (Unix seconds)
  • X-EveryFeed-Event and X-EveryFeed-Event-Id
  • X-EveryFeed-Delivery-Id
  • X-EveryFeed-Secret-Version
  • X-EveryFeed-Test (true on test deliveries)

Verify against the raw body, not JSON that has been parsed and serialized again. This Node.js example also rejects requests more than five minutes away from the receiver's clock:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyEveryFeedWebhook(input: {
  rawBody: Buffer;
  secret: string;
  signature: string;
  timestamp: string;
}) {
  if (!/^\d+$/.test(input.timestamp)) return false;
  if (!/^v1=[a-f0-9]{64}$/i.test(input.signature)) return false;

  const ageSeconds = Math.abs(Date.now() / 1_000 - Number(input.timestamp));
  if (ageSeconds > 300) return false;

  const expected = createHmac("sha256", input.secret)
    .update(input.timestamp)
    .update(".")
    .update(input.rawBody)
    .digest();
  const supplied = Buffer.from(input.signature.slice(3), "hex");

  return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}

After verification, make processing idempotent with the event ID and return 2xx only after the event is safely accepted. A replay reuses the original event and delivery IDs on purpose — a receiver that already processed the event treats the replay as a duplicate, and one that missed it processes it normally.

Retries and delivery history

Transport errors and responses with 408, 425, 429, or 5xx are retried up to six delivery attempts per run — a replay starts a fresh run with its own six attempts. The minimum delays are 10 seconds, 30 seconds, 2 minutes, 10 minutes, and 1 hour. A valid Retry-After can extend a delay up to one hour. Other responses, including redirects and most 4xx errors, are terminal, so the receiver should return 2xx directly.

Admins can open an endpoint's delivery history to filter by status and event, inspect every attempt, and replay a completed successful or failed delivery while the endpoint is active. Replays keep the original event, payload, and destination URL — each delivery snapshots the URL it was created with. Response bodies are captured up to the first 8 KB (anything past that is never stored), so receivers should not return secrets or other sensitive data.

Self-hosting requirement

Delivery workflows run through the EveryFeed Temporal worker. Self-hosted installations must keep that worker running for events and retries to be delivered.