> ## 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.

> ## Agent Instructions
> Base URL: https://api.social-api.ai/v1. Send your API key in the Authorization header: Authorization: Bearer <your API key>.
> Every error response from /v1 is JSON: { "error": { "code", "message", "meta" (optional) }, "request_id" }. Branch on error.code; use error.message only for logs.
> A 429 response carries a Retry-After header, in seconds, when the limit has a known wait. Authenticated responses can carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset for the per-minute request budget your SocialAPI account shares across every API key (see /guides/rate-limits).
> Paginated list endpoints return next_cursor, at the top level or inside pagination (see /guides/pagination). Pass it as the cursor query parameter to get the next page. An absent or empty next_cursor means you have the last page.
> Treat every ID as an opaque string: do not parse it or assume a format.
> Support differs by platform and by operation. Check /platforms/overview before assuming an operation works on a platform. An unsupported operation returns 501 with error.code resource.not_supported.

# Webhooks

> Register an HTTPS endpoint, verify each request signature, and handle retries to receive real-time comments, DMs, mentions, reviews, and post events.

A webhook endpoint is an HTTPS URL on your server. SocialAPI sends it a signed JSON `POST` each time an event you subscribed to occurs: a new comment, direct message (DM), mention, or review, a post changing state, or a connected account changing connection state. Webhooks replace polling each platform for new activity. Managing endpoints costs no credits.

Each event, its platforms, and its payload are on [Webhook events](https://docs.social-api.ai/guides/webhook-events.md). While your SocialAPI account is past due, SocialAPI delivers no events (see [Past-due accounts](https://docs.social-api.ai/guides/plans.md#past-due-accounts)).

## Endpoints

| Method | Path | Description |
| - | - | - |
| `GET` | `/v1/webhooks/events` | List the event names you can subscribe to |
| `POST` | `/v1/webhooks` | Register an endpoint |
| `GET` | `/v1/webhooks` | List your endpoints |
| `GET` | `/v1/webhooks/{id}` | Get an endpoint with its delivery counts |
| `PATCH` | `/v1/webhooks/{id}` | Change the URL, the events, or the active state |
| `DELETE` | `/v1/webhooks/{id}` | Delete an endpoint |
| `GET` | `/v1/webhooks/{id}/deliveries` | List an endpoint's deliveries |
| `GET` | `/v1/webhooks/{id}/deliveries/{did}` | Get one delivery with its payload and attempts |
| `POST` | `/v1/webhooks/{id}/deliveries/{did}/retry` | Send a delivery again |
| `POST` | `/v1/webhooks/{id}/test` | Send a test payload to an endpoint |

You can also manage endpoints in the dashboard under **Webhooks**.

## Register an endpoint

Subscribe an endpoint to 3 events:

```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": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90",
  "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."
}
```

| Field | Required | Description |
| - | - | - |
| `url` | Yes | The URL that receives events. It must use `https` |
| `events` | Yes | A non-empty array of event names from [Webhook events](https://docs.social-api.ai/guides/webhook-events.md) |

The `secret` is a 64-character string. It is returned once, in this response. `GET /v1/webhooks/{id}` returns only a hint made of `whsec_...` and the last 4 characters. To replace a lost secret, register a new endpoint and delete the old one.

SocialAPI does not call your URL when you register it, so registration succeeds even when the server is not running. Send a test delivery to check the endpoint.

An invalid request returns `400 validation.field_required` (no `url`), `400 validation.field_invalid` (`url` is not an HTTPS URL, `events` is empty, or an event name is unknown), or `400 validation.body_invalid` (the body is not valid JSON).

An endpoint receives events for every connected account and brand of your SocialAPI account. There is no filter by account or brand: route each event with the `brand_id` it carries (see [Route events by brand](https://docs.social-api.ai/guides/webhook-events.md#route-events-by-brand)).

## What SocialAPI sends

Every delivery is a `POST` with a JSON body that holds an `event` name and a `data` value: an object, except an array on a `mention.received` event sent when you list mentions (see the [event envelope](https://docs.social-api.ai/guides/webhook-events.md#event-envelope)). These headers are on every request:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `X-SocialAPI-Event` | The event name, for example `comment.received` |
| `X-SocialAPI-Timestamp` | Unix time in seconds when SocialAPI sent this attempt |
| `X-SocialAPI-Signature-V2` | `sha256=` followed by the hex HMAC-SHA256 of `<timestamp>.<raw body>`. Use this one |
| `X-SocialAPI-Signature` | `sha256=` followed by the hex HMAC-SHA256 of the raw body alone. It has no freshness check |
| `X-SocialAPI-Delivery` | The delivery's ID. It is the same on every retry of the delivery. A test delivery has none |

## Verify signatures

Verify `X-SocialAPI-Signature-V2` on every request before you act on it. The signature proves the request came from SocialAPI and that nobody changed the body. Follow these rules:

1. Read the raw request body. Do not parse it and serialize it again, because that changes the bytes.
2. Use the endpoint `secret` exactly as returned, as the key. Do not decode it from hex.
3. Compute the HMAC-SHA256 of the timestamp header value, a literal `.`, and the raw body. Prefix the hex digest with `sha256=`.
4. Compare it with `X-SocialAPI-Signature-V2` in constant time.
5. Reject the request when the timestamp is more than 5 minutes from your clock (see [Replay protection](#replay-protection)).

```javascript Node.js theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();

function verify(secret, rawBody, headers, toleranceSeconds = 300) {
  const timestamp = headers["x-socialapi-timestamp"];
  const received = headers["x-socialapi-signature-v2"];
  if (!timestamp || !received) return false;

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > toleranceSeconds) return false;

  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/socapi", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(process.env.SOCAPI_WEBHOOK_SECRET, req.body, req.headers)) {
    return res.sendStatus(401);
  }
  const payload = JSON.parse(req.body.toString("utf8"));
  // Store payload, then process it after you respond.
  res.sendStatus(200);
});
```

```python Python theme={null}
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)


def verify(secret: str, raw_body: bytes, headers, tolerance_seconds: int = 300) -> bool:
    timestamp = headers.get("X-SocialAPI-Timestamp", "")
    received = headers.get("X-SocialAPI-Signature-V2", "")
    try:
        age = abs(time.time() - int(timestamp))
    except ValueError:
        return False
    if age > tolerance_seconds:
        return False

    expected = "sha256=" + hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received)


@app.post("/webhooks/socapi")
def webhook():
    if not verify(os.environ["SOCAPI_WEBHOOK_SECRET"], request.get_data(), request.headers):
        abort(401)
    payload = request.get_json()
    # Store payload, then process it after you respond.
    return "", 200
```

```go Go theme={null}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"math"
	"net/http"
	"os"
	"strconv"
	"time"
)

func verify(secret string, rawBody []byte, h http.Header) bool {
	timestamp := h.Get("X-SocialAPI-Timestamp")
	received := h.Get("X-SocialAPI-Signature-V2")
	sent, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-sent)) > 300 {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(rawBody)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(received))
}

func handler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil || !verify(os.Getenv("SOCAPI_WEBHOOK_SECRET"), body, r.Header) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}
	// Store body, then process it after you respond.
	w.WriteHeader(http.StatusOK)
}

func main() {
	http.HandleFunc("/webhooks/socapi", handler)
	http.ListenAndServe(":8080", nil)
}
```

```php PHP theme={null}
<?php
function verify(string $secret, string $rawBody, array $headers, int $tolerance = 300): bool {
    $timestamp = $headers['X-SocialAPI-Timestamp'] ?? '';
    $received = $headers['X-SocialAPI-Signature-V2'] ?? '';
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > $tolerance) {
        return false;
    }
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $received);
}

$rawBody = file_get_contents('php://input');
if (!verify(getenv('SOCAPI_WEBHOOK_SECRET'), $rawBody, getallheaders())) {
    http_response_code(401);
    exit;
}
$payload = json_decode($rawBody, true);
// Store $payload, then process it after you respond.
http_response_code(200);
```

Use a constant-time comparison (`timingSafeEqual`, `compare_digest`, `hmac.Equal`, `hash_equals`). A plain string comparison leaks timing information.

`X-SocialAPI-Signature` (v1) is the HMAC-SHA256 of the raw body alone, prefixed with `sha256=`. It is kept for existing integrations. It proves authenticity but not freshness, so a captured request still verifies later.

## Replay protection

`X-SocialAPI-Signature-V2` covers the timestamp, so an attacker cannot move a captured request into a later time window by editing the header. Reject any request whose `X-SocialAPI-Timestamp` is more than 5 minutes from your clock. The timestamp is the time of the attempt, so a retry of an old event carries a new timestamp and passes the check.

To process each event once, store the `X-SocialAPI-Delivery` IDs you handled and skip repeats. A manual retry (`POST .../retry`) creates a new delivery with a new ID, so also deduplicate on the event's own identity: `data.id` for comment and DM events, `data.id` or each `data[].id` for `mention.received`, and `data.post_id` with the event name for post events.

## Respond to a webhook

Your endpoint must return an HTTP `2xx` status within 10 seconds. Any other status, a timeout, or a connection error counts as a failed attempt. Return `200` once you have stored the body, and do the real work afterward.

SocialAPI delivers each event at least once. The same event can arrive more than once, and events can arrive in a different order than they happened. Make your handler idempotent, and read the current state from the API when order matters.

While an endpoint's `is_active` is `false`, SocialAPI sends it nothing. It does not send those events later when you turn the endpoint on again. To recover what you missed, read `GET /v1/events` (within your plan's event history) or the inbox and post endpoints.

## Retries

A failed attempt is retried until the delivery has used 10 attempts. The wait before each retry grows, so an endpoint that recovers within about 14 hours of the first attempt still receives the event:

| Attempt | Sent | Time since attempt 1 |
| - | - | - |
| 1 | When the event occurs | |
| 2 | 10 seconds after attempt 1 fails | about 10 seconds |
| 3 | 1 minute after attempt 2 fails | about 1 minute |
| 4 | 5 minutes after attempt 3 fails | about 6 minutes |
| 5 | 15 minutes after attempt 4 fails | about 21 minutes |
| 6 | 30 minutes after attempt 5 fails | about 51 minutes |
| 7 | 1 hour after attempt 6 fails | about 2 hours |
| 8 | 2 hours after attempt 7 fails | about 4 hours |
| 9 | 4 hours after attempt 8 fails | about 8 hours |
| 10 | 6 hours after attempt 9 fails | about 14 hours |

Each wait varies by up to 10 percent either way. After the tenth failed attempt, about 14 hours after the first, the delivery's `status` becomes `failed` and SocialAPI makes no further attempts. Send a failed delivery again yourself with `POST /v1/webhooks/{id}/deliveries/{did}/retry`.

A retried event can arrive hours after events that happened later. Order events by their own timestamps, not by arrival.

When an attempt ends in a connection error, a `429`, or a `5xx` other than `501`, SocialAPI sends the same request one more time within the same attempt. Your endpoint can therefore receive the same delivery twice, seconds apart. Both requests carry the same `X-SocialAPI-Delivery`.

## Manage endpoints

List your endpoints:

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

Response (HTTP 200):

```json theme={null}
{
  "data": [
    {
      "id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90",
      "url": "https://app.example.com/webhooks/socapi",
      "events": ["comment.received", "dm.received", "review.received"],
      "is_active": true,
      "created_at": "2026-10-13T09:00:00Z"
    }
  ],
  "count": 1
}
```

`GET /v1/webhooks/{id}` returns the same fields plus `secret_hint` and delivery counts:

```json theme={null}
{
  "id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90",
  "url": "https://app.example.com/webhooks/socapi",
  "events": ["comment.received", "dm.received", "review.received"],
  "is_active": true,
  "secret_hint": "whsec_...f5a8",
  "created_at": "2026-10-13T09:00:00Z",
  "stats": {
    "last_24h": { "delivered": 142, "failed": 3 },
    "last_7d": { "delivered": 980, "failed": 11 }
  }
}
```

Change an endpoint with `PATCH`. Send at least 1 of `url`, `events`, and `is_active`. `events` replaces the whole list. The `url` and `events` rules are the same as at registration, and a body with no fields returns `400 validation.body_invalid`.

```bash theme={null}
curl -X PATCH https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90 \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"is_active": false}'
```

The response (HTTP 200) is the updated endpoint, in the shape of the list items.

Delete an endpoint:

```bash theme={null}
curl -X DELETE https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90 \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{ "ok": true }
```

Deleting stops future deliveries. A delivery already in progress can still reach your URL once. An unknown endpoint ID returns `404 resource.not_found`.

## Monitor deliveries

SocialAPI records every delivery with its status, the HTTP status of your response, and how long your endpoint took. A delivery's `status` is `pending` (not finished), `delivered` (your endpoint returned `2xx`), or `failed` (10 attempts used).

```bash theme={null}
curl "https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90/deliveries?status=failed&limit=20" \
  -H "Authorization: Bearer $SOCAPI_KEY"
```

Response (HTTP 200):

```json theme={null}
{
  "data": [
    {
      "id": "b1d4e7a2-6c3f-4a90-8e15-2f7c9d0a3b68",
      "event_type": "comment.received",
      "status": "failed",
      "response_status_code": 500,
      "response_duration_ms": 412,
      "attempt_count": 5,
      "description": "Instagram comment received",
      "created_at": "2026-10-13T14:30:00Z"
    }
  ],
  "next_cursor": "2026-10-13T14:12:00Z",
  "count": 1
}
```

| Parameter | Description |
| - | - |
| `status` | Filter by `pending`, `delivered`, or `failed` |
| `event_type` | Filter by event name |
| `limit` | Page size from 1 to 100. The default is 50. A value outside the range uses 50 |
| `cursor` | The `next_cursor` of the previous page. See [Pagination](https://docs.social-api.ai/guides/pagination.md) |

`count` is the number of deliveries that match the filters across all pages. `next_cursor` is absent on the last page. Deliveries are sorted newest first.

`GET /v1/webhooks/{id}/deliveries/{did}` returns the delivery with the exact `payload` that was sent and every attempt:

```json theme={null}
{
  "id": "b1d4e7a2-6c3f-4a90-8e15-2f7c9d0a3b68",
  "endpoint_id": "3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90",
  "event_type": "comment.received",
  "status": "failed",
  "response_status_code": 500,
  "response_duration_ms": 412,
  "attempt_count": 5,
  "description": "Instagram comment received",
  "payload": { "event": "comment.received", "data": { "id": "sapi_cmt_GXRM4BNTW7KFYSD3HAEV6QJZ2CLP9U1I" } },
  "attempts": [
    {
      "attempt_number": 1,
      "status_code": 500,
      "response_body": "Internal Server Error",
      "duration_ms": 412,
      "created_at": "2026-10-13T14:30:01Z"
    }
  ],
  "created_at": "2026-10-13T14:30:00Z"
}
```

Each attempt holds the first 10,240 bytes of your response body. An attempt that ended in a connection error has an `error_message` in place of a response. The example `payload` is shortened. The real one is the full body that was sent.

`POST /v1/webhooks/{id}/deliveries/{did}/retry` creates a new delivery with the same event and payload and the description `Retry of {did}`. It returns the new delivery with `status` `pending`. The new delivery gets 10 attempts of its own. The original delivery is unchanged.

SocialAPI keeps deliveries for the number of days your plan allows (see [History and export allowances](https://docs.social-api.ai/guides/plans.md#history-and-export-allowances)).

## Test an endpoint

`POST /v1/webhooks/{id}/test` sends one request to the endpoint and returns the result. You choose the event name and the body:

```bash theme={null}
curl -X POST https://api.social-api.ai/v1/webhooks/3f6b0c1e-5a47-4d2b-9a0e-8c1d7b2e4f90/test \
  -H "Authorization: Bearer $SOCAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "comment.received",
    "payload": { "event": "comment.received", "data": { "text": "Test comment" } }
  }'
```

Response (HTTP 200):

```json theme={null}
{
  "delivery_id": "c8a2f5d1-9e47-4b63-a1d0-7e3b5c2f8a94",
  "status": "delivered",
  "response_status_code": 200,
  "response_body": "ok",
  "response_duration_ms": 87
}
```

SocialAPI sends `payload` exactly as you give it, as the request body. It does not wrap it. The request carries the signature headers and `X-SocialAPI-Event` set to `event_type`, but no `X-SocialAPI-Delivery`. `status` is `delivered` for a `2xx` response and `failed` otherwise, and `response_status_code` is `0` when the connection failed. SocialAPI does not retry a failed test. The delivery appears in the delivery list with the description `Test delivery`. `event_type` and `payload` are both required (`400 validation.field_required`). `event_type` can be any string, including names that are not subscribable.

## Security

* **Verify every signature** with `X-SocialAPI-Signature-V2` and reject stale timestamps.
* **Keep the secret out of source control.** Store it in an environment variable or a secret manager.
* **Use HTTPS only.** SocialAPI rejects any other scheme at registration.
* **Return `200` fast** and process afterward, to avoid timeouts and duplicate retries.
* **Make handlers idempotent.** SocialAPI delivers each event at least once.
* **Rotate by replacement.** There is no rotate call: register a new endpoint, switch your handler to its secret, then delete the old endpoint.

## Platform support

Which platforms emit each event is listed under the event on [Webhook events](https://docs.social-api.ai/guides/webhook-events.md). Platforms that push nothing to SocialAPI have no comment, DM, mention, or review events: read those with the inbox endpoints.

## Limits

| Limit | Value |
| - | - |
| Response time per request | 10 seconds |
| Attempts per delivery | 10 |
| Response body kept per attempt | 10,240 bytes |
| Deliveries per list page | 1 to 100, default 50 |
| Delivery history | Per plan: [History and export allowances](https://docs.social-api.ai/guides/plans.md#history-and-export-allowances) |
| Endpoint URL scheme | `https` only |

## Errors and debugging

When an event does not arrive, check in this order:

1. **Endpoint state.** `GET /v1/webhooks/{id}` must show `is_active: true` and the event name in `events`. An event is delivered only to endpoints subscribed to it when it occurs.
2. **Past due.** While your SocialAPI account is past due, SocialAPI sends no events and records no deliveries. Calls to the API return `402 billing.past_due`.
3. **Platform support.** Check **Emitted for** under the event on [Webhook events](https://docs.social-api.ai/guides/webhook-events.md). A platform that does not emit the event never sends it.
4. **Deliveries.** `GET /v1/webhooks/{id}/deliveries?status=failed` lists failures. Read the attempts of one delivery: `status_code` is your endpoint's response, and `error_message` explains a connection error or timeout.
5. **Signature.** When your handler returns `401` on every request, check that you hash the raw body, key the HMAC with the secret string as returned, and compare against `X-SocialAPI-Signature-V2`.
6. **Test.** Send `POST /v1/webhooks/{id}/test` to separate your endpoint's behavior from SocialAPI's event delivery.

Other codes on these endpoints: `401 auth.invalid_key` and its siblings for a missing or invalid key, `403 auth.insufficient_scope` when the key lacks `webhooks:manage`, and `404 resource.not_found` for an unknown endpoint or delivery. Every code is in [Errors](https://docs.social-api.ai/guides/errors.md).

## Scopes

| Scope | Grants |
| - | - |
| `webhooks:manage` | Every endpoint on this page except `GET /v1/webhooks/events` |

A brand-restricted key is refused these endpoints with `403 auth.insufficient_scope`, even when it holds `webhooks:manage`, because endpoints belong to your SocialAPI account and not to one brand. See [Scoped API keys](https://docs.social-api.ai/guides/scoped-keys.md).

## Use from an AI agent

| Tool | What it does |
| - | - |
| `social_api_list_webhooks` | List endpoints |
| `social_api_create_webhook` | Register an endpoint. Returns the secret once |
| `social_api_update_webhook` | Change the URL, events, or `is_active` |
| `social_api_delete_webhook` | Delete an endpoint |
| `social_api_get_webhook` | Get an endpoint with its delivery counts |
| `social_api_list_webhook_deliveries` | List deliveries, with filters |
| `social_api_get_webhook_delivery` | Get one delivery with its attempts |
| `social_api_retry_webhook_delivery` | Send a delivery again |
| `social_api_test_webhook` | Send a test payload |

See [MCP tools](https://docs.social-api.ai/integrations/tools.md) for the full list.

## Related

* [Webhook events](https://docs.social-api.ai/guides/webhook-events.md): every event, its platforms, and its payload.
* [Track post delivery](https://docs.social-api.ai/guides/delivery.md): the post statuses and target errors that the `post.*` events report.
* [Scoped API keys](https://docs.social-api.ai/guides/scoped-keys.md): restrict a key to `webhooks:manage` for a webhook-only integration.
* [Plans and usage](https://docs.social-api.ai/guides/plans.md): delivery history length, and what happens while an account is past due.
* [Errors](https://docs.social-api.ai/guides/errors.md): every error code and how to handle it.
* [Automations](https://docs.social-api.ai/guides/automations.md): answer Instagram and Facebook comments and DMs without running a webhook handler.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.