Webhooks: Slack, Discord, and signed JSON
Pipe follow-backs and safety events to Slack, Discord, or your own endpoint. Payload format, HMAC signature verification, and delivery semantics.
Everything that appears in the activity feed can be delivered outside the dashboard. Create a webhook on the Integrations page, choose a destination format, and optionally filter to the event types you care about. Delivery is in order, retried on failure, and signed when you use the JSON format.
Three destination formats
| Format | Best for | What gets sent |
|---|---|---|
| Signed JSON | Your own endpoint, Zapier catch hooks, internal tools | Full event payload with an HMAC signature header |
| Slack | A Slack channel via an incoming-webhook URL | A readable one-line message per event |
| Discord | A Discord channel webhook | A readable one-line message per event |
Event types
| Event | Fires when |
|---|---|
| follow | A campaign follows a candidate |
| follow_back | Someone you followed follows back |
| unfollow | Cleanup releases a non-follower after the wait window |
| pause | A campaign or account pauses, including safety holds and rate-limit backoffs |
| error | An action fails |
| research | A research run refreshes a campaign's candidate pool |
| profile_saved | A profile is queued via the API, for example from your own browser extension or internal tool |
| test | You press Send test on the Integrations page |
The signed JSON payload
POST <your endpoint>
content-type: application/json
x-skyfollowing-event: follow_back
x-skyfollowing-signature: sha256=6b3c...e29f
{
"id": "6f1c9e0a-4d2b-4e8a-9a71-2c5d8f3b9101",
"event": "follow_back",
"message": "@ada.bsky.social followed you back",
"campaignId": "c1a2...d4",
"workspaceId": "9b8a...01",
"createdAt": "2026-07-01T14:09:22.000Z"
}Verifying the signature
Each webhook has a secret, shown on the Integrations page. The signature header is the hex HMAC SHA-256 of the raw request body using that secret, prefixed with sha256=. Compare with a constant-time check and reject anything that fails.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySkyFollowing(
rawBody: string,
signatureHeader: string, // "sha256=<hex>"
secret: string,
): boolean {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Delivery semantics
- In order, per webhook. Events deliver oldest first, and a cursor tracks progress, so nothing is skipped.
- Batched. Up to 25 events deliver per run, with an 8-second timeout per request.
- Retried. A failed delivery stops the batch; the same event retries on the next run. Successes reset the failure count.
- Self-disabling. After 15 consecutive failed runs, the webhook disables itself rather than hammering a dead endpoint. Re-enable it from the Integrations page once the endpoint is fixed.
- Observable. Each webhook shows its last status code, last error, and last delivery time right on the page.
Create a Zapier catch hook, paste its URL into a signed JSON webhook, and filter to the events you want. Zapier receives the full payload and can fan it out to a spreadsheet, CRM, or anywhere else.
Frequently asked questions
How do I test a webhook before real events flow?
Every webhook row has a Send test button that delivers a sample payload with event type test through the same pipeline as real deliveries, so you validate the endpoint, format, and signature in one click.
My webhook shows Failing. What now?
The row shows the last HTTP status and error. Fix the endpoint, press Send test to confirm, and delivery resumes from the cursor. If it auto-disabled after repeated failures, re-enable it once the test passes.
Are Slack and Discord messages signed?
No. Those formats post plain notification text to URLs that are themselves secrets. Signature verification applies to the generic JSON format, where you control the receiving endpoint.
Every setting on this page ships with safe defaults. Free for 7 days, no card required.