Skip to content

Developers & AI agents

Webhooks & events

The X-Buddy event stream, webhook delivery and retries, verifying signatures, URL rules and test sends.

Last checked: October 24, 2026Markdown version

X-Buddy records what happens in a workspace as events. Read them by polling, or receive them as webhooks on your own HTTPS endpoint.

Polling

GET /events (events_list) returns events in order. Pass the cursor of the last event you processed as after to get only newer ones. Event cursors are increasing numbers (as strings). Events are kept 30 days. Polling is the reliable path; webhooks are the fast path.

Event types

Group Types
Jobs job.succeeded, job.failed, job.unknown, job.held, job.cancelled, batch.completed
Inbox & engagement dm.received, dm.request_received, reply.received, mention.received, follower.gained, follower.lost
Posts post.published, post.failed, post.partial
Campaigns campaign.person_entered, campaign.person_replied, campaign.person_completed, campaign.paused_auto
X accounts account.session_lost, account.session_restored, account.safety_changed, account.x_rate_limited, account.chat_locked
Infrastructure & other vps.offline, vps.online, proxy.failed, alert.opened, integration.paused, scan.completed, limit.reached

The envelope

Events follow CloudEvents 1.0:

{
  "specversion": "1.0",
  "id": "<event id>",
  "type": "job.succeeded",
  "source": "x-buddy:workspace:<workspace_id>",
  "subject": "<what the event is about>",
  "time": "2026-11-01T09:00:00Z",
  "observed_at": "2026-11-01T09:00:01Z",
  "cursor": "1042",
  "workspace_id": "<workspace_id>",
  "account_id": "<account_id>",
  "job_id": "<job_id>",
  "integration_id": null,
  "data": {}
}

When a connection may not read message text, content-bearing events carry "text_withheld": true instead of the text.

Verify the signature

Deliveries follow Standard Webhooks and carry three headers: webhook-id, webhook-timestamp and webhook-signature (v1,<base64>).

  1. Build the string <webhook-id>.<webhook-timestamp>.<raw body>.
  2. Compute HMAC-SHA256 with your signing secret (base64-decode it after removing the whsec_ prefix).
  3. Compare with each v1 signature in the header (there may be two during a secret rotation, which overlaps 24 hours).
  4. Reject timestamps more than 5 minutes from your clock.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret: string, id: string, ts: string, body: string, header: string): boolean {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest();
  return header.split(" ").some((part) => {
    const [v, sig] = part.split(",");
    if (v !== "v1" || !sig) return false;
    const got = Buffer.from(sig, "base64");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Delivery and retries

  • POST with a 10-second timeout. Only a 2xx answer counts as delivered; redirects are not followed.
  • Retries after 30 s, 2 min, 10 min, 1 h, 6 h, 12 h, 24 h and 24 h (±20% jitter), for up to 72 hours.
  • After 20 consecutive failures within 24 hours, the endpoint is paused and an alert opens.

Use the event id to ignore duplicates: a retried delivery has the same id.

URL rules

Endpoints must use https://, on the default port, 443 or 8443. Addresses with credentials, and internal, loopback, link-local, private or cloud-metadata addresses are refused — including names that resolve to them. X-Buddy checks again just before sending.

Test an endpoint

POST /webhooks/{endpoint_id}/test (webhook_test) sends a signed webhook.test event at once and returns the HTTP status, the time taken and the start of your response. It does nothing on X.