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.

OrdenPuertaQué haceError
1Bloqueo por fuerza brutaSi una IP intenta 20 veces con una clave o firma incorrecta en 10 minutos, esa IP se bloquea temporalmente429 ip_locked
2Límite del cuerpoNo se lee un cuerpo de solicitud de más de 5 MB413 payload_too_large
3ClaveLa clave solo se acepta en el encabezado; en el sistema solo se conserva su resumen SHA-256401 invalid_token
4Duración y cancelaciónLa clave vencida o revocada se rechaza401 token_expired
5IP permitidaSi la clave tiene una lista de IP, solo pasan las solicitudes provenientes de esas direcciones403 ip_not_allowed
6Estado de la cuentaLas claves de una cuenta cerrada o inactiva no funcionan403 account_inactive
7AlcanceLa clave solo accede a las API permitidas403 scope_denied
8FirmaSi en la clave está activado "firma obligatoria", se verifican la firma HMAC, la marca de tiempo y el nonce de un solo uso401 signature_*
9Límite de velocidadLímite por minuto por clave y en el total de la cuenta429 rate_limited

Inicio rápido

  1. En el panel Cuenta y Soporte > Claves de API abra la página (solo la ve el responsable de la cuenta).
  2. Nueva clave: póngale un nombre, marque solo los permisos necesarios, escriba la IP de salida de su servidor, Solicitud firmada obligatoriaÁbralo.
  3. Copie los dos valores que se muestran una sola vez en pantalla: clave de API (bt_…, en cada solicitud Authorization se envía en el encabezado) y secreto de firma (bts_…, sirve para firmar la solicitud, nunca se envía).
  4. 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.

AlcanceAPI que abre
autocallLlamadas 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
callControl de llamadas y colas: begin_call, bridge, hangup, transfer, mute, queues, queues_pending, queue_user_list, queue_manage_users, agent_statuses
voicebotAsistente de Voz (api/voicebot_api.php) y verificación por voz
voice_otpCódigo de verificación por voz (api/voice_otp.php)
smsAPI de SMS (api/sms.php). Ningún otro alcance puede acceder a SMS
bridgeDatos 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

EncabezadoValor
AuthorizationBearer bt_…
X-Bt-TimestampHora Unix, en segundos (p. ej. 1790802088)
X-Bt-NonceNueva en cada solicitud, de 16-64 caracteres A-Z a-z 0-9 _ - (p. ej. 32 hex)
X-Bt-Signaturev1= + 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íneaContenido
1Versión, fijo v1
2Método HTTP, en mayúsculas (GET, POST)
3Ruta y cadena de consulta, tal como se envió la solicitud (/api/sms.php?action=send). No incluye dominio ni esquema
4X-Bt-Timestamp el valor
5X-Bt-Nonce el valor
6Resumen 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íntomaMotivo
signature_invalid solo en POSTEl 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 turcosUsted 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_expiredLa hora de su servidor está desfasada. NTP (timedatectl set-ntp true) ábralo; tolerancia ±300 s
signature_replayedEl mismo nonce se envió dos veces. Al reintentar, cambie el nonce y la marca de tiempo vuelva a generar y vuelva a firmar
signature_requiredLa clave exige firma pero faltan los encabezados
signature_not_configuredLa 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.

HTTPcodeQué hacer
401missing_tokenAuthorization: Bearer … agregue el encabezado
401invalid_tokenClave incorrecta, revocada o inexistente. No reintentar, corrija el ajuste
401token_expiredRenueve la clave desde el panel
401query_key_disabledEnvíe la clave en el encabezado en lugar de la URL
401signature_*Vea la tabla anterior
403ip_not_allowedAgregue la IP de salida de su servidor a la lista de la clave
403scope_deniedConceda el permiso correspondiente a la clave o use la clave correcta
403account_inactiveLa cuenta está cerrada; hable con soporte
403module_disabledEl servicio correspondiente no está activo en su paquete
413payload_too_largeDivida la solicitud en partes (p. ej. add_leads máximo 5.000 registros)
429rate_limited / tenant_rate_limitedRetry-After espere hasta
429ip_lockedSe 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ímitePredeterminado
Por clave120 solicitudes por minuto (se puede reducir desde los ajustes de la clave)
Suma de todas las claves de la cuenta600 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:

  1. Claves de API > clave correspondiente > Ajustes > Renovar clave. Seleccione "La anterior seguirá activa 24 horas".
  2. Se genera una nueva clave con los mismos ajustes y un nuevo secreto de firma, que se muestra una sola vez en pantalla.
  3. Actualice su integración con los nuevos valores.
  4. 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 APIGenerado en Buluthat bt_… clave (alcance: autocall + voicebot + voice_otp + call)
Ajustes > Llamadas automáticas > Secreto de firmaDe la misma clave bts_… secreto. Si está completo, cada solicitud que el CRM envía a Buluthat se firma
Ajustes de VoIP > Buluthat API TokenPantalla 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.

  1. 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.
  2. ByCRM > Integraciones > Buluthat:
CampoValor
Buluthat API Tokenbt_…
Secreto de firmabts_…
Bridge Tokenel mismo bt_… clave
CRM Bridge URLhttps://api.buluthat.com/api/crm_bridge.php
Autocall URLhttps://api.buluthat.com/api/autocall.php
Buluthat Tenant ID / PBX Server IDNo se utiliza; llegan los datos de la cuenta a la que pertenezca la clave
  1. 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 (.env No 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_VERIFYPEER activado); 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.