Webhooks
Subscribe to Reepl events, verify the signature on every delivery, and understand retries, failures, and the source filter.
A webhook tells your system when something happens in Reepl, so you do not have to poll for it. Register a URL, pick the events you care about, and Reepl posts JSON to that URL whenever one fires.
Webhooks require a Pro plan or above. Registering one through the API needs a key with the webhooks:manage scope.
Register a subscription
Open Settings → Developers → Webhooks
Go to Settings → Developers and switch to the Webhooks tab.
Add your endpoint
Give Reepl the URL to POST to. It has to be reachable from the public internet — localhost will not work, so use a tunnel while developing.
Choose your events
Tick the events you want. Group by group is fine; subscribing to everything is rarely what you want, because you pay the verification cost on every delivery.
Save the signing secret
Reepl generates a secret starting with whsec_. You need it to verify deliveries. Store it the way you would store a password.
You can do the same over the API with POST /external/webhooks/subscriptions, and fire a test delivery with POST /external/webhooks/test.
Events
Twenty events, in eight groups.
Posts
| Event | Fires when |
|---|---|
post_published | A post went live on a connected channel |
post_scheduled | A post was scheduled for later |
post_updated | A scheduled or published post was edited |
post_deleted | A post was deleted |
publish_failed | A post failed to publish |
Drafts
| Event | Fires when |
|---|---|
draft_created | A new draft was created |
draft_updated | A draft was edited |
draft_deleted | A draft was deleted |
Articles
| Event | Fires when |
|---|---|
article_published | An article was published to a destination |
article_publish_failed | An article failed to publish to a destination |
article_draft_created | Reserved. You can subscribe to it, but nothing currently emits it. |
Carousels
| Event | Fires when |
|---|---|
carousel_draft_created | A new carousel draft was created |
carousel_draft_updated | A carousel draft was edited |
carousel_draft_deleted | A carousel draft was deleted |
Engagement
| Event | Fires when |
|---|---|
comment_added | A comment was added on a post |
Signals
| Event | Fires when |
|---|---|
signal_received | A new signal was captured. Signals sourced from X are excluded. |
AI agent
| Event | Fires when |
|---|---|
approval_requested | A post entered your workspace's review queue — queued by Andy, or submitted through the API |
Contacts (deprecated)
| Event | Fires when |
|---|---|
contact_added | A contact was added |
contact_updated | A contact was updated |
contact_deleted | A contact was removed |
Contacts have been removed from the Reepl app. These three events still fire but are scheduled for removal — do not build anything new on them.
Only hearing about AI-generated activity
Four events — post_published, post_scheduled, draft_created and approval_requested — carry a source field saying what created them. Add a filter to your subscription to receive only those:
{ "filters": { "source": "andy" } }
Filters are matched against the top level of the event payload and apply to the whole subscription, not to individual events. Any event whose payload lacks a matching key is filtered out, so do not add a source filter to a subscription that also carries events without one.
Verifying a delivery
Every request carries four headers:
| Header | Contents |
|---|---|
X-Reepl-Event | The event type, e.g. post_published |
X-Reepl-Webhook-ID | Unique delivery id, del_… — use it to deduplicate |
X-Reepl-Webhook-Timestamp | Unix seconds, as a string |
X-Reepl-Webhook-Signature | v1= followed by the hex HMAC |
The signature is an HMAC-SHA256 over the timestamp, a literal dot, and the raw request body, keyed with your whsec_ secret:
signature = HMAC_SHA256(secret, "{timestamp}.{raw_body}")
import crypto from "node:crypto";
function verify(rawBody, headers, secret) {
const timestamp = headers["x-reepl-webhook-timestamp"];
const received = headers["x-reepl-webhook-signature"];
const expected =
"v1=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare, so a timing attack cannot recover the signature.
const a = Buffer.from(expected);
const b = Buffer.from(received ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verify against the raw body, exactly as received. If your framework parses JSON before you get to it, re-serializing will change the bytes and every signature will fail. Reject anything that does not verify, and reject old timestamps to blunt replay attempts.
Retries and failures
Reepl waits up to 30 seconds for your endpoint to respond. Return a 2xx quickly and do the real work afterwards — a slow handler reads as a failure.
A delivery is retried when it times out, when the connection fails, or when your endpoint returns 429 or any 5xx. Other 4xx responses are treated as a permanent rejection and are not retried.
Retries back off at 1 minute, 5 minutes, 15 minutes, then 1 hour, for up to 5 attempts.
After 10 consecutive failures the subscription is disabled automatically and stops delivering until you re-enable it. If a webhook goes quiet, check whether it was switched off.
Deliveries can arrive more than once. Deduplicate on X-Reepl-Webhook-ID rather than assuming exactly-once.