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
| Event | Sent when |
|---|---|
post.published | A root post is successfully published. |
post.failed | Publishing 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-SignatureX-EveryFeed-Timestamp(Unix seconds)X-EveryFeed-EventandX-EveryFeed-Event-IdX-EveryFeed-Delivery-IdX-EveryFeed-Secret-VersionX-EveryFeed-Test(trueon 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.