Sécurité de l'API

L'API Buluthat pilote votre standard téléphonique : elle lance des appels, raccroche, envoie des SMS, traite les numéros de vos clients. Une clé qui fuit ne signifie donc pas « un rapport est visible » mais « des appels sont passés depuis votre compte ». Cette page explique comment protéger votre clé et quelles protections Buluthat applique à votre place.

Adresse de base : https://api.buluthat.com/api/

Les couches en un coup d'œil

Chaque requête passe successivement par ces contrôles. Si l'un la refuse, la requête n'est pas traitée et est inscrite au journal.

RangContrôleCe qu'il faitErreur
1Verrou anti-force bruteSi une même IP envoie 20 clés ou signatures erronées en 10 minutes, cette IP est verrouillée temporairement429 ip_locked
2Limite du corpsun corps de requête supérieur à 5 Mo n'est pas lu413 payload_too_large
3CléLa clé n'est acceptée que dans l'en-tête ; seule son empreinte SHA-256 est conservée dans le système401 invalid_token
4Durée et résiliationUne clé expirée ou révoquée est refusée401 token_expired
5IP autoriséeSi une liste d'IP est définie pour la clé, seules les requêtes provenant de ces adresses passent403 ip_not_allowed
6Statut du compteLes clés d'un compte fermé ou inactif ne fonctionnent pas403 account_inactive
7PortéeLa clé n'accède qu'aux API autorisées403 scope_denied
8SignatureSi « signature obligatoire » est activé sur la clé, la signature HMAC, l'horodatage et le nonce à usage unique sont vérifiés401 signature_*
9Limite de débitLimite par minute, par clé et sur le total du compte429 rate_limited

Démarrage rapide

  1. Dans le panneau Compte et Assistance > Clés API ouvrez la page (visible uniquement par le responsable du compte).
  2. Nouvelle clé: donnez un nom, cochez uniquement les droits nécessaires, indiquez l'IP de sortie de votre serveur, Requête signée obligatoireActivez-le.
  3. Copiez les deux valeurs affichées une seule fois à l'écran : clé API (bt_…, à chaque requête Authorization dans l'en-tête) et secret de signature (bts_…, utilisée pour signer la requête, n'est jamais envoyé).
  4. Testez la connexion :
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
  -H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Si la signature est obligatoire, cette requête 401 signature_required renvoie ; le suivant Signature des requêtes utilisez l'un des exemples de la section.

La clé et le secret de signature ne sont pas stockés sous une forme relisible dans le système n'est pas conservé. Si vous la perdez, nous ne pouvons pas la récupérer ; vous la renouvelez depuis le panneau.

Portées

Chaque clé est générée avec une ou plusieurs portées. Une requête vers une API hors portée 403 scope_denied renvoie.

PortéeAPI qu'elle ouvre
autocallAppels automatiques (api/autocall.php). Pour la compatibilité avec les anciennes intégrations, ouvre aussi les points d'accès de l'assistant vocal, de la vérification vocale et du contrôle d'appels
callContrôle d'appels et files : begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotAssistant vocal (api/voicebot_api.php) et vérification vocale
voice_otpCode de vérification vocal (api/voice_otp.php)
smsAPI SMS (api/sms.php). Aucune autre portée n'a accès aux SMS
bridgeDonnées du standard en direct (api/crm_bridge.php) : appels en direct, statut des agents, enregistrements d'appels, enregistrement audio, liste noire, annonces. Uniquement le compte de la clé ; les messages envoyés tenant_id est ignoré
*Toutes les API. Uniquement si vraiment nécessaire

Principe : une clé distincte par intégration, avec le minimum de droits pour chaque clé. Si la clé SMS de votre site e-commerce fuit, l'attaquant ne peut pas lancer d'appels ; vous ne révoquez que cette clé.

Ne pas envoyer la clé

Clé dans l'en-tête est envoyé :

Authorization: Bearer bt_xxxxxxxx

Authorization pour les environnements qui ne peuvent pas définir l'en-tête X-Api-Key: bt_xxxxxxxx est aussi accepté.

Clé dans l'URL (?key=)

?key=bt_… le format, pour les nouvelles clés est désactivé et 401 query_key_disabled renvoie. Les URL se retrouvent dans les journaux du serveur web, les enregistrements de proxy, l'historique du navigateur et Referer tombe dans l'en-tête ; la clé fuite par là. Uniquement pour un ancien système qui ne peut pas envoyer d'en-tête, dans les paramètres de la clé "Accepter la clé dans l'URL" peuvent être ouvertes. Le panneau signale ces clés en rouge ?key= affiche avec le badge.

Cette autorisation est laissée activée pour les clés générées avant V54, afin de ne pas casser les anciennes intégrations. Désactivez-la une fois votre intégration passée à l'en-tête.

IP autorisée

Dans les paramètres de la clé Adresses IP autorisées dans le champ, une IP ou un bloc CIDR par ligne :

85.111.89.220
10.20.0.0/24
2a01:4f8:c0c:1234::/64

Si la liste est vide, toute IP est acceptée. Si elle est renseignée, une requête provenant d'une adresse hors liste est refusée même si la clé est correcte 403 ip_not_allowed renvoie. L'adresse à saisir est celle qui appelle l'API est l'IP de sortie de votre serveur (et non de votre propre ordinateur). En cas de doute, consultez dans le journal la colonne IP de la requête provenant de ce serveur.

Signature des requêtes

La signature garantit qu'aucune requête ne peut être envoyée même si la clé est volée : l'attaquant a aussi besoin du secret de signature, et ce secret ne transite par le réseau dans aucune requête. La signature permet aussi :

  • Verrouille le corps : si un seul caractère change dans le chemin, la signature ne correspond pas.
  • Empêche la relecture : chaque nonce n'est accepté qu'une seule fois ; une requête interceptée ne peut pas être rejouée.
  • Rejette la requête périmée : si l'horodatage s'écarte de l'heure du serveur de plus de ±5 minutes, la requête est refusée.

Dans la clé Requête signée obligatoire si elle est activée, chaque requête doit être signée. Même si elle est désactivée, si vous envoyez les en-têtes de signature, la signature est quand même vérifiée ; une mauvaise signature ne passe pas inaperçue.

En-têtes

En-têteValeur
AuthorizationBearer bt_…
X-Bt-TimestampTemps Unix, en secondes (ex. 1790802088)
X-Bt-NonceNouveau à chaque requête, 16-64 caractères A-Z a-z 0-9 _ - (ex. 32 hex)
X-Bt-Signaturev1= + signature en hexadécimal minuscule

Texte canonique

Le texte signé, entre eux \n (LF) soit six lignes :

v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
LigneContenu
1Version, fixe v1
2Méthode HTTP, en majuscules (GET, POST)
3Chemin et chaîne de requête, tel qu'envoyé dans la requête (/api/sms.php?action=send). Nom de domaine et schéma non inclus
4X-Bt-Timestamp la valeur
5X-Bt-Nonce la valeur
6Empreinte SHA-256 du corps, en hexadécimal minuscule. Pour une requête sans corps et multipart/form-data Pour les requêtes (envoi de fichiers), empreinte d'un corps vide : e3b0c442…b855

Signature : hex( HMAC-SHA256( anahtar = imza_sırrı, mesaj = kanonik_metin ) )

PHP

function buluthat_request(string $method, string $url, string $token, string $secret, ?array $data = null): array
{
    $body = $data === null ? '' : json_encode($data, JSON_UNESCAPED_UNICODE);
    $p = parse_url($url);
    $uri = ($p['path'] ?? '/') . (isset($p['query']) ? '?' . $p['query'] : '');
    $ts = (string)time();
    $nonce = bin2hex(random_bytes(16));
    $canonical = implode("\n", ['v1', strtoupper($method), $uri, $ts, $nonce, hash('sha256', $body)]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
            'X-Bt-Timestamp: ' . $ts,
            'X-Bt-Nonce: ' . $nonce,
            'X-Bt-Signature: v1=' . hash_hmac('sha256', $canonical, $secret),
        ],
    ]);
    if ($body !== '') {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $body);   // imzalanan gövdenin AYNISI
    }
    $res = json_decode((string)curl_exec($ch), true) ?: [];
    curl_close($ch);
    return $res;
}

$token  = getenv('BULUTHAT_TOKEN');    // bt_...
$secret = getenv('BULUTHAT_SECRET');   // bts_...
print_r(buluthat_request('POST', 'https://api.buluthat.com/api/sms.php?action=send', $token, $secret, [
    'header' => 'FIRMAM', 'message' => 'Siparişiniz kargoya verildi.', 'phones' => ['05321234567'],
]));

Les clients prêts à l'emploi signent aussi : buluthat-autocall-client.php et buluthat-voice-otp-client.php dans le quatrième paramètre ['signing_secret' => 'bts_…'] prend.

Node.js

const crypto = require('crypto');

async function buluthatRequest(method, url, token, secret, data) {
  const body = data === undefined ? '' : JSON.stringify(data);
  const u = new URL(url);
  const ts = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomBytes(16).toString('hex');
  const canonical = ['v1', method.toUpperCase(), u.pathname + u.search, ts, nonce,
    crypto.createHash('sha256').update(body).digest('hex')].join('\n');
  const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');

  const res = await fetch(url, {
    method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'X-Bt-Timestamp': ts,
      'X-Bt-Nonce': nonce,
      'X-Bt-Signature': `v1=${signature}`,
    },
    body: body || undefined,
  });
  return res.json();
}

buluthatRequest('GET', 'https://api.buluthat.com/api/voice_otp.php?action=status&id=42',
  process.env.BULUTHAT_TOKEN, process.env.BULUTHAT_SECRET).then(console.log);

Python

import hashlib, hmac, json, os, secrets, time, urllib.parse
import requests

def buluthat_request(method, url, token, secret, data=None):
    body = b"" if data is None else json.dumps(data, ensure_ascii=False).encode("utf-8")
    u = urllib.parse.urlsplit(url)
    uri = u.path + ("?" + u.query if u.query else "")
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    canonical = "\n".join(["v1", method.upper(), uri, ts, nonce, hashlib.sha256(body).hexdigest()])
    sig = hmac.new(secret.encode(), canonical.encode("utf-8"), hashlib.sha256).hexdigest()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "X-Bt-Timestamp": ts,
        "X-Bt-Nonce": nonce,
        "X-Bt-Signature": f"v1={sig}",
    }
    return requests.request(method, url, data=body or None, headers=headers, timeout=30).json()

print(buluthat_request("POST", "https://api.buluthat.com/api/autocall.php?action=add_leads",
      os.environ["BULUTHAT_TOKEN"], os.environ["BULUTHAT_SECRET"],
      {"campaign_id": 12, "leads": [{"phone": "05321234567", "name": "Ayşe Yılmaz"}]}))

Ligne de commande (bash + openssl)

TOKEN=bt_xxx; SECRET=bts_xxx
URI='/api/autocall.php?action=ping'
TS=$(date +%s); NONCE=$(openssl rand -hex 16)
BODYHASH=$(printf '' | openssl dgst -sha256 -hex | awk '{print $NF}')
SIG=$(printf 'v1\nGET\n%s\n%s\n%s\n%s' "$URI" "$TS" "$NONCE" "$BODYHASH" \
      | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl "https://api.buluthat.com$URI" -H "Authorization: Bearer $TOKEN" \
  -H "X-Bt-Timestamp: $TS" -H "X-Bt-Nonce: $NONCE" -H "X-Bt-Signature: v1=$SIG"

Pourquoi la signature ne correspond-elle pas ?

SymptômeCause
signature_invalid uniquement en POSTLe corps que vous avez signé diffère du corps que vous avez envoyé. Le JSON une seule fois générez, donnez la même variable à la fois au résumé et à la requête
signature_invalid Dans une requête avec caractères turcsVous avez assemblé le chemin vous-même et l'avez encodé différemment. Dans la signature, utilisez le chemin et la requête de l'URL que le client envoie réellement (parse_url / new URL())
signature_expiredL'horloge de votre serveur est décalée. NTP (timedatectl set-ntp true) ; tolérance ±300 s
signature_replayedLe nonce a été envoyé deux fois. En cas de nouvelle tentative (retry), renouvelez le nonce et l'horodatage régénérez et signez à nouveau
signature_requiredLa signature est obligatoire pour la clé mais les en-têtes sont absents
signature_not_configuredLa clé n'a pas de secret de signature ; générez-en un depuis le panneau via « Nouveau secret de signature »

Codes d'erreur

Les erreurs d'identité et de sécurité sont identiques sur tous les points d'accès JSON code renvoie les valeurs. Les points d'accès de contrôle d'appels et de files (compatibles Verimor) renvoient le même code HTTP avec un message en texte brut.

HTTPcodeQue faire
401missing_tokenAuthorization: Bearer … ajoutez l'en-tête
401invalid_tokenLa clé est incorrecte, révoquée ou absente. Ne pas réessayer, corrigez le paramètre
401token_expiredRenouvelez la clé depuis le panneau
401query_key_disabledEnvoyez la clé dans l'en-tête plutôt que dans l'URL
401signature_*Voir le tableau ci-dessus
403ip_not_allowedAjoutez l'IP de sortie de votre serveur à la liste de la clé
403scope_deniedAccordez à la clé l'autorisation voulue ou utilisez la bonne clé
403account_inactiveCompte fermé ; contactez le support
403module_disabledLe service concerné n'est pas activé dans votre forfait
413payload_too_largeDécoupez la requête en parties (ex. add_leads maximum 5.000 enregistrements)
429rate_limited / tenant_rate_limitedRetry-After patientez jusqu'à
429ip_lockedDe nombreuses tentatives erronées proviennent de cette IP ; corrigez la configuration erronée, le verrou se lèvera de lui-même

Exemple de réponse d'erreur :

{ "ok": false, "error": "Bu anahtarın bu API için yetkisi yok.", "code": "scope_denied", "request_id": "df5fca3a599653f6" }

Limites de débit

LimitePar défaut
Par clé120 requêtes par minute (peut être réduit dans les paramètres de la clé)
Total de toutes les clés du compte600 requêtes par minute
Statuts des agents (agent_statuses)ainsi que 2 requêtes par minute par compte

Chaque réponse réussie indique dans les en-têtes les droits restants :

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1790802120

429 à la réception Retry-After (secondes). Erreur réseau et 5xx utilisez un retrait exponentiel pour (1 s, 2 s, 4 s… 5 essais au maximum). 401/403 les erreurs ne pas réessayer: il s'agit d'une erreur de configuration ; les tentatives déclenchent le verrouillage anti-force brute.

Renouvellement des clés

Renouvelez les clés tous les 90-180 jours, au départ d'un collaborateur ou en cas de soupçon de fuite. Passage sans interruption :

  1. Clés API > clé concernée > Paramètres > Renouveler la clé. Choisissez « L'ancienne reste valable 24 heures ».
  2. Une nouvelle clé et un nouveau secret de signature sont générés avec les mêmes paramètres, affichés une seule fois à l'écran.
  3. Mettez à jour votre intégration avec les nouvelles valeurs.
  4. Dans le journal, le préfixe de l'ancienne clé (bt_7820d84…) n'apparaît plus, patientez ; l'ancienne clé se désactive d'elle-même à l'expiration du délai.

En cas de soupçon de fuite Choisissez « Fermer l'ancienne immédiatement » ; les requêtes envoyées avec l'ancienne clé sont refusées aussitôt.

Journal des requêtes

En bas de la page Clés API, Journal des requêtes affiche chaque requête : heure, préfixe de clé, IP, point d'accès et action, statut HTTP, code d'erreur, durée, signée ou non. Le résumé des 24 dernières heures (requêtes, erreurs, limite de débit, erreurs d'identité, requêtes signées, nombre d'IP différentes) est en haut de la page. Les enregistrements sont conservés 90 jours.

Dans chaque réponse X-Request-Id est présent. Indiquez cette valeur dans votre demande d'assistance ; nous retrouvons aussitôt votre requête dans le journal. Ne placez jamais la clé ni le secret de signature dans une demande d'assistance, un e-mail ou une capture d'écran.

Si vous voyez une requête provenant d'une IP inconnue ou à des heures inattendues, renouvelez immédiatement la clé avec « Fermer l'ancienne immédiatement ».

Configuration de Byfix CRM

Byfix CRM se connecte à Buluthat avec deux identités distinctes :

Paramètre (CRM)Valeur
Paramètres > Appels automatiques > Clé APIGénéré dans Buluthat bt_… la clé (portée : autocall + voicebot + voice_otp + call)
Paramètres > Appels automatiques > Secret de signatureDe la même clé bts_… secret. S'il est renseigné, chaque requête que le CRM envoie à Buluthat est signée
Paramètres VoIP > Buluthat API TokenÉcran en direct / pont CDR (crm_bridge) ; fourni par l'équipe Buluthat, distinct de la clé ci-dessus

Ordre recommandé : la clé Requête signée obligatoire générez-la désactivée, saisissez la clé et le secret dans le CRM, dans le journal les requêtes de imzalı voyez qu'il arrive avec le badge, puis rendez la signature obligatoire sur la clé. Ajoutez aussi à la clé l'IP du serveur du CRM.

Clic pour appeler (begin_call) n'envoie plus la clé dans l'URL mais dans l'en-tête. Après la mise à jour de votre CRM, vous pouvez désactiver l'autorisation « Clé dans l'URL » de l'ancienne clé.

Configuration de ByCRM

Dans ByCRM, chaque entreprise vers Buluthat avec sa propre clé est rattaché ; les entreprises ne peuvent pas voir les données les unes des autres.

  1. Avec le compte de l'entreprise dans le panneau Buluthat Clés API > Nouvelle clé: droits Données du standard en direct, Contrôle d'appels et files, Appels automatiques (si utilisé Assistant vocal, Code de vérification vocal). Indiquez l'adresse IP du serveur ByCRM.
  2. ByCRM > Intégrations > Buluthat:
ChampValeur
Buluthat API Tokenbt_…
Secret de signaturebts_…
Bridge Tokenle même bt_… la clé
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
URL d'appel automatiquehttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDNon utilisé ; les données renvoyées sont celles du compte auquel la clé appartient
  1. Essayez l'écran en direct et le clic pour appeler, dans le journal imzalı voyez les badges, puis sur la clé Requête signée obligatoireActivez-le.
Comme l'écran en direct interroge le pont toutes les quelques secondes, les requêtes du pont ont une limite de débit distincte et plus large (1.200 par minute et par clé) ; seules les requêtes de pont en erreur sont inscrites au journal.

Vérification des webhooks

Les webhooks que Buluthat vous envoie sont également signés (X-Buluthat-Signature: sha256=…). Sur votre serveur, la signature corps brut ne traitez aucun webhook sans vérification via X-Buluthat-Delivery ne traitez pas deux fois la même livraison avec. Détail : Webhooks.

Liste de contrôle de sécurité

  • [ ] Chaque intégration a sa propre clé, avec uniquement les portées nécessaires
  • [ ] Les clés ne sont pas dans le code mais dans une variable d'environnement ou un gestionnaire de secrets (.env (n'entre pas dans Git)
  • [ ] La clé n'est que côté serveur ; pas dans le JavaScript du navigateur, ni dans l'application mobile, ni dans une macro Excel
  • [ ] La liste d'IP autorisées est renseignée
  • [ ] Signature de requête obligatoire activée
  • [ ] Clé dans l'URL (?key=) désactivé
  • [ ] Une date d'expiration existe ou un rappel de renouvellement est programmé au calendrier
  • [ ] Le client vérifie le certificat TLS (CURLOPT_SSL_VERIFYPEER activé) ; si la vérification est désactivée, quelqu'un qui s'interpose peut lire et modifier la requête
  • [ ] L'heure du serveur est synchronisée par NTP
  • [ ] La signature du webhook est vérifiée
  • [ ] Le journal est revu une fois par mois ; les clés auxquelles accédait un collaborateur parti ont été renouvelées

Signalement de faille de sécurité

Si vous pensez avoir trouvé une faille de sécurité dans l'API Buluthat, utilisez le Centre d'assistance dans le panneau « Notification de sécurité » ouvrez un dossier sur le sujet. Indiquez comment vous avez reproduit la faille et, le cas échéant, X-Request-Id ajoutez les valeurs. Nous vous demandons de ne pas partager les détails tant que nous n'avons pas examiné et corrigé le signalement.