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

# Format des charges utiles et vérification

> Comment Social media studio signe et livre les événements webhook, avec des exemples de charges utiles pour les événements courants

Chaque webhook livré par Social media studio suit la même enveloppe et le même schéma de signature. Cette page présente la forme exacte de la requête, la façon de vérifier la signature et l'aspect du bloc `data` pour les événements courants.

## Format de la requête

### En-têtes

Chaque livraison inclut ces en-têtes :

| En-tête | Description |
| - | - |
| `Content-Type` | `application/json` |
| `X-Webhook-Event` | Le nom de l'événement, par ex. `post.published` |
| `X-Webhook-Delivery-Id` | UUID de la ligne du journal de livraison, utile pour dédoublonner les nouvelles tentatives |
| `X-Webhook-Signature` | HMAC-SHA256 du corps brut de la requête, encodé en hexadécimal, signé avec le secret de votre endpoint |

### Enveloppe

Sauf si vous configurez un `payloadTemplate` personnalisé, le corps est un objet JSON à quatre champs :

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

* `event` — Le nom exact de l'événement (identique à `X-Webhook-Event`)
* `category` — La catégorie à laquelle appartient l'événement (par ex. `post`, `inbox`, `subscription`)
* `timestamp` — Horodatage ISO 8601 de l'envoi de l'événement
* `data` — La charge utile propre à l'événement (voir les exemples ci-dessous)

> [!NOTE]
>
> Les nouvelles tentatives réutilisent le même `X-Webhook-Delivery-Id`. Utilisez-le comme clé d'idempotence afin qu'une livraison relancée ne retraite pas un événement déjà géré.

## Vérification de la signature

Chaque corps de requête est signé avec le secret de votre endpoint en HMAC-SHA256. Vérifiez-le à chaque requête pour confirmer que la livraison provient bien de Social media studio.

> [!WARNING]
>
> Vérifiez toujours à partir du **corps brut de la requête**, avant son analyse en JSON — resérialiser l'objet analysé modifie les espaces et invalide la signature.

```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)
}
```

## Exemples de charges utiles

La forme du bloc `data` dépend de l'événement. Voici des exemples représentatifs issus des catégories les plus courantes.

### Publication

```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"
  }
}
```

### Approbation

```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"
  }
}
```

### Boîte de réception

```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"
  }
}
```

### Compte 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..."
  }
}
```

### Abonnement

Le champ `newPlanName` contient l'identifiant interne de l'offre, comme `scale` pour 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/..."
  }
}
```

### Base de connaissances

```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
  }
}
```

### Analyses

```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
  }
}
```

### Meilleur moment

```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"
  }
}
```

### Parrainage

```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]
>
> Consultez la [présentation des webhooks](/fr/webhooks/overview.md) pour la liste complète des 162 événements répartis en 26 catégories.

## Modèles de charge utile personnalisés

Si votre endpoint attend une forme différente (par ex. Slack, Discord), définissez un `payloadTemplate` sur votre abonnement webhook. Les variables disponibles sont :

* `{{event}}`
* `{{category}}`
* `{{timestamp}}`
* `{{data}}` — l'objet data complet en JSON
* `{{data.any_field}}` — n'importe quel champ imbriqué de la charge utile propre à l'événement

Les variables non résolues restent en place (par ex. `{{data.postId}}` reste littéral si l'événement ne fournit pas `postId`). Consultez la page de présentation pour des exemples de modèles Discord et Slack.

## Tester en local

Pendant le développement :

1. Utilisez un outil de tunnel comme [ngrok](https://ngrok.com/) ou [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) pour exposer votre serveur local.
2. Créez un abonnement webhook dans **Paramètres → Webhooks** pointant vers l'URL de votre tunnel.
3. Cliquez sur le bouton **Tester** de l'abonnement pour envoyer un événement d'exemple sans déclencher d'activité réelle.
4. Inspectez la charge utile dans [Webhook.site](https://webhook.site/) si vous voulez voir la requête brute avant d'écrire un vérificateur.

> [!WARNING]
>
> Si votre endpoint échoue **10 livraisons consécutives**, l'abonnement est désactivé automatiquement. Réactivez-le depuis le tableau de bord une fois le problème corrigé.


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