Events

Notifications & Webhooks

Get an email or a signed webhook when posts are scheduled, published, partially fail or fail.

Notification rules tell you when something happens to a post, so you don't have to poll GET /v1/posts/{id}. Each rule sends either an email or a webhook (an HTTPS POST signed with HMAC-SHA256).

Events

EventFires when
post.scheduledA post is created with scheduledFor. Posts sent with publishNow don't fire this.
post.publishedEvery target of a post published.
post.partialA post finished with some targets published and some failed.
post.failedEvery target of a post failed.

The last three fire once per post, when it reaches its final state. A post that's retrying on one platform hasn't finished yet, so you won't get an event until it does.

Create a rule

Rules are managed in the dashboard under Settings โ†’ Notifications at https://social-publish-kappa.vercel.app/dashboard/settings?tab=notifications. Only project owners and admins can manage them.

  1. Click New notification.
  2. Choose the events.
  3. Choose which profiles it applies to: all profiles, or specific accounts. A rule fires if the post targets at least one of the chosen accounts.
  4. Optionally tick Only scheduled posts to skip posts sent with publishNow.
  5. Pick the channel: email recipients (up to 20 addresses), or a webhook URL.
  6. Use Send test to try it, then save.

Rules can be turned off and on without deleting them. There's no public API for managing rules.

Email

Email notifications show the post text, each platform's result with a link to the live post or the error, and a button to open the post in the dashboard.

Email delivery needs an email provider configured on the deployment. If it isn't, the dashboard says so and email rules are skipped.

Webhooks

Webhook URLs must be public https:// URLs. URLs with credentials in them, localhost, and hosts that resolve to private, loopback or link-local addresses are rejected, both when you save the rule and again before every delivery. Redirects are not followed.

When you create a webhook rule, the dashboard shows its signing secret (it starts with whsec_). Copy it then: it's stored encrypted and can't be shown again.

Request

Each delivery is a POST with a JSON body:

POST /hooks/relaycast HTTP/1.1
Content-Type: application/json
User-Agent: SocialPublish-Webhooks/1.0
x-webhook-id: 9b1f..._5e6f7a8b-..._post.published
x-webhook-timestamp: 1791203000
x-signature: 3c5a0e9f4b...
{
  "id": "9b1f..._5e6f7a8b-..._post.published",
  "event": "post.published",
  "data": {
    "postId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
    "status": "published",
    "scheduledFor": "2026-10-05T14:00:00.000Z",
    "scheduled": true,
    "content": "We just shipped dark mode ๐ŸŒ™",
    "dashboardUrl": "https://social-publish-kappa.vercel.app/dashboard/posts",
    "targets": [
      {
        "id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "accountId": "8f1c2a4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
        "platform": "linkedin",
        "handle": "jane-doe",
        "status": "published",
        "url": "https://www.linkedin.com/feed/update/urn:li:share:...",
        "error": null
      }
    ]
  }
}
FieldDescription
idDelivery ID, also sent as x-webhook-id. It's the same on every retry of this delivery, so use it to de-duplicate.
eventOne of the events above.
testtrue for deliveries sent with Send test; absent otherwise.
data.statusThe post's status.
data.scheduledfalse if the post was sent with publishNow.
data.targets[]One entry per account, with its status, url of the live post, or error message.

Test deliveries use a sample post.published payload with placeholder IDs.

Verify the signature

The x-signature header is the hex-encoded HMAC-SHA256 of {timestamp}.{raw body}, keyed with your signing secret, where timestamp is the x-webhook-timestamp header (Unix seconds). Compute it over the raw request body, before any JSON parsing, and compare in constant time. Reject old timestamps to block replays.

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

export function verifyWebhook(rawBody: string, headers: Headers, secret: string) {
  const timestamp = headers.get("x-webhook-timestamp") ?? "";
  const signature = headers.get("x-signature") ?? "";
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");

  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300; // 5 minutes
  const a = Buffer.from(signature, "hex");
  const b = Buffer.from(expected, "hex");
  return fresh && a.length === b.length && timingSafeEqual(a, b);
}
webhook.py
import hashlib
import hmac
import time

def verify_webhook(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("x-webhook-timestamp", "")
    signature = headers.get("x-signature", "")
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    try:
        fresh = abs(time.time() - int(timestamp)) < 300  # 5 minutes
    except ValueError:
        return False
    return fresh and hmac.compare_digest(signature, expected)

Respond and retries

Return any 2xx status within 10 seconds to acknowledge a delivery. Anything else, including timeouts and redirects, counts as a failure, and the delivery is retried with exponential backoff (starting at about 10 seconds), up to 8 attempts.

  • Respond quickly and do slow work asynchronously.
  • De-duplicate on x-webhook-id, since a delivery can arrive more than once.
  • Don't rely on ordering. Use data.status and the target statuses as the source of truth, or fetch the post with GET /v1/posts/{id}.

See Webhook events for the payload reference.

Credit usage emails

Separately from notification rules, project owners and admins get emails when the project's credits run low. Turn them on or off under Settings โ†’ General. See Credits & Usage.

This page is also available as Markdown.