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.
| Rang | Contrôle | Ce qu'il fait | Erreur |
|---|---|---|---|
| 1 | Verrou anti-force brute | Si une même IP envoie 20 clés ou signatures erronées en 10 minutes, cette IP est verrouillée temporairement | 429 ip_locked |
| 2 | Limite du corps | un corps de requête supérieur à 5 Mo n'est pas lu | 413 payload_too_large |
| 3 | Clé | La clé n'est acceptée que dans l'en-tête ; seule son empreinte SHA-256 est conservée dans le système | 401 invalid_token |
| 4 | Durée et résiliation | Une clé expirée ou révoquée est refusée | 401 token_expired |
| 5 | IP autorisée | Si une liste d'IP est définie pour la clé, seules les requêtes provenant de ces adresses passent | 403 ip_not_allowed |
| 6 | Statut du compte | Les clés d'un compte fermé ou inactif ne fonctionnent pas | 403 account_inactive |
| 7 | Portée | La clé n'accède qu'aux API autorisées | 403 scope_denied |
| 8 | Signature | Si « signature obligatoire » est activé sur la clé, la signature HMAC, l'horodatage et le nonce à usage unique sont vérifiés | 401 signature_* |
| 9 | Limite de débit | Limite par minute, par clé et sur le total du compte | 429 rate_limited |
Démarrage rapide
- Dans le panneau Compte et Assistance > Clés API ouvrez la page (visible uniquement par le responsable du compte).
- 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.
- Copiez les deux valeurs affichées une seule fois à l'écran : clé API (
bt_…, à chaque requêteAuthorizationdans l'en-tête) et secret de signature (bts_…, utilisée pour signer la requête, n'est jamais envoyé). - 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ée | API qu'elle ouvre |
|---|---|
autocall | Appels 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 |
call | Contrôle d'appels et files : begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses |
voicebot | Assistant vocal (api/voicebot_api.php) et vérification vocale |
voice_otp | Code de vérification vocal (api/voice_otp.php) |
sms | API SMS (api/sms.php). Aucune autre portée n'a accès aux SMS |
bridge | Donné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ête | Valeur |
|---|---|
Authorization | Bearer bt_… |
X-Bt-Timestamp | Temps Unix, en secondes (ex. 1790802088) |
X-Bt-Nonce | Nouveau à chaque requête, 16-64 caractères A-Z a-z 0-9 _ - (ex. 32 hex) |
X-Bt-Signature | v1= + 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
| Ligne | Contenu |
|---|---|
| 1 | Version, fixe v1 |
| 2 | Méthode HTTP, en majuscules (GET, POST) |
| 3 | Chemin 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 |
| 4 | X-Bt-Timestamp la valeur |
| 5 | X-Bt-Nonce la valeur |
| 6 | Empreinte 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ôme | Cause |
|---|---|
signature_invalid uniquement en POST | Le 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 turcs | Vous 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_expired | L'horloge de votre serveur est décalée. NTP (timedatectl set-ntp true) ; tolérance ±300 s |
signature_replayed | Le 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_required | La signature est obligatoire pour la clé mais les en-têtes sont absents |
signature_not_configured | La 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.
| HTTP | code | Que faire |
|---|---|---|
| 401 | missing_token | Authorization: Bearer … ajoutez l'en-tête |
| 401 | invalid_token | La clé est incorrecte, révoquée ou absente. Ne pas réessayer, corrigez le paramètre |
| 401 | token_expired | Renouvelez la clé depuis le panneau |
| 401 | query_key_disabled | Envoyez la clé dans l'en-tête plutôt que dans l'URL |
| 401 | signature_* | Voir le tableau ci-dessus |
| 403 | ip_not_allowed | Ajoutez l'IP de sortie de votre serveur à la liste de la clé |
| 403 | scope_denied | Accordez à la clé l'autorisation voulue ou utilisez la bonne clé |
| 403 | account_inactive | Compte fermé ; contactez le support |
| 403 | module_disabled | Le service concerné n'est pas activé dans votre forfait |
| 413 | payload_too_large | Découpez la requête en parties (ex. add_leads maximum 5.000 enregistrements) |
| 429 | rate_limited / tenant_rate_limited | Retry-After patientez jusqu'à |
| 429 | ip_locked | De 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
| Limite | Par 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 compte | 600 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 :
- Clés API > clé concernée > Paramètres > Renouveler la clé. Choisissez « L'ancienne reste valable 24 heures ».
- 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.
- Mettez à jour votre intégration avec les nouvelles valeurs.
- 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é API | Généré dans Buluthat bt_… la clé (portée : autocall + voicebot + voice_otp + call) |
| Paramètres > Appels automatiques > Secret de signature | De 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.
- 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.
- ByCRM > Intégrations > Buluthat:
| Champ | Valeur |
|---|---|
| Buluthat API Token | bt_… |
| Secret de signature | bts_… |
| Bridge Token | le même bt_… la clé |
| CRM Bridge URL | https://api.buluthat.com/api/crm_bridge.php |
| URL d'appel automatique | https://api.buluthat.com/api/autocall.php |
| Buluthat Tenant ID / PBX Server ID | Non utilisé ; les données renvoyées sont celles du compte auquel la clé appartient |
- 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_VERIFYPEERactivé) ; 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.
