Aller au contenu

API — Exemples v1

Ce catalogue fournit des exemples copiables pour tous les endpoints de l'API SalamBot v1. Chaque exemple respecte l'envelope unifié et inclut l'authentification Bearer complète.

Références :

Variables d'environnement

NOTE: Tous les exemples utilisent $BASE_URL sans re-préfixer /v1 (ex: $BASE_URL/messages/analyze).

Configuration bash

## Variables globales
export BASE_URL="https://api.salambot.local/v1"
export TOKEN="YOUR_TOKEN"
export TENANT="acme"
export LOCALE="fr-MA"
export REQUEST_ID="00000000-0000-4000-8000-000000000001"
export IDEMPOTENCY_KEY="00000000-0000-4000-8000-000000000002"
export TIMESTAMP="2025-08-14T10:30:00Z"

Configuration Python

import os
import requests
import uuid
from datetime import datetime

# Configuration
BASE_URL = "https://api.salambot.local/v1"
TOKEN = os.getenv("SALAMBOT_TOKEN", "YOUR_TOKEN")
TENANT = "acme"
LOCALE = "fr-MA"

## Headers standards
HEADERS = {
    "Authorization": f"Bearer {TOKEN}",
    "Content-Type": "application/json",
    "X-SalamBot-Tenant": TENANT,
    "X-Request-Id": str(uuid.uuid4())
}

def get_timestamp():
    return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")

Configuration JavaScript

// Configuration
const BASE_URL = 'https://api.salambot.local/v1';
const TOKEN = process.env.SALAMBOT_TOKEN || 'YOUR_TOKEN';
const TENANT = 'acme';
const LOCALE = 'fr-MA';

// Headers standards
const headers = {
  Authorization: `Bearer ${TOKEN}`,
  'Content-Type': 'application/json',
  'X-SalamBot-Tenant': TENANT,
  'X-Request-Id': crypto.randomUUID(),
};

const getTimestamp = () => new Date().toISOString();

Authentification & En-têtes

NOTE: Ces en-têtes (Authorization, X-SalamBot-Tenant, X-Request-Id, Idempotency-Key pour POST) s'appliquent aux endpoints de l'API. Exclusion: webhooks.

En-têtes recommandés

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
Content-Type: application/json; charset=utf-8
X-SalamBot-Tenant: acme
X-Request-Id: 00000000-0000-4000-8000-000000000001
Idempotency-Key: 00000000-0000-4000-8000-000000000002

Exemple d'URL correcte

# Correct: utilisation de $BASE_URL sans double /v1
curl -X POST "$BASE_URL/messages/analyze" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json"

Structure envelope standard

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "webchat",
  "message_id": "msg_123",
  "correlation_id": "corr_abc",
  "timestamp": "2025-08-14T10:30:00Z",
  "locale": "fr-MA",
  "data": {
    "NOTE": "payload spécifique à l'endpoint"
  }
}

Exemples par endpoint

POST /v1/messages/analyze

Analyse NLU d'un message utilisateur.

cURL - Analyse NLU

curl -X POST "$BASE_URL/messages/analyze" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "webchat",
    "message_id": "msg_123",
    "correlation_id": "corr_abc",
    "timestamp": "2025-08-14T10:30:00Z",
    "locale": "fr-MA",
    "data": {
      "text": "Je veux annuler ma commande",
      "lang": "fr"
    }
  }'

Python - Analyse NLU

import requests
import json

url = f"{BASE_URL}/messages/analyze"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "webchat",
    "message_id": "msg_123",
    "correlation_id": "corr_abc",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "text": "Je veux annuler ma commande",
        "lang": "fr"
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

JavaScript - Analyse NLU

const analyzeMessage = async () => {
  const url = `${BASE_URL}/messages/analyze`;
  const payload = {
    schema_version: '1.0',
    tenant: TENANT,
    channel: 'webchat',
    message_id: 'msg_123',
    correlation_id: 'corr_abc',
    timestamp: getTimestamp(),
    locale: LOCALE,
    data: {
      text: 'Je veux annuler ma commande',
      lang: 'fr',
    },
  };

  const headersWithIdempotency = {
    ...headers,
    'Idempotency-Key': crypto.randomUUID(),
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: headersWithIdempotency,
    body: JSON.stringify(payload),
  });

  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "webchat",
  "message_id": "msg_123",
  "correlation_id": "corr_abc",
  "timestamp": "2025-08-14T10:30:01Z",
  "locale": "fr-MA",
  "data": {
    "intent": "cancel_order",
    "confidence": 0.92,
    "entities": [
      { "type": "action", "value": "annuler", "start": 8, "end": 15 }
    ]
  }
}

POST /v1/search/query

Recherche RAG dans la base de connaissances.

cURL - Recherche RAG

curl -X POST "$BASE_URL/v1/search/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "webchat",
    "message_id": "msg_456",
    "correlation_id": "corr_def",
    "timestamp": "2025-08-14T10:32:00Z",
    "locale": "fr-MA",
    "data": {
      "query": "politique de remboursement",
      "filters": {"category": "support"},
      "limit": 5
    }
  }'

Python - Recherche RAG

url = f"{BASE_URL}/v1/search/query"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "webchat",
    "message_id": "msg_456",
    "correlation_id": "corr_def",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "query": "politique de remboursement",
        "filters": {"category": "support"},
        "limit": 5
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

JavaScript - Recherche RAG

const searchQuery = async () => {
  const url = `${BASE_URL}/v1/search/query`;
  const payload = {
    schema_version: '1.0',
    tenant: TENANT,
    channel: 'webchat',
    message_id: 'msg_456',
    correlation_id: 'corr_def',
    timestamp: getTimestamp(),
    locale: LOCALE,
    data: {
      query: 'politique de remboursement',
      filters: { category: 'support' },
      limit: 5,
    },
  };

  const headersWithIdempotency = {
    ...headers,
    'Idempotency-Key': crypto.randomUUID(),
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: headersWithIdempotency,
    body: JSON.stringify(payload),
  });

  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "webchat",
  "message_id": "msg_456",
  "correlation_id": "corr_def",
  "timestamp": "2025-08-14T10:32:01Z",
  "locale": "fr-MA",
  "data": {
    "passages": [
      "Les remboursements sont traités sous 7 jours...",
      "Pour demander un remboursement, contactez..."
    ],
    "ids": ["doc_123", "doc_456"],
    "scores": [0.89, 0.76]
  }
}

POST /v1/generate/reply

Génération de réponse via LLM Router.

cURL - Génération LLM

curl -X POST "$BASE_URL/v1/generate/reply" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "webchat",
    "message_id": "msg_789",
    "correlation_id": "corr_ghi",
    "timestamp": "2025-08-14T10:34:00Z",
    "locale": "fr-MA",
    "data": {
      "prompt": "Comment puis-je annuler ma commande ?",
      "context": [
        "L'utilisateur a une commande active #12345",
        "Politique: annulation possible sous 24h"
      ],
      "model": "llama3"
    }
  }'

Python - Génération LLM

url = f"{BASE_URL}/v1/generate/reply"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "webchat",
    "message_id": "msg_789",
    "correlation_id": "corr_ghi",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "prompt": "Comment puis-je annuler ma commande ?",
        "context": [
            "L'utilisateur a une commande active #12345",
            "Politique: annulation possible sous 24h"
        ],
        "model": "llama3"
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

JavaScript - Génération LLM

const generateReply = async () => {
  const url = `${BASE_URL}/v1/generate/reply`;
  const payload = {
    schema_version: '1.0',
    tenant: TENANT,
    channel: 'webchat',
    message_id: 'msg_789',
    correlation_id: 'corr_ghi',
    timestamp: getTimestamp(),
    locale: LOCALE,
    data: {
      prompt: 'Comment puis-je annuler ma commande ?',
      context: [
        "L'utilisateur a une commande active #12345",
        'Politique: annulation possible sous 24h',
      ],
      model: 'llama3',
    },
  };

  const headersWithIdempotency = {
    ...headers,
    'Idempotency-Key': crypto.randomUUID(),
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: headersWithIdempotency,
    body: JSON.stringify(payload),
  });

  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "webchat",
  "message_id": "msg_789",
  "correlation_id": "corr_ghi",
  "timestamp": "2025-08-14T10:34:02Z",
  "locale": "fr-MA",
  "data": {
    "reply_text": "Pour annuler votre commande #12345, vous pouvez...",
    "model_used": "llama3",
    "tokens": 156,
    "confidence": 0.94,
    "filtered": false
  }
}

POST /v1/incidents

Création d'incident système.

cURL - Incidents

curl -X POST "$BASE_URL/incidents" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "system",
    "message_id": "msg_inc1",
    "correlation_id": "corr_inc",
    "timestamp": "2025-08-14T10:36:00Z",
    "locale": "fr-MA",
    "data": {
      "trigger": "Panne serveur principal",
      "severity": "high",
      "details": {
        "affected_services": ["webchat", "api"],
        "estimated_impact": "30min"
      }
    }
  }'

Python - Incidents

url = f"{BASE_URL}/incidents"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "system",
    "message_id": "msg_inc1",
    "correlation_id": "corr_inc",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "trigger": "Panne serveur principal",
        "severity": "high",
        "details": {
            "affected_services": ["webchat", "api"],
            "estimated_impact": "30min"
        }
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "system",
  "message_id": "msg_inc1",
  "correlation_id": "corr_inc",
  "timestamp": "2025-08-14T10:36:01Z",
  "locale": "fr-MA",
  "data": {
    "ticket_id": "INC-2025-001",
    "status": "assigned",
    "assignee": "ops-team"
  }
}

POST /v1/broadcasts

Diffusion de message système.

cURL - Broadcasts

curl -X POST "$BASE_URL/broadcasts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "admin",
    "message_id": "msg_bcast",
    "correlation_id": "corr_bcast",
    "timestamp": "2025-08-14T10:38:00Z",
    "locale": "fr-MA",
    "data": {
      "message": "Maintenance programmée ce soir 22h-23h",
      "audience": "all_users",
      "schedule": "2025-08-14T20:00:00Z"
    }
  }'

Python - Broadcasts

url = f"{BASE_URL}/broadcasts"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "admin",
    "message_id": "msg_bcast",
    "correlation_id": "corr_bcast",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "message": "Maintenance programmée ce soir 22h-23h",
        "audience": "all_users",
        "schedule": "2025-08-14T20:00:00Z"
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "admin",
  "message_id": "msg_bcast",
  "correlation_id": "corr_bcast",
  "timestamp": "2025-08-14T10:38:01Z",
  "locale": "fr-MA",
  "data": {
    "event": "broadcast_scheduled",
    "recipients": 1250,
    "status": "pending"
  }
}

POST /v1/tickets

Création de ticket support.

cURL - Tickets

curl -X POST "$BASE_URL/tickets" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "webchat",
    "message_id": "msg_tkt",
    "correlation_id": "corr_tkt",
    "timestamp": "2025-08-14T10:40:00Z",
    "locale": "fr-MA",
    "data": {
      "incident": "Problème complexe non résolu",
      "user_context": {
        "id": "user_123",
        "history": ["previous_interactions"]
      },
      "priority": "high"
    }
  }'

Python - Tickets

url = f"{BASE_URL}/tickets"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "webchat",
    "message_id": "msg_tkt",
    "correlation_id": "corr_tkt",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "incident": "Problème complexe non résolu",
        "user_context": {
            "id": "user_123",
            "history": ["previous_interactions"]
        },
        "priority": "high"
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.post(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "webchat",
  "message_id": "msg_tkt",
  "correlation_id": "corr_tkt",
  "timestamp": "2025-08-14T10:40:01Z",
  "locale": "fr-MA",
  "data": {
    "ticket_id": "TKT-2025-456",
    "status": "assigned",
    "eta_hours": 2
  }
}

GET /v1/admin/policies

Lecture des politiques tenant.

cURL - GET Policies

curl -X GET "$BASE_URL/admin/policies" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID"

Python - GET Policies

url = f"{BASE_URL}/admin/policies"
response = requests.get(url, headers=HEADERS)
print(json.dumps(response.json(), indent=2))

JavaScript - GET Policies

const getPolicies = async () => {
  const url = `${BASE_URL}/admin/policies`;
  const response = await fetch(url, {
    method: 'GET',
    headers: headers,
  });
  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "admin",
  "message_id": "msg_pol_get",
  "correlation_id": "corr_pol",
  "timestamp": "2025-08-14T10:42:00Z",
  "locale": "fr-MA",
  "data": {
    "policies": {
      "max_tokens": 1000,
      "allowed_models": ["llama3"],
      "branding": {
        "logo_url": "https://cdn.example.com/logo.png",
        "colors": "#123456"
      }
    }
  }
}

PUT /v1/admin/policies

Mise à jour des politiques tenant.

cURL - PUT Policies

curl -X PUT "$BASE_URL/admin/policies" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "admin",
    "message_id": "msg_pol_put",
    "correlation_id": "corr_pol_upd",
    "timestamp": "2025-08-14T10:44:00Z",
    "locale": "fr-MA",
    "data": {
      "policies": {
        "max_tokens": 1500,
        "allowed_models": ["llama3", "gpt-4"]
      }
    }
  }'

Python - PUT Policies

url = f"{BASE_URL}/admin/policies"
payload = {
    "schema_version": "1.0",
    "tenant": TENANT,
    "channel": "admin",
    "message_id": "msg_pol_put",
    "correlation_id": "corr_pol_upd",
    "timestamp": get_timestamp(),
    "locale": LOCALE,
    "data": {
        "policies": {
            "max_tokens": 1500,
            "allowed_models": ["llama3", "gpt-4"]
        }
    }
}

headers_with_idempotency = HEADERS.copy()
headers_with_idempotency["Idempotency-Key"] = str(uuid.uuid4())

response = requests.put(url, json=payload, headers=headers_with_idempotency)
print(json.dumps(response.json(), indent=2))

JavaScript - PUT Policies

const updatePolicies = async () => {
  const url = `${BASE_URL}/admin/policies`;
  const payload = {
    schema_version: '1.0',
    tenant: TENANT,
    channel: 'admin',
    message_id: 'msg_pol_put',
    correlation_id: 'corr_pol_upd',
    timestamp: getTimestamp(),
    locale: LOCALE,
    data: {
      policies: {
        max_tokens: 1500,
        allowed_models: ['llama3', 'gpt-4'],
      },
    },
  };

  const headersWithIdempotency = {
    ...headers,
    'Idempotency-Key': crypto.randomUUID(),
  };

  const response = await fetch(url, {
    method: 'PUT',
    headers: headersWithIdempotency,
    body: JSON.stringify(payload),
  });

  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "tenant": "acme",
  "channel": "admin",
  "message_id": "msg_pol_put",
  "correlation_id": "corr_pol_upd",
  "timestamp": "2025-08-14T10:44:01Z",
  "locale": "fr-MA",
  "data": {
    "ack": true,
    "version": "v2.1",
    "applied_at": "2025-08-14T10:44:01Z"
  }
}

GET /v1/health

Vérification de santé de l'API.

cURL - Health

curl -X GET "$BASE_URL/health" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json"

JavaScript - Health

const checkHealth = async () => {
  const url = `${BASE_URL}/health`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
  });
  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "data": {
    "status": "ok",
    "time": "2025-08-14T10:46:00Z"
  }
}

GET /v1/ready

Vérification de disponibilité des dépendances.

cURL - Ready

curl -X GET "$BASE_URL/ready" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json"

JavaScript - Ready

const checkReadiness = async () => {
  const url = `${BASE_URL}/ready`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
  });
  return await response.json();
};

Réponse attendue :

{
  "schema_version": "1.0",
  "data": {
    "status": "ready",
    "deps": {
      "database": "ok",
      "redis": "ok",
      "llm_service": "ok"
    }
  }
}

POST /webhooks/facebook

Webhook Facebook Messenger.

cURL - Facebook Webhook

curl -X POST "$BASE_URL/webhooks/facebook" \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=abc123..." \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "facebook",
    "message_id": "msg_fb",
    "correlation_id": "corr_fb",
    "timestamp": "2025-08-14T10:48:00Z",
    "locale": "fr-MA",
    "data": {
      "webhook_data": {
        "object": "page",
        "entry": [{
          "messaging": [{
            "sender": {"id": "123"},
            "message": {"text": "Bonjour"}
          }]
        }]
      }
    }
  }'

Réponse attendue: 202 Accepted

La tolérance ±5 min est vérifiée à partir des horodatages natifs du payload canal (data.webhook_data).

Vérification HMAC (pseudo-code)

import hmac
import hashlib

def verify_webhook_signature(payload, signature, secret_key):
    # Calculer la signature HMAC avec le payload brut
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Comparer les signatures
    return hmac.compare_digest(f"sha256={expected_signature}", signature)

POST /webhooks/whatsapp

Webhook WhatsApp Business.

cURL - WhatsApp Webhook

curl -X POST "$BASE_URL/webhooks/whatsapp" \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=def456..." \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "whatsapp",
    "message_id": "msg_wa",
    "correlation_id": "corr_wa",
    "timestamp": "2025-08-14T10:50:00Z",
    "locale": "fr-MA",
    "data": {
      "webhook_data": {
        "contacts": [{"profile": {"name": "John"}}],
        "messages": [{"text": {"body": "Salut"}}]
      }
    }
  }'

Réponse attendue: 202 Accepted

La tolérance ±5 min est vérifiée à partir des horodatages natifs du payload canal (data.webhook_data).

Erreurs & Rate limiting

Exemple d'erreur 429 (Rate Limited)

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1642694400
Retry-After: 60

{
  "schema_version": "1.0",
  "error": "rate_limited",
  "http": 429,
  "message": "Limite de 1000 requêtes/min dépassée",
  "correlation_id": "corr_abc",
  "retry_after": 60
}

Gestion des erreurs en Python

import time

def make_request_with_retry(url, payload, headers, max_retries=3):
    for attempt in range(max_retries):
        response = requests.post(url, json=payload, headers=headers)

        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            print(f"Rate limited. Waiting {retry_after} seconds...")
            time.sleep(retry_after)
            continue

        if response.status_code < 500:
            return response

        # Retry on 5xx errors
        wait_time = 2 ** attempt
        print(f"Server error. Retrying in {wait_time} seconds...")
        time.sleep(wait_time)

    return response

Idempotency

Démonstration Idempotency-Key

Les requêtes POST avec la même Idempotency-Key retournent le même résultat :

## Première requête (OK)
curl -X POST "$BASE_URL/messages/analyze" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "Idempotency-Key: 00000000-0000-4000-8000-000000000002" \
  -d '{
    "schema_version":"1.0",
    "tenant":"acme",
    "channel":"webchat",
    "message_id":"msg_ide1",
    "correlation_id":"corr_ide1",
    "timestamp":"2025-08-14T10:30:00Z",
    "locale":"fr-MA",
    "data":{"text":"test","lang":"fr"}
  }'
## → HTTP 200 OK

## Requête identique (même body, même Idempotency-Key → réutilisation serveur)
curl -X POST "$BASE_URL/messages/analyze" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "Idempotency-Key: 00000000-0000-4000-8000-000000000002" \
  -d '{
    "schema_version":"1.0",
    "tenant":"acme",
    "channel":"webchat",
    "message_id":"msg_ide1",
    "correlation_id":"corr_ide1",
    "timestamp":"2025-08-14T10:30:00Z",
    "locale":"fr-MA",
    "data":{"text":"test","lang":"fr"}
  }'
## → HTTP 200 OK (réponse idempotente)

## Même Idempotency-Key mais body différent → mismatch
curl -X POST "$BASE_URL/messages/analyze" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "Idempotency-Key: 00000000-0000-4000-8000-000000000002" \
  -d '{
    "schema_version":"1.0",
    "tenant":"acme",
    "channel":"webchat",
    "message_id":"msg_ide1",
    "correlation_id":"corr_ide1",
    "timestamp":"2025-08-14T10:30:00Z",
    "locale":"fr-MA",
    "data":{"text":"different","lang":"fr"}
  }'
## → HTTP 400 Bad Request (idempotency mismatch)

Pagination & filtres

Exemple avec paramètres de pagination

curl -X GET "$BASE_URL/admin/policies?page=2&page_size=50&sort=created_at:desc" \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-SalamBot-Tenant: $TENANT"

Filtres avancés

NOTE: L'endpoint /v1/search/query est POST-only.

curl -X POST "$BASE_URL/search/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-SalamBot-Tenant: $TENANT" \
  -H "X-Request-Id: $REQUEST_ID" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "schema_version": "1.0",
    "tenant": "acme",
    "channel": "webchat",
    "message_id": "msg_456",
    "correlation_id": "corr_def",
    "timestamp": "2025-08-14T10:32:00Z",
    "locale": "fr-MA",
    "data": {
      "query": "remboursement",
      "filters": {
        "category": "support",
        "lang": "fr"
      },
      "limit": 5
    }
  }'

Webhooks

Vérification signature Facebook (pseudo-code)

import hmac
import hashlib

def verify_facebook_signature(payload, signature, secret):
    """
    Vérifie la signature X-Hub-Signature-256 de Facebook
    """
    expected_signature = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected_signature, signature)

## Utilisation
payload = request.get_data(as_text=True)
signature = request.headers.get('X-Hub-Signature-256')
secret = os.getenv('FACEBOOK_APP_SECRET')

if not verify_facebook_signature(payload, signature, secret):
    return {'error': 'Invalid signature'}, 401

Annexes

Snippet complet d'environnement

#!/bin/bash
## setup-env.sh

## Configuration SalamBot API
export BASE_URL="https://api.salambot.local/v1"
export TOKEN="YOUR_TOKEN_HERE"
export TENANT="acme"
export LOCALE="fr-MA"

## Génération d'IDs uniques
export REQUEST_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
export IDEMPOTENCY_KEY=$(uuidgen | tr '[:upper:]' '[:lower:]')
export TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

echo "Environment configured:"
echo "  BASE_URL: $BASE_URL"
echo "  TENANT: $TENANT"
echo "  LOCALE: $LOCALE"
echo "  REQUEST_ID: $REQUEST_ID"
echo "  IDEMPOTENCY_KEY: $IDEMPOTENCY_KEY"
echo "  TIMESTAMP: $TIMESTAMP"

Client Python complet

#!/usr/bin/env python3
## salambot_client.py

import os
import requests
import uuid
import json
from datetime import datetime
from typing import Dict, Any, Optional

class SalamBotClient:
    def __init__(self, base_url: str, token: str, tenant: str = "acme",
                 locale: str = "fr-MA"):
        self.base_url = base_url.rstrip('/')
        self.tenant = tenant
        self.locale = locale
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "X-SalamBot-Tenant": tenant
        }

    def _make_envelope(self, channel: str, data: Dict[str, Any],
                      message_id: Optional[str] = None,
                      correlation_id: Optional[str] = None) -> Dict[str, Any]:
        return {
            "schema_version": "1.0",
            "tenant": self.tenant,
            "channel": channel,
            "message_id": message_id or f"msg_{uuid.uuid4().hex[:8]}",
            "correlation_id": correlation_id or f"corr_{uuid.uuid4().hex[:8]}",
            "timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
            "locale": self.locale,
            "data": data
        }

    def analyze_message(self, text: str, lang: str = "fr") -> Dict[str, Any]:
        url = f"{self.base_url}/messages/analyze"
        payload = self._make_envelope("webchat", {"text": text, "lang": lang})

        headers = self.headers.copy()
        headers["X-Request-Id"] = str(uuid.uuid4())
        headers["Idempotency-Key"] = str(uuid.uuid4())

        response = requests.post(url, json=payload, headers=headers)
        return response.json()

    def search_query(self, query: str, filters: Optional[Dict] = None,
                     limit: int = 5) -> Dict[str, Any]:
        url = f"{self.base_url}/search/query"
        data = {"query": query, "limit": limit}
        if filters:
            data["filters"] = filters

        payload = self._make_envelope("webchat", data)

        headers = self.headers.copy()
        headers["X-Request-Id"] = str(uuid.uuid4())
        headers["Idempotency-Key"] = str(uuid.uuid4())

        response = requests.post(url, json=payload, headers=headers)
        return response.json()

    def generate_reply(self, prompt: str, context: Optional[list] = None,
                       model: str = "llama3") -> Dict[str, Any]:
        url = f"{self.base_url}/generate/reply"
        data = {"prompt": prompt, "model": model}
        if context:
            data["context"] = context

        payload = self._make_envelope("webchat", data)

        headers = self.headers.copy()
        headers["X-Request-Id"] = str(uuid.uuid4())
        headers["Idempotency-Key"] = str(uuid.uuid4())

        response = requests.post(url, json=payload, headers=headers)
        return response.json()

# Utilisation
if __name__ == "__main__":
    client = SalamBotClient(
        base_url="https://api.salambot.local/v1",
        token=os.getenv("SALAMBOT_TOKEN", "YOUR_TOKEN"),
        tenant="acme",
        locale="fr-MA"
    )

    # Test NLU
    result = client.analyze_message("Je veux annuler ma commande")
    print("NLU Result:", json.dumps(result, indent=2))

    # Test RAG
    result = client.search_query("politique de remboursement")
    print("Search Result:", json.dumps(result, indent=2))

    # Test LLM
    result = client.generate_reply("Comment puis-je vous aider ?")
    print("Generated Reply:", json.dumps(result, indent=2))

Client JavaScript/Node.js complet

#!/usr/bin/env node
// salambot-client.js

const crypto = require('crypto');

class SalamBotClient {
  constructor(baseUrl, token, tenant = 'acme', locale = 'fr-MA') {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.tenant = tenant;
    this.locale = locale;
    this.headers = {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-SalamBot-Tenant': tenant,
    };
  }

  _makeEnvelope(channel, data, messageId = null, correlationId = null) {
    return {
      schema_version: '1.0',
      tenant: this.tenant,
      channel: channel,
      message_id: messageId || `msg_${crypto.randomBytes(4).toString('hex')}`,
      correlation_id:
        correlationId || `corr_${crypto.randomBytes(4).toString('hex')}`,
      timestamp: new Date().toISOString(),
      locale: this.locale,
      data: data,
    };
  }

  async analyzeMessage(text, lang = 'fr') {
    const url = `${this.baseUrl}/messages/analyze`;
    const payload = this._makeEnvelope('webchat', { text, lang });

    const headers = {
      ...this.headers,
      'X-Request-Id': crypto.randomUUID(),
      'Idempotency-Key': crypto.randomUUID(),
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(payload),
    });

    return await response.json();
  }

  async searchQuery(query, filters = null, limit = 5) {
    const url = `${this.baseUrl}/search/query`;
    const data = { query, limit };
    if (filters) data.filters = filters;

    const payload = this._makeEnvelope('webchat', data);

    const headers = {
      ...this.headers,
      'X-Request-Id': crypto.randomUUID(),
      'Idempotency-Key': crypto.randomUUID(),
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(payload),
    });

    return await response.json();
  }

  async generateReply(prompt, context = null, model = 'llama3') {
    const url = `${this.baseUrl}/generate/reply`;
    const data = { prompt, model };
    if (context) data.context = context;

    const payload = this._makeEnvelope('webchat', data);

    const headers = {
      ...this.headers,
      'X-Request-Id': crypto.randomUUID(),
      'Idempotency-Key': crypto.randomUUID(),
    };

    const response = await fetch(url, {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(payload),
    });

    return await response.json();
  }
}

// Utilisation
if (require.main === module) {
  const client = new SalamBotClient(
    'https://api.salambot.local/v1',
    process.env.SALAMBOT_TOKEN || 'YOUR_TOKEN',
    'acme',
    'fr-MA',
  );

  (async () => {
    try {
      // Test NLU
      const nluResult = await client.analyzeMessage(
        'Je veux annuler ma commande',
      );
      console.log('NLU Result:', JSON.stringify(nluResult, null, 2));

      // Test RAG
      const searchResult = await client.searchQuery(
        'politique de remboursement',
      );
      console.log('Search Result:', JSON.stringify(searchResult, null, 2));

      // Test LLM
      const replyResult = await client.generateReply(
        'Comment puis-je vous aider ?',
      );
      console.log('Generated Reply:', JSON.stringify(replyResult, null, 2));
    } catch (error) {
      console.error('Error:', error);
    }
  })();
}

module.exports = SalamBotClient;