> ## Documentation Index
> Fetch the complete documentation index at: https://docs.social-api.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when interactions arrive on your connected accounts.

SocialAPI.ai webhooks deliver real-time social media events (new comments, direct messages, and mentions) across every connected network through one signed, retried webhook stream, so you do not poll each platform separately.

Instead of polling the API, register a webhook endpoint and SocialAPI will POST a signed payload to your server whenever a new comment, DM, review, or mention arrives, or whenever one of your posts changes lifecycle state (scheduled, published, deleted, etc.).

## Register an endpoint

Endpoints can be registered in the dashboard under **Webhooks** → **Add Endpoint**, or via the API:

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/webhooks \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.example.com/webhooks/socapi",
    "events": ["comment.received", "dm.received", "review.received"]
  }'
```

Response (HTTP 201):

```json theme={null}
{
  "id": "wh_01HZ9X3Q4R5M6N7P8V2K0W1J",
  "url": "https://app.example.com/webhooks/socapi",
  "events": ["comment.received", "dm.received", "review.received"],
  "secret": "a3f8c2e1b9d4f7a6c5e0b3d2f1a8e7c4b9d6f3a2e5c8b1d4f7a0e3c6b9d2f5a8",
  "message": "Store the secret securely. It will not be shown again."
}
```

The `secret` is returned **once**. Store it immediately in your environment. It cannot be retrieved again. You use it to verify incoming signatures.

<Warning>
  Endpoint URLs must use HTTPS. HTTP URLs are rejected with `400`.
</Warning>

<Note>
  The `events` array is required. Sending an empty array or omitting it returns `400 field_invalid`.
</Note>

<Info>
  When you create an endpoint, SocialAPI sends a verification ping to your URL. Your server must respond with a `2xx` status code or the endpoint will not be saved. Make sure your server is running and reachable before registering.
</Info>

## Event types

Events are grouped into three categories: **inbox** (incoming interactions), **posts** (post lifecycle transitions), and **accounts** (connection state).

| Event                  | Category | When it fires                                                                                                                                                                                                                                                                                                                    |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `comment.received`     | inbox    | A new comment arrives on any connected account                                                                                                                                                                                                                                                                                   |
| `dm.received`          | inbox    | A new direct message arrives (may include `metadata.referral` if the DM originated from an ad)                                                                                                                                                                                                                                   |
| `dm.sent`              | inbox    | Your account sent a direct message (echo confirmation from Meta)                                                                                                                                                                                                                                                                 |
| `dm.referral`          | inbox    | A user clicked an ad or referral link without sending a message. The interaction has empty content and includes `metadata.referral`                                                                                                                                                                                              |
| `dm.postback`          | inbox    | A user tapped a button or quick reply in a DM                                                                                                                                                                                                                                                                                    |
| `review.received`      | inbox    | A new review arrives (Google Business Profile)                                                                                                                                                                                                                                                                                   |
| `mention.received`     | inbox    | Your account is mentioned on a supported platform                                                                                                                                                                                                                                                                                |
| `dm.status.sent`       | inbox    | A direct message you sent was accepted by the platform                                                                                                                                                                                                                                                                           |
| `dm.status.delivered`  | inbox    | A direct message you sent was delivered to the recipient                                                                                                                                                                                                                                                                         |
| `dm.status.read`       | inbox    | A direct message you sent was read by the recipient                                                                                                                                                                                                                                                                              |
| `dm.status.failed`     | inbox    | A direct message you sent failed to deliver                                                                                                                                                                                                                                                                                      |
| `post.scheduled`       | posts    | A post was created with a future `scheduled_at` and entered the `scheduled` state                                                                                                                                                                                                                                                |
| `post.published`       | posts    | A post finished publishing successfully on all targets                                                                                                                                                                                                                                                                           |
| `post.partial`         | posts    | A post published on some targets but failed on others                                                                                                                                                                                                                                                                            |
| `post.failed`          | posts    | A post failed to publish on all targets                                                                                                                                                                                                                                                                                          |
| `post.updated`         | posts    | A draft, scheduled, or failed post was edited                                                                                                                                                                                                                                                                                    |
| `post.deleted`         | posts    | A post was deleted. The payload includes `was_status` so you know what state the post was in before removal                                                                                                                                                                                                                      |
| `post.unpublished`     | posts    | A published post was removed from one or more platforms (the post row stays unless every target is unpublished)                                                                                                                                                                                                                  |
| `post.retried`         | posts    | A failed or partial post is being retried. A subsequent `post.published`, `post.partial`, or `post.failed` event fires once the retry completes                                                                                                                                                                                  |
| `account.connected`    | accounts | A social account finished connecting. Payload includes username, display name, profile picture, bio, and a public profile link                                                                                                                                                                                                   |
| `account.disconnected` | accounts | A connected account was revoked by the user on the platform side (Meta deauthorize webhook, TikTok deauthorize webhook), or flagged for reconnection because all of its pages were removed or its access was revoked. When the payload includes `reconnect_required: true`, the account still exists but needs to be reconnected |
| `page.removed`         | accounts | A page on a multi-page account (for example a Facebook Page) was detected as removed during a periodic refresh and was deactivated. Other pages on the same account are unaffected                                                                                                                                               |

You can subscribe to any combination. Fetch the authoritative list with `GET /v1/webhooks/events` and subscribe to everything by passing the full set.

<Tip>
  The authoritative list of event types is available at `GET /v1/webhooks/events`. It returns each event's description and category, so dashboard UIs and integrations can render the catalog without hard-coding it.
</Tip>

### Post lifecycle payloads

Each post lifecycle event carries a small payload with the `post_id` and event-specific fields.

```json theme={null}
{
  "event": "post.scheduled",
  "data": {
    "post_id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "status": "scheduled",
    "scheduled_at": "2026-06-01T14:00:00Z",
    "targets": [
      { "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "instagram" },
      { "account_id": "acc_02HZ9X3Q4R5M6N7P8V2K0W1J", "platform": "facebook" }
    ]
  }
}
```

```json theme={null}
{
  "event": "post.updated",
  "data": {
    "post_id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "status": "scheduled",
    "changed_fields": ["text", "scheduled_at"]
  }
}
```

```json theme={null}
{
  "event": "post.deleted",
  "data": {
    "post_id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "was_status": "published"
  }
}
```

```json theme={null}
{
  "event": "post.unpublished",
  "data": {
    "post_id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "unpublished_count": 1
  }
}
```

When `account_id` is `null` on a `post.unpublished` event, the post was unpublished from every target.

```json theme={null}
{
  "event": "post.retried",
  "data": {
    "post_id": "post_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "retried_count": 2
  }
}
```

`post.published`, `post.partial`, and `post.failed` share a `{ post_id, status, results }` payload, where `results` is the same per-target outcome array returned by `POST /v1/posts`.

### `account.connected` payload

Fires once per connected account after a successful OAuth exchange or direct connection. Use this to render the connected profile in your UI or kick off post-connect work.

```json theme={null}
{
  "event": "account.connected",
  "data": {
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "brand_id": "br_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "platform": "instagram",
    "platform_user_id": "17841405793187218",
    "username": "acmecorp",
    "display_name": "ACME Corp",
    "profile_picture_url": "https://scontent.cdninstagram.com/...",
    "bio": "Official ACME account",
    "profile_url": "https://www.instagram.com/acmecorp/",
    "user_email": "owner@acme.com",
    "connected_at": "2026-05-25T14:30:00Z"
  }
}
```

`profile_url` is built per platform from `username` (Instagram, Threads, TikTok, X) or `platform_user_id` (Facebook, LinkedIn, YouTube, Google Business Profile). It is empty when neither is available.

### `page.removed` payload

Fires when a single page on a multi-page account (for example a Facebook Page) is detected as removed during a periodic refresh. The page is deactivated and stops appearing in page listings. Other pages on the same account stay active.

```json theme={null}
{
  "event": "page.removed",
  "data": {
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "platform_page_id": "102938475610293",
    "name": "ACME Storefront",
    "platform": "facebook",
    "reason": "page_not_found"
  }
}
```

### `account.disconnected` payload

Fires in two cases. A platform-side revocation (the user removes the app) carries no `reconnect_required` flag and the account is deleted. A reconnection prompt carries `reconnect_required: true`: the account row still exists but its status is set so that calls signal it needs reconnecting. The `reason` is `all_pages_removed` (the last remaining page was removed) or `access_revoked` (the account token or permissions were revoked, which fails every page at once).

```json theme={null}
{
  "event": "account.disconnected",
  "data": {
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "platform": "facebook",
    "reason": "all_pages_removed",
    "reconnect_required": true
  }
}
```

## Payload format

Every webhook POST has a JSON body with an `event` field and a `data` field containing the full interaction object:

```json theme={null}
{
  "event": "comment.received",
  "data": {
    "id": "sapi_cmt_aW5zdGFncmFtOjE3ODQxNDA1",
    "type": "comment",
    "platform": "instagram",
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "author": {
      "id": "10215054824",
      "name": "Jane Smith",
      "avatar_url": "https://example.com/avatar.jpg"
    },
    "content": {
      "text": "Love this product!",
      "media": []
    },
    "received_at": "2026-03-01T14:30:00Z",
    "metadata": {}
  }
}
```

The `data` object is the same `Interaction` shape returned by `GET /accounts/{id}/comments` (and the equivalent DM, review, and mention endpoints). The `id` field is a stable SocialAPI interaction ID - see [Interaction IDs](/guides/interaction-ids).

### Referral metadata

When a DM originates from an ad click (Instagram CTD or Facebook CTM ads), the interaction includes referral context in `metadata.referral`:

```json theme={null}
{
  "event": "dm.received",
  "data": {
    "id": "sapi_dm_aW5zdGFncmFtOm1pZC5yZWYx",
    "type": "dm",
    "platform": "instagram",
    "account_id": "acc_01HZ9X3Q4R5M6N7P8V2K0W1J",
    "author": { "id": "17846744073097661" },
    "content": { "text": "I saw your ad!" },
    "received_at": "2026-03-01T14:30:00Z",
    "metadata": {
      "referral": {
        "ad_id": "120210572164750432",
        "source": "ADS",
        "type": "OPEN_THREAD",
        "ads_context_data": {
          "ad_title": "Spring Sale 25% Off",
          "photo_url": "https://scontent.xx.fbcdn.net/..."
        }
      }
    }
  }
}
```

Only non-empty referral fields are included. `dm.referral` events (standalone ad clicks without a message) have empty `content.text` and always include `metadata.referral`.

## Request headers

Every webhook request includes:

| Header                  | Value                                   |
| ----------------------- | --------------------------------------- |
| `Content-Type`          | `application/json`                      |
| `X-SocialAPI-Signature` | `sha256=<hmac-sha256-hex>`              |
| `X-SocialAPI-Event`     | The event type, e.g. `comment.received` |

## Verifying signatures

Always verify the `X-SocialAPI-Signature` header before processing a webhook. This confirms the payload came from SocialAPI and was not tampered with.

The signature is HMAC-SHA256 of the raw request body, using your endpoint secret as the key, prefixed with `sha256=`.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import crypto from "crypto";

    function verifySignature(secret, rawBody, signatureHeader) {
      const expected = "sha256=" + crypto
        .createHmac("sha256", secret)
        .update(rawBody)
        .digest("hex");
      return crypto.timingSafeEqual(
        Buffer.from(expected),
        Buffer.from(signatureHeader)
      );
    }

    // Express example
    app.post("/webhooks/socapi", express.raw({ type: "application/json" }), (req, res) => {
      const sig = req.headers["x-socialapi-signature"];
      if (!verifySignature(process.env.SOCAPI_WEBHOOK_SECRET, req.body, sig)) {
        return res.status(401).send("Invalid signature");
      }
      const event = JSON.parse(req.body);
      // handle event.event and event.data
      res.sendStatus(200);
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib

    def verify_signature(secret: str, raw_body: bytes, signature_header: str) -> bool:
        expected = "sha256=" + hmac.new(
            secret.encode(), raw_body, hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(expected, signature_header)

    # Flask example
    @app.route("/webhooks/socapi", methods=["POST"])
    def webhook():
        sig = request.headers.get("X-SocialAPI-Signature", "")
        if not verify_signature(os.environ["SOCAPI_WEBHOOK_SECRET"], request.data, sig):
            abort(401)
        event = request.json
        # handle event["event"] and event["data"]
        return "", 200
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    func verifySignature(secret string, body []byte, sigHeader string) bool {
        mac := hmac.New(sha256.New, []byte(secret))
        mac.Write(body)
        expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
        return hmac.Equal([]byte(expected), []byte(sigHeader))
    }
    ```
  </Tab>
</Tabs>

<Note>
  Use a constant-time comparison (`timingSafeEqual` / `compare_digest` / `hmac.Equal`) to prevent timing attacks. A simple string equality check is not safe.
</Note>

## Responding to webhooks

Your endpoint must return an HTTP `2xx` status within **10 seconds**. The webhook HTTP client enforces a 10-second timeout per delivery attempt. Any non-2xx response or a timeout is treated as a delivery failure.

Return `200` immediately and process the event asynchronously if your handler does heavier work.

## Retry behavior

Failed deliveries are automatically retried up to **5 attempts** with exponential backoff:

| Attempt | Delay        |
| ------- | ------------ |
| 1       | Immediate    |
| 2       | \~30 seconds |
| 3       | \~5 minutes  |
| 4       | \~30 minutes |
| 5       | \~3 hours    |

After 5 failed attempts, the delivery is marked `failed` and no further retries occur.

## Managing endpoints

### List endpoints

```bash theme={null}
curl https://api.social-api.ai/v1/webhooks \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

### Get endpoint details

Retrieve a single endpoint with delivery statistics (delivered/failed counts for the last 24 hours and 7 days). The secret is shown as a hint (last 4 characters only).

```bash theme={null}
curl https://api.social-api.ai/v1/webhooks/{id} \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

### Update an endpoint

Change the URL, subscribed events, or toggle delivery on/off. All fields are optional; provide at least one.

```bash theme={null}
curl -X PATCH https://api.social-api.ai/v1/webhooks/{id} \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'
```

### Delete an endpoint

```bash theme={null}
curl -X DELETE https://api.social-api.ai/v1/webhooks/{id} \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Deleting an endpoint stops all future deliveries immediately. In-flight jobs already queued may still attempt delivery once.

## Delivery monitoring

Every webhook delivery is recorded with its status, HTTP response code, and duration. You can inspect delivery history, view individual attempts, send test payloads, and retry failed deliveries.

| Endpoint                                        | Description                                                                 |
| ----------------------------------------------- | --------------------------------------------------------------------------- |
| `GET /v1/webhooks/{id}/deliveries`              | List deliveries (filterable by `status` and `event_type`, cursor-paginated) |
| `GET /v1/webhooks/{id}/deliveries/{did}`        | Full delivery detail including payload and all retry attempts               |
| `POST /v1/webhooks/{id}/deliveries/{did}/retry` | Re-enqueue a failed delivery                                                |
| `POST /v1/webhooks/{id}/test`                   | Send a test payload to verify your endpoint is working                      |

See the [API Reference](/api-reference) for full request and response schemas.

## Security best practices

* **Always verify signatures** - never trust a webhook payload without checking `X-SocialAPI-Signature`
* **Store your secret in an environment variable** - never hardcode it or commit it to source control
* **Use HTTPS** - HTTP endpoints are rejected at registration time
* **Respond quickly** - return `200` before doing heavy processing to avoid timeouts and spurious retries
* **Make handlers idempotent** - retries mean the same event may arrive more than once; use the interaction `id` to deduplicate
