Webhooks
Receive real-time notifications when posts are published, accounts are connected, or tokens expire. UniPost sends HMAC-signed HTTP POST requests to the URL you configure.
Setup
Create a webhook subscription via the API. The signing secret is returned once in the create response — store it securely. Use POST /v1/webhooks/:id/rotate to generate a new secret if needed.
# Create a webhook subscription
curl -X POST https://api.unipost.dev/v1/webhooks \
-H "Authorization: Bearer up_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/unipost",
"events": ["post.published", "post.failed", "account.connected"]
}'
# Response (save the secret — it's shown only once):
# {
# "data": {
# "id": "wh_abc123",
# "url": "https://yourapp.com/webhooks/unipost",
# "events": ["post.published", "post.failed", "account.connected"],
# "secret": "whsec_a1b2c3d4e5f6..."
# }
# }
# Rotate the signing secret
curl -X POST https://api.unipost.dev/v1/webhooks/wh_abc123/rotate \
-H "Authorization: Bearer up_live_xxxx"Events
Payload Examples
post.published
{
"event": "post.published",
"timestamp": "2026-04-08T10:00:00Z",
"data": {
"id": "post_abc123",
"caption": "Hello from UniPost!",
"status": "published",
"created_at": "2026-04-08T10:00:00Z",
"results": [
{
"social_account_id": "sa_instagram_123",
"platform": "instagram",
"status": "published",
"external_id": "17841234567890",
"published_at": "2026-04-08T10:00:01Z"
}
]
}
}account.connected
{
"event": "account.connected",
"timestamp": "2026-04-08T10:00:00Z",
"data": {
"social_account_id": "sa_twitter_789",
"platform": "twitter",
"account_name": "@example",
"external_user_id": "your_user_123",
"connection_type": "managed"
}
}Signature Verification
Every webhook request includes an X-UniPost-Signature header with the format sha256=<hex>. Always verify this signature before processing the payload.
Without verification, any HTTP client can POST to your webhook URL and impersonate UniPost. Use
timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing attacks.const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const expectedSig = 'sha256=' + expected;
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSig)
);
}
// In your webhook handler:
app.post('/webhooks/unipost', (req, res) => {
const signature = req.headers['x-unipost-signature'];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET // whsec_xxxx from POST /v1/webhooks
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
switch (event) {
case 'post.published':
console.log('Published:', data.id);
break;
case 'post.failed':
console.error('Failed:', data.id, data.results);
break;
case 'account.connected':
console.log('New account:', data.social_account_id);
break;
}
res.status(200).json({ received: true });
});Retry Behavior
UniPost retries failed webhook deliveries up to 3 times with exponential backoff (30s, 2min, 10min). A delivery is considered failed when:
- Your endpoint returns a non-2xx status code
- Your endpoint doesn't respond within 10 seconds
- The connection is refused or times out
After 3 failures on the same event, the delivery is marked as permanently failed. Persistent failures across multiple events may trigger automatic webhook disabling — check the webhook status via GET /v1/webhooks/:id.