Seguridad de la API
La API de Buluthat gestiona su central telefónica: inicia llamadas, cuelga, envía SMS y procesa los números de sus clientes. Por eso, la filtración de la clave no significa "se ve un informe", sino "se hacen llamadas desde su cuenta". Esta página explica cómo proteger su clave y qué protecciones aplica Buluthat en su nombre.
Dirección base: https://api.buluthat.com/api/
Las capas de un vistazo
Cada solicitud pasa por estas puertas en orden. Si una la rechaza, la solicitud no se procesa y se registra.
| Orden | Puerta | Qué hace | Error |
|---|---|---|---|
| 1 | Bloqueo por fuerza bruta | Si una IP intenta 20 veces con una clave o firma incorrecta en 10 minutos, esa IP se bloquea temporalmente | 429 ip_locked |
| 2 | Límite del cuerpo | No se lee un cuerpo de solicitud de más de 5 MB | 413 payload_too_large |
| 3 | Clave | La clave solo se acepta en el encabezado; en el sistema solo se conserva su resumen SHA-256 | 401 invalid_token |
| 4 | Duración y cancelación | La clave vencida o revocada se rechaza | 401 token_expired |
| 5 | IP permitida | Si la clave tiene una lista de IP, solo pasan las solicitudes provenientes de esas direcciones | 403 ip_not_allowed |
| 6 | Estado de la cuenta | Las claves de una cuenta cerrada o inactiva no funcionan | 403 account_inactive |
| 7 | Alcance | La clave solo accede a las API permitidas | 403 scope_denied |
| 8 | Firma | Si en la clave está activado "firma obligatoria", se verifican la firma HMAC, la marca de tiempo y el nonce de un solo uso | 401 signature_* |
| 9 | Límite de velocidad | Límite por minuto por clave y en el total de la cuenta | 429 rate_limited |
Inicio rápido
- En el panel Cuenta y Soporte > Claves de API abra la página (solo la ve el responsable de la cuenta).
- Nueva clave: póngale un nombre, marque solo los permisos necesarios, escriba la IP de salida de su servidor, Solicitud firmada obligatoriaÁbralo.
- Copie los dos valores que se muestran una sola vez en pantalla: clave de API (
bt_…, en cada solicitudAuthorizationse envía en el encabezado) y secreto de firma (bts_…, sirve para firmar la solicitud, nunca se envía). - Pruebe el enlace:
curl "https://api.buluthat.com/api/autocall.php?action=ping" \
-H "Authorization: Bearer bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Si la firma es obligatoria, esta solicitud 401 signature_required devuelve; la siguiente Firma de solicitudes utilice uno de los ejemplos de la sección.
La clave y el secreto de firma no se pueden leer de vuelta en el sistema no se guarda. Si la pierde, no podremos recuperarla; renueve la clave desde el panel.
Alcances
Cada clave se genera con uno o varios alcances. Una solicitud a una API fuera del alcance 403 scope_denied devuelve.
| Alcance | API que abre |
|---|---|
autocall | Llamadas automáticas (api/autocall.php). Por compatibilidad con integraciones antiguas, también habilita los endpoints de asistente de voz, verificación por voz y control de llamadas |
call | Control de llamadas y colas: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses |
voicebot | Asistente de Voz (api/voicebot_api.php) y verificación por voz |
voice_otp | Código de verificación por voz (api/voice_otp.php) |
sms | API de SMS (api/sms.php). Ningún otro alcance puede acceder a SMS |
bridge | Datos de la central en vivo (api/crm_bridge.php): llamadas en vivo, estado de agentes, registros de llamadas, grabaciones, lista negra, locuciones. Solo la cuenta de la clave; enviados tenant_id se ignora |
* | Todas las API. Solo si realmente es necesario |
Principio: una clave por integración, el mínimo de permisos por clave. Si se filtra la clave de SMS de su sitio de comercio electrónico, el atacante no puede iniciar llamadas; usted solo revoca esa clave.
No enviar la clave
Clave en el encabezado se envía:
Authorization: Bearer bt_xxxxxxxx
Authorization para entornos que no pueden establecer el encabezado X-Api-Key: bt_xxxxxxxx también se acepta.
Clave en la URL (?key=)
?key=bt_… el formato en las claves nuevas está desactivado y 401 query_key_disabled devuelve. Las URL quedan en los registros del servidor web, en los registros del proxy, en el historial del navegador y Referer cae en el encabezado; por ahí se filtra la clave. Solo para un sistema antiguo que no puede enviar encabezados, en los ajustes de la clave "Aceptar la clave en la URL" se puede abrir. El panel muestra estas claves en rojo ?key= lo muestra con la insignia.
En las claves generadas antes de V54 este permiso se dejó abierto para que no se corten las integraciones antiguas. Ciérrelo cuando haya trasladado su integración al encabezado.
IP permitida
En los ajustes de la clave Direcciones IP permitidas en el campo se escribe una IP o bloque CIDR por línea:
85.111.89.220
10.20.0.0/24
2a01:4f8:c0c:1234::/64
Si la lista está vacía, se acepta cualquier IP. Si tiene contenido, la solicitud de una dirección fuera de la lista, aunque la clave sea correcta, 403 ip_not_allowed devuelve. La dirección que se escribirá es la que llama a la API es la IP de salida de su servidor (no el de su propio equipo). Si no está seguro, consulte en el registro la columna IP de la solicitud proveniente de ese servidor.
Firma de solicitudes
La firma garantiza que, aunque la clave caiga en manos ajenas, no se puedan hacer solicitudes: el atacante necesita además el secreto de firma, que no viaja por la red en ninguna solicitud. La firma también:
- Bloquea el cuerpo: si cambia un solo carácter en la ruta, la firma no coincide.
- Impide la repetición: cada nonce se acepta solo una vez; una solicitud interceptada no se puede enviar por segunda vez.
- Rechaza la solicitud obsoleta: si la marca de tiempo se desvía más de ±5 minutos de la hora del servidor, la solicitud se rechaza
En la clave Solicitud firmada obligatoria si está activo, toda solicitud debe ir firmada. Incluso si está desactivado, si envía los encabezados de firma la firma se sigue verificando; una firma incorrecta no pasa en silencio.
Encabezados
| Encabezado | Valor |
|---|---|
Authorization | Bearer bt_… |
X-Bt-Timestamp | Hora Unix, en segundos (p. ej. 1790802088) |
X-Bt-Nonce | Nueva en cada solicitud, de 16-64 caracteres A-Z a-z 0-9 _ - (p. ej. 32 hex) |
X-Bt-Signature | v1= + la firma en hexadecimal minúscula |
Texto canónico
El texto firmado, entre ellos \n son seis líneas separadas por (LF):
v1
POST
/api/sms.php?action=send
1790802088
9f86d081884c7d659a2feaa0c55ad015
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
| Línea | Contenido |
|---|---|
| 1 | Versión, fijo v1 |
| 2 | Método HTTP, en mayúsculas (GET, POST) |
| 3 | Ruta y cadena de consulta, tal como se envió la solicitud (/api/sms.php?action=send). No incluye dominio ni esquema |
| 4 | X-Bt-Timestamp el valor |
| 5 | X-Bt-Nonce el valor |
| 6 | Resumen SHA-256 del cuerpo, en hexadecimal minúscula. En una solicitud sin cuerpo y multipart/form-data Resumen del cuerpo vacío en las solicitudes de (carga de archivos): e3b0c442…b855 |
Firma: 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'],
]));
Los clientes listos también firman: buluthat-autocall-client.php y buluthat-voice-otp-client.php en el cuarto parámetro ['signing_secret' => 'bts_…'] toma.
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"}]}))
Línea de comandos (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"
¿Por qué no coincide la firma?
| Síntoma | Motivo |
|---|---|
signature_invalid solo en POST | El cuerpo que firmó y el que envió son distintos. El JSON una vez genérelo, entregue la misma variable al resumen y a la solicitud |
signature_invalid En una consulta con caracteres turcos | Usted armó la ruta por su cuenta y la codificó de otra forma. En la firma use la ruta y la consulta de la URL que el cliente realmente envió (parse_url / new URL()) |
signature_expired | La hora de su servidor está desfasada. NTP (timedatectl set-ntp true) ábralo; tolerancia ±300 s |
signature_replayed | El mismo nonce se envió dos veces. Al reintentar, cambie el nonce y la marca de tiempo vuelva a generar y vuelva a firmar |
signature_required | La clave exige firma pero faltan los encabezados |
signature_not_configured | La clave no tiene secreto de firma; genere un "Nuevo secreto de firma" en el panel |
Códigos de error
Los errores de identidad y seguridad son iguales en todos los endpoints JSON code vuelve con los valores. Los endpoints de control de llamadas y de colas (compatibles con Verimor) devuelven el mismo código HTTP con un mensaje en texto plano.
| HTTP | code | Qué hacer |
|---|---|---|
| 401 | missing_token | Authorization: Bearer … agregue el encabezado |
| 401 | invalid_token | Clave incorrecta, revocada o inexistente. No reintentar, corrija el ajuste |
| 401 | token_expired | Renueve la clave desde el panel |
| 401 | query_key_disabled | Envíe la clave en el encabezado en lugar de la URL |
| 401 | signature_* | Vea la tabla anterior |
| 403 | ip_not_allowed | Agregue la IP de salida de su servidor a la lista de la clave |
| 403 | scope_denied | Conceda el permiso correspondiente a la clave o use la clave correcta |
| 403 | account_inactive | La cuenta está cerrada; hable con soporte |
| 403 | module_disabled | El servicio correspondiente no está activo en su paquete |
| 413 | payload_too_large | Divida la solicitud en partes (p. ej. add_leads máximo 5.000 registros) |
| 429 | rate_limited / tenant_rate_limited | Retry-After espere hasta |
| 429 | ip_locked | Se recibieron numerosos intentos fallidos desde esta IP; corrija la configuración incorrecta y el bloqueo se levantará solo |
Ejemplo de respuesta de error:
{ "ok": false, "error": "Bu anahtarın bu API için yetkisi yok.", "code": "scope_denied", "request_id": "df5fca3a599653f6" }
Límites de velocidad
| Límite | Predeterminado |
|---|---|
| Por clave | 120 solicitudes por minuto (se puede reducir desde los ajustes de la clave) |
| Suma de todas las claves de la cuenta | 600 solicitudes por minuto |
Estados de agentes (agent_statuses) | además, 2 solicitudes por minuto por cuenta |
En cada respuesta correcta, el límite restante llega en los encabezados:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1790802120
429 al recibir Retry-After (segundos). Error de red y 5xx use retroceso exponencial para (1 s, 2 s, 4 s… máximo 5 intentos). 401/403 los errores no reintentar: es un error de configuración; los intentos activan el bloqueo por fuerza bruta.
Renovación de claves
Renueve las claves cada 90-180 días, cuando se vaya un empleado o ante sospecha de filtración. Transición sin interrupciones:
- Claves de API > clave correspondiente > Ajustes > Renovar clave. Seleccione "La anterior seguirá activa 24 horas".
- Se genera una nueva clave con los mismos ajustes y un nuevo secreto de firma, que se muestra una sola vez en pantalla.
- Actualice su integración con los nuevos valores.
- En el registro, el prefijo de la clave anterior (
bt_7820d84…) ya no aparece, espere; al vencer el plazo la clave anterior se cierra sola.
Ante sospecha de filtración Seleccione "Cerrar la anterior de inmediato"; las solicitudes con la clave anterior se rechazan al instante.
Registro de solicitudes
Al final de la página Claves de API, Registro de solicitudes muestra cada solicitud: hora, prefijo de la clave, IP, endpoint y acción, estado HTTP, código de error, duración, si estaba firmada. Arriba de la página está el resumen de las últimas 24 horas (solicitudes, errores, límite de velocidad, error de identidad, solicitudes firmadas, número de IP distintas). Los registros se conservan 90 días.
En cada respuesta X-Request-Id tiene el encabezado. Escriba este valor en la solicitud de soporte; encontramos su solicitud en el registro de inmediato. Nunca ponga la clave ni el secreto de firma en una solicitud de soporte, un correo electrónico o una captura de pantalla.
Si ve una IP que no reconoce o solicitudes en horas que no espera, renueve de inmediato la clave con "Cerrar la anterior de inmediato".
Configuración de Byfix CRM
Byfix CRM se conecta a Buluthat con dos identidades distintas:
| Ajuste (CRM) | Valor |
|---|---|
| Ajustes > Llamadas automáticas > Clave de API | Generado en Buluthat bt_… clave (alcance: autocall + voicebot + voice_otp + call) |
| Ajustes > Llamadas automáticas > Secreto de firma | De la misma clave bts_… secreto. Si está completo, cada solicitud que el CRM envía a Buluthat se firma |
| Ajustes de VoIP > Buluthat API Token | Pantalla en vivo / puente CDR (crm_bridge); lo proporciona el equipo de Buluthat y es distinta de la clave anterior |
Orden recomendado: la clave Solicitud firmada obligatoria genérela con la verificación desactivada, introduzca la clave y el secreto en el CRM y, en el registro, las solicitudes de imzalı con la insignia; luego haga obligatoria la firma en la clave. Agregue también a la clave la IP del servidor del CRM.
Clic para llamar (begin_call) ya no envía la clave en la URL sino en el encabezado. Tras actualizar el CRM puede desactivar el permiso "clave en la URL" de la clave anterior.
Configuración de ByCRM
En ByCRM, cada empresa a Buluthat con su propia clave se conecta; las empresas no pueden ver los datos de las demás.
- En el panel de Buluthat, con la cuenta de la empresa Claves de API > Nueva clave: permisos Datos en vivo de la central, Control de llamadas y colas, Llamadas automáticas (si se utiliza Asistente de Voz, Código de verificación por voz). Escriba la IP del servidor de ByCRM.
- ByCRM > Integraciones > Buluthat:
| Campo | Valor |
|---|---|
| Buluthat API Token | bt_… |
| Secreto de firma | bts_… |
| Bridge Token | el mismo bt_… clave |
| CRM Bridge URL | https://api.buluthat.com/api/crm_bridge.php |
| Autocall URL | https://api.buluthat.com/api/autocall.php |
| Buluthat Tenant ID / PBX Server ID | No se utiliza; llegan los datos de la cuenta a la que pertenezca la clave |
- Pruebe la pantalla en vivo y el clic para llamar; en el registro
imzalıvea las insignias y luego, en la clave Solicitud firmada obligatoriaÁbralo.
Como la pantalla en vivo consulta el puente cada pocos segundos, las solicitudes del puente tienen un límite de velocidad aparte y más amplio (1.200 por minuto por clave); en el registro solo se escriben las solicitudes de puente con error.
Verificación de los webhooks
Los webhooks que Buluthat le envía también están firmados (X-Buluthat-Signature: sha256=…). En su servidor, la firma cuerpo sin procesar no procese ningún webhook sin verificarlo mediante X-Buluthat-Delivery no procese dos veces la misma entrega. Detalle: Webhooks.
Lista de verificación de seguridad
- [ ] Cada integración tiene su propia clave, solo con los alcances necesarios
- [ ] Las claves no están en el código sino en variables de entorno o en un gestor de secretos (
.envNo se incluye en Git) - [ ] La clave está solo en el servidor; no en JavaScript del navegador, ni en la aplicación móvil, ni en una macro de Excel
- [ ] La lista de IP permitidas está completa
- [ ] Solicitud firmada obligatoria activada
- [ ] Clave en la URL (
?key=) desactivado - [ ] Tiene fecha de vencimiento o se configuró un recordatorio de renovación en el calendario
- [ ] El cliente verifica el certificado TLS (
CURLOPT_SSL_VERIFYPEERactivado); con la verificación desactivada, alguien interpuesto puede leer y modificar la solicitud - [ ] La hora del servidor está sincronizada con NTP
- [ ] Se verifica la firma del webhook
- [ ] El registro se revisa una vez al mes; se renovaron las claves a las que accedía el personal que se fue
Notificación de vulnerabilidades
Si cree haber encontrado una vulnerabilidad en la API de Buluthat, desde el Centro de Soporte dentro del panel "Aviso de seguridad" abra un registro con el asunto. Le pedimos que explique cómo reprodujo la vulnerabilidad y, si lo hay, X-Request-Id agregue los valores. Le pedimos que no comparta los detalles hasta que revisemos y corrijamos la notificación.
