Skip to main content
Webhooks are how you know if a post actually went live or if it exploded. Instead of polling us every 2 seconds like a psychopath, just give us a URL and we’ll tell you.
Prerequisite: You need an API Key and a URL where we can send POST requests. Webhooks are configured at the organization level - all teams under that org share the same webhook endpoints.

Setup

  1. Go to the Webhooks Dashboard.
  2. Add your URL (e.g., https://api.yourapp.com/webhooks/bundle). Must be HTTPS.
  3. Get your Signing Secret.
Local Dev: Use ngrok to test locally. ngrok http 3000.

Events

EventWhen it fires
post.publishedPost went live (or failed permanently after retries)
post.failedPost failed - check the error details
comment.publishedFirst comment was posted (or failed permanently)
social-account.createdUser connected a new social account
social-account.deletedSocial account was disconnected
team.createdA new team was created
team.updatedTeam details changed (name, avatar, social accounts added/removed)
team.deletedA team was deleted
team.updated also fires when a social account is added to or removed from a team - so you don’t need to listen to social account events separately if you’re already tracking teams.

Payload Structure

Every webhook delivery is an HTTP POST with this shape:
{
  "type": "post.published",
  "data": {
    // ... event-specific payload
  }
}

Headers We Send

HeaderValue
Content-Typeapplication/json
User-Agentbundlesocial
x-signatureHMAC-SHA256 signature (see Security)

Post Events Payload

When you receive post.published or post.failed, the data field contains the full post object:
{
  "type": "post.published",
  "data": {
    "id": "post_abc123",
    "title": "My awesome post",
    "content": "Hello world!",
    "status": "POSTED",
    "scheduledDate": "2026-01-15T10:00:00.000Z",
    "postedDate": "2026-01-15T10:00:02.341Z",
    "teamId": "team_xyz",
    "createdAt": "2026-01-14T15:30:00.000Z",
    "updatedAt": "2026-01-15T10:00:02.341Z",
    "deletedAt": null,
    "uploads": [
      {
        "upload": {
          "id": "upload_456",
          "fileName": "cover.jpg",
          "mimeType": "image/jpeg",
          "size": 245000
        }
      }
    ],
    "socialAccounts": [
      {
        "socialAccount": {
          "id": "sa_789",
          "type": "INSTAGRAM",
          "username": "mybrand",
          "displayName": "My Brand"
        }
      }
    ]
  }
}

Comment Events Payload

{
  "type": "comment.published",
  "data": {
    "id": "comment_abc",
    "content": "#growth #mindset",
    "status": "POSTED",
    "postId": "post_abc123",
    "teamId": "team_xyz",
    "createdAt": "2026-01-15T10:00:05.000Z",
    "socialAccounts": [
      {
        "socialAccount": {
          "id": "sa_789",
          "type": "INSTAGRAM",
          "username": "mybrand"
        }
      }
    ]
  }
}

Social Account Events Payload

{
  "type": "social-account.created",
  "data": {
    "id": "sa_789",
    "type": "INSTAGRAM",
    "teamId": "team_xyz",
    "username": "mybrand",
    "displayName": "My Brand",
    "avatarUrl": "https://...",
    "externalId": "17841400000",
    "channels": [],
    "createdAt": "2026-01-10T12:00:00.000Z"
  }
}
Sensitive fields like accessToken, refreshToken, secret, and expiresAt are never included in webhook payloads. We’re not that reckless.

Team Events Payload

{
  "type": "team.created",
  "data": {
    "id": "team_xyz",
    "name": "Marketing Team",
    "avatarUrl": "https://...",
    "organizationId": "org_123",
    "createdById": "user_456",
    "createdAt": "2026-01-10T12:00:00.000Z",
    "organization": { "id": "org_123", "name": "Acme Corp" },
    "createdBy": { "id": "user_456", "email": "..." },
    "socialAccounts": []
  }
}

Delivery & Retries

We really try to deliver your webhooks. Here’s how hard we try:
SettingValue
Timeout15 seconds per request
Max attempts3 (initial + 2 retries)
BackoffExponential, starting at 30 seconds
ConcurrencyWe process up to 50 deliveries simultaneously
So if your server is down, we’ll try 3 times with increasing delays. After that, the event is marked as failed and we move on. We’re persistent, not obsessive.

Auto-Disable (The Safety Net)

If your webhook endpoint fails 50 consecutive times within a 24-hour window, we automatically disable it. This protects both of us - we stop wasting resources, and you stop missing events you don’t know about. When this happens:
  • Your webhook is marked as disabled
  • We stop sending events to that URL
  • You can re-enable it from the dashboard or API (this resets the failure counter)
  • On the next successful delivery, the failure counter resets to 0
Don’t let your endpoint go down for too long. 50 consecutive failures is generous, but if your server is having a bad day, re-enable manually once it’s back. Events that happened while disabled are gone - we don’t replay them.

Resending Events

Made a mistake in your handler? Event got lost? You can resend any webhook event from the dashboard or via the API. We’ll create a fresh delivery attempt with the same payload.

Best Practices

  1. Respond fast. Return a 200 within 15 seconds. Do heavy processing asynchronously. If you take too long, we’ll count it as a failure.
  2. Be idempotent. We might deliver the same event twice in rare cases (retries, race conditions). Use the event ID to deduplicate.
  3. Verify signatures. Always. No exceptions. Even in development. See the SDK for built-in signature verification, or check the Swagger for the raw details.
  4. Use HTTPS. We only send webhooks to HTTPS URLs. Sorry, http://localhost works via ngrok though.
  5. Monitor your failures. If you’re getting close to 50 consecutive failures, fix your endpoint before we disable it.