Webhooks
Event types, delivery headers, signature verification with Node.js and Python code, retries and idempotency.
Webhooks tell your server when something changes, so you do not poll. Siar sends an HTTPS POST with a JSON body to each endpoint subscribed to the event.
Create an endpoint
Organization owners and admins manage webhooks from the dashboard. API keys cannot create or edit them. You can have up to 5 endpoints; each has its own event list and its own secret.
# Webhooks are managed by organization owners and admins from the dashboard.
# API keys cannot create or change them. The underlying request is:
POST https://api.siaran.my/api/v1/webhook
{
"url": "https://yourapp.example/webhooks/siar",
"events": ["post.published", "comment.received", "social-account.updated"]
}
# → { "id": "...", "url": "...", "events": [...], "active": true, "secret": "..." }
# The secret is returned once. It signs every delivery. The endpoint must be an HTTPS URL that answers within 15 seconds. Siar does not follow redirects.
The request
Headers:
| Header | Value |
|---|---|
x-siar-event | The event type. |
x-siar-delivery | A unique id for this delivery. |
x-siar-timestamp | Unix time in seconds when it was signed. |
x-siar-signature | Hex HMAC-SHA256 of timestamp + "." + rawBody with your secret. |
user-agent | Siar-Webhooks/1.0 |
Body:
{
"id": "evt_...",
"type": "post.published",
"createdAt": "2026-11-02T09:00:04.120Z",
"organizationId": "ORG_ID",
"data": { }
}
id is stable across redeliveries of the same event; data holds the object the event is about.
Verify the signature
Compute the HMAC over the raw bytes of the body, before any JSON parsing, using the secret from when you created the endpoint. Compare in constant time, and reject timestamps more than five minutes from now to stop replays.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Use the raw request body exactly as received, before any JSON parsing.
export function verifySiarWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
const timestamp = headers['x-siar-timestamp'];
const signature = headers['x-siar-signature'];
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(String(signature), 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
// Express example: keep the raw body for this route.
app.post('/webhooks/siar', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifySiarWebhook(raw, req.headers, process.env.SIAR_WEBHOOK_SECRET)) return res.sendStatus(401);
const event = JSON.parse(raw);
// Deduplicate on the x-siar-delivery header or event.id, then handle event.type.
res.sendStatus(200);
}); import hashlib, hmac, time
def verify_siar_webhook(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> bool:
timestamp = headers.get("x-siar-timestamp")
signature = headers.get("x-siar-signature")
if not timestamp or not signature:
return False
if abs(time.time() - int(timestamp)) > tolerance:
return False
message = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.post("/webhooks/siar")
def siar_webhook():
raw = request.get_data() # raw bytes, before JSON parsing
if not verify_siar_webhook(raw, request.headers, os.environ["SIAR_WEBHOOK_SECRET"]):
abort(401)
event = request.get_json()
return "", 200 import { Siar } from '@siar/sdk';
const siar = new Siar();
app.post('/webhooks/siar', express.raw({ type: 'application/json' }), (req, res) => {
const event = siar.webhooks.constructEvent(req.body, req.headers, process.env.SIAR_WEBHOOK_SECRET);
if (event.type === 'post.published') {
// event.data holds the post; deduplicate on event.id
}
res.sendStatus(200);
}); Events
| Event | When it fires |
|---|---|
post.published | A post finished publishing on a network, or failed for good. Sent once per post per network. |
comment.published | A comment or reply you created through Siar was published (or failed). |
team.created | A team was created. |
team.updated | A team was renamed or its settings changed. |
team.deleted | A team was deleted. |
social-account.created | An account was connected to a team. |
social-account.updated | An account changed: channel picked, profile refreshed, or it needs reconnecting. |
social-account.deleted | An account was disconnected. |
credit.low-balance | The X credit balance fell below the threshold you set. |
credit.exhausted | The X credit balance reached zero. X actions stop until you top up; other networks keep publishing. |
comment.received | Someone commented on a post, and Siar picked it up from the network. |
conversation.created | A new direct-message conversation started. |
conversation.updated | A conversation was archived, reopened or otherwise changed. |
conversation.message.created | A message was sent or received in a conversation. |
conversation.message.updated | A message changed (reaction, edit, delivery state). |
conversation.read.updated | The read state of a conversation changed. |
Delivery and retries
A 2xx response means delivered. Anything else, or a timeout, schedules a retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, after which the delivery is marked failed. Order is not guaranteed and a delivery can arrive more than once, so:
- Return
200quickly and do the work afterwards. - Make your handler idempotent: deduplicate on
x-siar-deliveryor the eventid. - Treat the event as a prompt to read current state when order matters.
Operate
GET /webhook/{id}/deliverieslists deliveries with response codes and timings.POST /webhook/{id}/deliveries/{deliveryId}/resendsends one again.POST /webhook/{id}/testsends a test event.POST /webhook/{id}/rotate-secretissues a new secret; update your receiver, then confirm with a test.
See the webhook reference for the exact schemas.