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

EventDescriptionStatus
post.publishedAll platforms published successfullyLive
post.failedAll platforms failedLive
post.partialSome platforms succeeded, some failedLive
account.connectedUser connected a social account (via Connect flow or dashboard)Live
account.disconnectedAccount disconnected or token permanently expiredLive
post.scheduledPost successfully queued for future publishingComing soon
account.refreshedManaged account token refreshed successfullyComing soon
account.quota_warningAccount reached 80% of monthly per-account limitComing soon
account.quota_exceededAccount exceeded monthly per-account limitComing soon

Payload Examples

post.published

Payload
{
  "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

Payload
{
  "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.

Security: always verify signatures
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.

← View full docs|Last updated: April 2026 · API v1