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

# Formato de la carga y verificación

> Cómo firma y entrega Social media studio los eventos de webhook, con cargas de ejemplo de los eventos más comunes

Todos los webhooks que entrega Social media studio siguen el mismo envoltorio y esquema de firma. Esta página muestra la forma exacta de la solicitud, cómo verificar la firma y qué aspecto tiene el bloque `data` en los eventos más comunes.

## Formato de la solicitud

### Cabeceras

Cada entrega incluye estas cabeceras:

| Cabecera | Descripción |
| - | - |
| `Content-Type` | `application/json` |
| `X-Webhook-Event` | El nombre del evento, p. ej. `post.published` |
| `X-Webhook-Delivery-Id` | UUID de la fila del registro de entrega, útil para deduplicar reintentos |
| `X-Webhook-Signature` | HMAC-SHA256 del cuerpo sin procesar de la solicitud, codificado en hexadecimal y firmado con el secreto de tu endpoint |

### Envoltorio

A menos que configures un `payloadTemplate` personalizado, el cuerpo es un objeto JSON con cuatro campos:

```json theme={null}
{
  "event": "post.published",
  "category": "post",
  "timestamp": "2026-04-22T14:30:00.000Z",
  "data": {
    "postId": "01J...",
    "platform": "instagram",
    "userId": "usr_..."
  }
}
```

* `event` — El nombre exacto del evento (coincide con `X-Webhook-Event`)
* `category` — La categoría a la que pertenece el evento (p. ej. `post`, `inbox`, `subscription`)
* `timestamp` — Marca de tiempo ISO-8601 del momento en que se envió el evento
* `data` — La carga específica del evento (consulta los ejemplos más abajo)

> [!NOTE]
>
> Los reintentos reutilizan el mismo `X-Webhook-Delivery-Id`. Úsalo como clave de idempotencia para que una entrega reintentada no vuelva a procesar un evento ya gestionado.

## Verificación de la firma

El cuerpo de cada solicitud se firma con el secreto de tu endpoint mediante HMAC-SHA256. Verifícalo en cada solicitud para confirmar que la entrega procede realmente de Social media studio.

> [!WARNING]
>
> Verifica siempre con el **cuerpo sin procesar de la solicitud** antes de analizarlo como JSON: volver a serializar el objeto analizado cambiará los espacios en blanco e invalidará la firma.

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

const app = express();
const WEBHOOK_SECRET = process.env.SOMESTUDIO_WEBHOOK_SECRET;

app.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-Webhook-Signature");
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    if (
      !signature ||
      !crypto.timingSafeEqual(
        Buffer.from(signature, "hex"),
        Buffer.from(expected, "hex"),
      )
    ) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(req.body.toString("utf8"));
    console.log(payload.event, payload.data);

    res.status(200).send("ok");
  },
);

app.listen(3000);
```

```python Python / Flask theme={null}
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SOMESTUDIO_WEBHOOK_SECRET"]

@app.post("/webhook")
def webhook():
    signature = request.headers.get("X-Webhook-Signature", "")
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        request.get_data(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    payload = request.get_json()
    print(payload["event"], payload["data"])
    return "ok", 200
```

```php PHP theme={null}
<?php
$secret    = getenv('SOMESTUDIO_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$body      = file_get_contents('php://input');
$expected  = hash_hmac('sha256', $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

$payload = json_decode($body, true);
error_log($payload['event']);

http_response_code(200);
echo 'ok';
```

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

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

var secret = []byte(os.Getenv("SOMESTUDIO_WEBHOOK_SECRET"))

func webhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    defer r.Body.Close()

    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Webhook-Signature"))) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ok"))
}

func main() {
    http.HandleFunc("/webhook", webhook)
    http.ListenAndServe(":3000", nil)
}
```

## Cargas de ejemplo

La forma del bloque `data` depende del evento. Estos son ejemplos representativos de las categorías más usadas.

### Publicación

```json theme={null}
{
  "event": "post.published",
  "category": "post",
  "timestamp": "2026-04-22T14:30:00.000Z",
  "data": {
    "postId": "01JABC123...",
    "platform": "instagram",
    "userId": "usr_abc123"
  }
}
```

```json theme={null}
{
  "event": "post.failed",
  "category": "post",
  "timestamp": "2026-04-22T14:31:00.000Z",
  "data": {
    "postId": "01JABC123...",
    "platform": "twitter",
    "failureReason": "Social account session has expired"
  }
}
```

### Aprobación

```json theme={null}
{
  "event": "post.approval.approved",
  "category": "approval",
  "timestamp": "2026-04-22T14:32:00.000Z",
  "data": {
    "postId": "01JABC123...",
    "userId": "usr_abc123",
    "reviewerId": "mem_xyz789"
  }
}
```

### Bandeja de entrada

```json theme={null}
{
  "event": "inbox.message_received",
  "category": "inbox",
  "timestamp": "2026-04-22T14:33:00.000Z",
  "data": {
    "threadId": "thr_abc",
    "messageId": "msg_xyz",
    "platform": "instagram",
    "fromUser": { "id": "ig_456", "name": "Jane Doe" }
  }
}
```

### WhatsApp

```json theme={null}
{
  "event": "whatsapp.message_sent",
  "category": "whatsapp",
  "timestamp": "2026-04-22T14:33:30.000Z",
  "data": {
    "accountId": "15551234567",
    "to": "+15557654321",
    "templateName": "order_update",
    "messageId": "wamid.HBgM..."
  }
}
```

```json theme={null}
{
  "event": "whatsapp.template_created",
  "category": "whatsapp",
  "timestamp": "2026-04-22T14:33:45.000Z",
  "data": {
    "accountId": "15551234567",
    "name": "order_update",
    "language": "en_US",
    "category": "UTILITY"
  }
}
```

### Cuenta social

```json theme={null}
{
  "event": "account.token_expired",
  "category": "account",
  "timestamp": "2026-04-22T14:34:00.000Z",
  "data": {
    "accountId": "acc_123",
    "platform": "facebook",
    "postId": "01JABC123..."
  }
}
```

### Suscripción

El campo `newPlanName` contiene el identificador interno del plan, como `scale` para Advanced.

```json theme={null}
{
  "event": "subscription.upgraded",
  "category": "subscription",
  "timestamp": "2026-04-22T14:35:00.000Z",
  "data": {
    "subscriptionId": "lsq_sub_123",
    "newPlanId": "pln_scale",
    "newPlanName": "scale"
  }
}
```

### IA

```json theme={null}
{
  "event": "ai.image_generated",
  "category": "ai",
  "timestamp": "2026-04-22T14:36:00.000Z",
  "data": {
    "imageId": "img_abc",
    "userId": "usr_xyz",
    "prompt": "A sunset over the mountains",
    "url": "https://cdn.so-me.studio/..."
  }
}
```

### Centro de conocimiento

```json theme={null}
{
  "event": "knowledge.created",
  "category": "knowledge",
  "timestamp": "2026-04-22T14:36:30.000Z",
  "data": {
    "id": "kn_abc123",
    "title": "Brand voice guide",
    "sourceType": "pdf",
    "isActive": true
  }
}
```

### Analíticas

```json theme={null}
{
  "event": "analytics.report_generated",
  "category": "analytics",
  "timestamp": "2026-04-22T14:00:00.000Z",
  "data": {
    "generatedAt": "2026-04-22T14:00:00.000Z",
    "accountCount": 4,
    "windowHours": 2
  }
}
```

### Mejor momento

```json theme={null}
{
  "event": "best_time.recommendation_recomputed",
  "category": "best_time",
  "timestamp": "2026-04-22T03:00:00.000Z",
  "data": {
    "socialAccountId": "acc_123",
    "platform": "instagram",
    "isPersonalized": true,
    "postCount": 87,
    "timezone": "America/New_York"
  }
}
```

### Referidos

```json theme={null}
{
  "event": "referral.reward_earned",
  "category": "referral",
  "timestamp": "2026-04-22T14:37:00.000Z",
  "data": {
    "referralId": "ref_abc",
    "userId": "usr_xyz",
    "role": "referrer",
    "creditsAwarded": 10
  }
}
```

> [!NOTE]
>
> Consulta el [resumen de webhooks](/es/webhooks/overview.md) para ver la lista completa de 162 eventos en 26 categorías.

## Plantillas de carga personalizadas

Si tu endpoint espera otra estructura (p. ej. Slack, Discord), define un `payloadTemplate` en tu suscripción de webhook. Las variables disponibles son:

* `{{event}}`
* `{{category}}`
* `{{timestamp}}`
* `{{data}}` — el objeto de datos completo como JSON
* `{{data.any_field}}` — cualquier campo anidado de la carga específica del evento

Las variables sin resolver se dejan tal cual (p. ej. `{{data.postId}}` permanece literal si el evento no proporciona `postId`). Consulta la página de resumen para ver ejemplos de plantillas de Discord y Slack.

## Probar en local

Durante el desarrollo:

1. Usa una herramienta de túnel como [ngrok](https://ngrok.com/) o [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) para exponer tu servidor local.
2. Crea una suscripción de webhook en **Ajustes → Webhooks** que apunte a la URL de tu túnel.
3. Haz clic en el botón **Probar** de la suscripción para enviar un evento de muestra sin generar actividad real.
4. Inspecciona la carga en [Webhook.site](https://webhook.site/) si quieres ver la solicitud sin procesar antes de escribir un verificador.

> [!WARNING]
>
> Si tu endpoint falla **10 entregas consecutivas**, la suscripción se deshabilita automáticamente. Vuelve a habilitarla desde el panel después de corregir el problema.


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