Developers & AI agents
Webhooks & events
The X-Buddy event stream, webhook delivery and retries, verifying signatures, URL rules and test sends.
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>).
- Build the string
<webhook-id>.<webhook-timestamp>.<raw body>. - Compute HMAC-SHA256 with your signing secret (base64-decode it after removing the
whsec_prefix). - Compare with each
v1signature in the header (there may be two during a secret rotation, which overlaps 24 hours). - 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
POSTwith a 10-second timeout. Only a2xxanswer 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.