Вставьте полный ключ v3, затем откройте нужный метод ниже. Запрос уйдёт только на этот сайт. Ключ не сохраняется, не добавляется в URL и стирается при закрытии страницы.
Если для ключа задан IP whitelist, разрешите IP текущего браузера. Не вводите ключ на чужом устройстве.
01 / Начало работы
Быстрый старт
1 Получите ключ
Откройте раздел API в Telegram-боте и создайте ключ v3. Полный ключ храните как пароль.
2 Настройте IP
При необходимости укажите исходящие IP ваших серверов в whitelist ключа: до 20 адресов IPv4/IPv6.
Передавайте полный ключ в заголовке Authorization: Bearer bd3_…. Не помещайте его в URL, логи или публичный JavaScript.
Для POST и PUT нужен Content-Type: application/json. Тело — JSON-объект размером до 256 KiB.
IP whitelist
Пустой список разрешает доступ с любого IP. Если список заполнен, другие адреса получают HTTP 403 ip_not_allowed.
Принимаются точные IPv4/IPv6, но не CIDR и не имена хостов.
Важно при покупке: одна логическая покупка — один Idempotency-Key. Повтор с тем же телом вернёт сохранённый ответ, другой payload даст 409 idempotency_conflict. После timeout не создавайте новую покупку с другим ключом: результат у регистратора может быть неизвестен.
Ключи доступа
Отдельные проекты
До 10 ключей
В боте откройте Настройки → API → Добавить проект. У каждого проекта собственный ключ, статус, IP whitelist и адрес webhook. Все проекты используют один аккаунт, его баланс и скидки.
Безопасное управление
Ключ проекта можно отключить, перевыпустить или отозвать независимо от остальных. При перевыпуске сохраняйте прежний Idempotency-Key незавершённого заказа.
Для каждой логической покупки используйте уникальный Idempotency-Key в пределах всего аккаунта, даже если запросы отправляют разные проекты. Это защищает от двойной регистрации при переключении ключей.
Асинхронные результаты
Webhooks
Укажите для проекта публичный HTTPS URL на порту 443 в боте: Настройки → API → Проект → Webhook. После настройки сохраните отдельный секрет подписи. Доставка начнётся только когда администратор включит защищённый исходящий прокси; основной сервер не отправляет уведомления напрямую.
События
domain.purchase.completed, domain.purchase.failed, domain.nameservers.completed, domain.nameservers.failed. Промежуточный статус pending не рассылается. Тестовая кнопка отправляет webhook.test.
Доставка
HTTP POST с JSON. Ответ 2xx подтверждает доставку. Сетевые ошибки, 408, 429 и 5xx повторяются до 10 попыток; возможны дубли и иной порядок событий. Сохраняйте event_id и обрабатывайте его один раз. Повторяется только уведомление, не покупка.
Заголовки: X-BuyDomain-Event-ID, X-BuyDomain-Timestamp (Unix seconds), X-BuyDomain-Signature: sha256=…. Подпись — HMAC-SHA256 от строки timestamp + "." + raw_body, ключ — секрет webhook. Проверяйте подпись по сырым байтам тела и отклоняйте timestamp старше пяти минут.
import hmac, hashlib, time
timestamp = request.headers["X-BuyDomain-Timestamp"]
signature = request.headers["X-BuyDomain-Signature"]
raw = request.get_data() # bytes, не пересобирайте JSON
expected = "sha256=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(timestamp)) > 300 or not hmac.compare_digest(signature, expected):
abort(401)
Интеграция
Готовые PHP и Python SDK
Клиент и пример для каждого языка приведены здесь целиком. Скопируйте оба блока в файлы с указанными именами. После timeout не повторяйте покупку или смену NS автоматически: сначала проверьте задачу и сохраните тот же Idempotency-Key.
PHP 8.2+ · ext-curl
BuyDomainV3Client.php · PHP
<?php
/** PHP 8.2+, ext-curl. No automatic retries for purchases or NS mutations. */
final class BuyDomainV3Client
{
public function __construct(private string $apiKey, private string $baseUrl = 'https://buydomain.bot/api/v3', private int $timeout = 30)
{
if (!preg_match('/^bd3_[A-Za-z0-9_-]{20,}$/', $apiKey)) throw new InvalidArgumentException('Invalid API v3 key');
if ($baseUrl !== 'https://buydomain.bot/api/v3') throw new InvalidArgumentException('Unsupported API host');
if ($timeout < 1 || $timeout > 120) throw new InvalidArgumentException('Invalid timeout');
}
public function me(): array { return $this->request('GET', '/me'); }
/** Balance of this project key, plus billing_source: user or key. */
public function balance(): array { return $this->request('GET', '/wallet/balance'); }
/** Code-only lookup; returns no owner identity or account balance. */
public static function balanceByTopupCode(string $code): array
{
if (!preg_match('/^bd3t_[a-f0-9]{32}$/', $code)) throw new InvalidArgumentException('Invalid top-up code');
$ch = curl_init('https://buydomain.bot/api/v3/wallet/balance');
if (!$ch) throw new RuntimeException('Could not initialize HTTP client');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['topup_code' => $code], JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FOLLOWLOCATION => false]);
$raw = curl_exec($ch);
$status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($raw === false) throw new RuntimeException('API transport error: ' . $error);
$response = json_decode($raw, true);
if (!is_array($response)) throw new RuntimeException('Invalid API response; HTTP ' . $status);
if ($status < 200 || $status >= 300) throw new BuyDomainV3Exception((string)($response['error_code'] ?? 'api_error'), (string)($response['message'] ?? 'API error'), $status, $response);
return $response;
}
public function check(string $domain): array { return $this->request('POST', '/domains/check', ['domain' => $domain]); }
public function checkBulk(array $domains): array { return $this->request('POST', '/domains/check/bulk', ['domains' => $domains]); }
public function purchase(string $domain, string $idempotencyKey, ?float $confirmedPrice = null): array
{
if ($idempotencyKey === '' || strlen($idempotencyKey) > 191) throw new InvalidArgumentException('Idempotency-Key is required');
$body = ['domain' => $domain];
if ($confirmedPrice !== null) $body['confirmed_price'] = $confirmedPrice;
return $this->request('POST', '/domains/purchase', $body, ['Idempotency-Key: ' . $idempotencyKey]);
}
public function task(int $id): array { return $this->request('GET', '/tasks/' . $id); }
public function domains(int $page = 1, int $limit = 50, string $sort = 'desc'): array
{
if ($page < 1 || $limit < 1 || $limit > 200 || !in_array($sort, ['asc', 'desc'], true)) throw new InvalidArgumentException('Invalid pagination');
return $this->request('GET', '/domains?' . http_build_query(compact('page', 'limit', 'sort')));
}
public function domain(string $domain): array { return $this->request('GET', '/domains/' . rawurlencode($domain)); }
public function nameservers(string $domain): array { return $this->request('GET', '/domains/' . rawurlencode($domain) . '/nameservers'); }
public function changeNameservers(string $domain, array $nameservers): array
{
return $this->request('PUT', '/domains/' . rawurlencode($domain) . '/nameservers', ['nameservers' => $nameservers]);
}
private function request(string $method, string $path, ?array $body = null, array $extraHeaders = []): array
{
$headers = array_merge(['Authorization: Bearer ' . $this->apiKey, 'Accept: application/json'], $extraHeaders);
$options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
CURLOPT_CONNECTTIMEOUT => min(10, $this->timeout), CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FOLLOWLOCATION => false];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$options[CURLOPT_HTTPHEADER] = $headers;
$options[CURLOPT_POSTFIELDS] = json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
}
$ch = curl_init($this->baseUrl . $path);
if (!$ch) throw new RuntimeException('Could not initialize HTTP client');
curl_setopt_array($ch, $options);
$raw = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($raw === false) throw new RuntimeException('API transport error: ' . $error . '. Do not change Idempotency-Key before checking the order.');
$response = json_decode($raw, true);
if (!is_array($response)) throw new RuntimeException('Invalid API response; HTTP ' . $code);
if ($code < 200 || $code >= 300) throw new BuyDomainV3Exception((string)($response['error_code'] ?? 'api_error'), (string)($response['message'] ?? 'API error'), $code, $response);
return $response;
}
}
final class BuyDomainV3Exception extends RuntimeException
{
public function __construct(public readonly string $errorCode, string $message, public readonly int $httpStatus, public readonly array $response)
{
parent::__construct($message, $httpStatus);
}
}
example.php · PHP
<?php
require_once __DIR__ . '/BuyDomainV3Client.php';
$client = new BuyDomainV3Client(getenv('BUYDOMAIN_API_KEY') ?: '');
$check = $client->check('example.com');
print_r($check);
// Only after confirming the price and intending to buy, uncomment this:
// $orderId = 'my-order-123'; // Keep this exact ID if your connection times out.
// $purchase = $client->purchase('example.com', $orderId, (float)$check['data']['amount']);
// $task = $client->task((int)$purchase['data']['task_id']);
Python 3.10+ · стандартная библиотека
buydomain_v3.py · Python
"""BuyDomain API v3, Python 3.10+, standard library. Mutations are never retried."""
import json
from urllib import error, parse, request
class ApiError(Exception):
def __init__(self, status, payload):
self.status = status
self.payload = payload
super().__init__(payload.get("message", "API error"))
class BuyDomainV3Client:
BASE = "https://buydomain.bot/api/v3"
def __init__(self, api_key, timeout=30, opener=None):
if not api_key.startswith("bd3_") or len(api_key) < 24:
raise ValueError("Invalid API v3 key")
if not 1 <= timeout <= 120:
raise ValueError("Invalid timeout")
self.api_key = api_key
self.timeout = timeout
self.opener = opener or request.urlopen
def _call(self, method, path, body=None, headers=None):
data = None if body is None else json.dumps(body, separators=(",", ":")).encode("utf-8")
req = request.Request(self.BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + self.api_key)
req.add_header("Accept", "application/json")
if data is not None:
req.add_header("Content-Type", "application/json")
for name, value in (headers or {}).items():
req.add_header(name, value)
try:
with self.opener(req, timeout=self.timeout) as response:
return json.load(response)
except error.HTTPError as exc:
try:
payload = json.load(exc)
except (ValueError, UnicodeDecodeError):
payload = {"message": "Invalid API response"}
raise ApiError(exc.code, payload) from exc
# URLError/timeout are intentionally not retried: the mutation result may be unknown.
def me(self): return self._call("GET", "/me")
def balance(self): return self._call("GET", "/wallet/balance")
@classmethod
def balance_by_topup_code(cls, code, timeout=30, opener=None):
import re
if not re.fullmatch(r"bd3t_[a-f0-9]{32}", code):
raise ValueError("Invalid top-up code")
payload = json.dumps({"topup_code": code}).encode("utf-8")
req = request.Request(cls.BASE + "/wallet/balance", data=payload, method="POST",
headers={"Content-Type": "application/json", "Accept": "application/json"})
try:
with (opener or request.urlopen)(req, timeout=timeout) as response:
return json.load(response)
except error.HTTPError as exc:
try:
response = json.load(exc)
except (ValueError, UnicodeDecodeError):
response = {"message": "Invalid API response"}
raise ApiError(exc.code, response) from exc
def check(self, domain): return self._call("POST", "/domains/check", {"domain": domain})
def check_bulk(self, domains): return self._call("POST", "/domains/check/bulk", {"domains": domains})
def purchase(self, domain, idempotency_key, confirmed_price=None):
if not idempotency_key or len(idempotency_key) > 191:
raise ValueError("Idempotency-Key is required")
body = {"domain": domain}
if confirmed_price is not None:
body["confirmed_price"] = confirmed_price
return self._call("POST", "/domains/purchase", body, {"Idempotency-Key": idempotency_key})
def task(self, task_id): return self._call("GET", "/tasks/" + str(int(task_id)))
def domains(self, page=1, limit=50, sort="desc"):
if page < 1 or not 1 <= limit <= 200 or sort not in ("asc", "desc"):
raise ValueError("Invalid pagination")
return self._call("GET", "/domains?" + parse.urlencode({"page": page, "limit": limit, "sort": sort}))
def domain(self, domain): return self._call("GET", "/domains/" + parse.quote(domain, safe=""))
def nameservers(self, domain): return self._call("GET", "/domains/" + parse.quote(domain, safe="") + "/nameservers")
def change_nameservers(self, domain, nameservers):
return self._call("PUT", "/domains/" + parse.quote(domain, safe="") + "/nameservers", {"nameservers": nameservers})
example.py · Python
import os
from buydomain_v3 import BuyDomainV3Client
client = BuyDomainV3Client(os.environ["BUYDOMAIN_API_KEY"])
check = client.check("example.com")
print(check)
# Only after confirming the price and intending to buy, uncomment this:
# order_id = "my-order-123" # Reuse this ID if your connection times out.
# purchase = client.purchase("example.com", order_id, check["data"]["amount"])
# print(client.task(purchase["data"]["task_id"]))
Финансы проектов
Баланс и списание
Источник списания
Для каждого ключа в боте можно выбрать billing_source=user (баланс владельца, по умолчанию) или billing_source=key (отдельный баланс ключа). Резервного списания со второго баланса при нехватке денег нет. Скидки определяются аккаунтом владельца.
Пополнение
Каждый ключ имеет код bd3t_…. В боте выберите «Пополнить баланс → Баланс API v3 ключа», введите код и оплатите с помощью доступного платёжного метода. Владелец может перевести деньги со своего баланса на ключ. Средства ключа доступны только для покупок и не выводятся. Остаток отозванного ключа переносит администратор на другой ключ того же владельца.
Оба метода GET /wallet/balance по Bearer-ключу и POST /wallet/balance по коду пополнения возвращают balance и billing_source. По коду не раскрываются данные владельца и его общий баланс.
amount включает наценку и текущую скидку владельца ключа. Это прайс зоны, не точная цена конкретного домена: для премиальных доменов итог может отличаться. Себестоимость регистратора не раскрывается.
▶ Попробовать запрос GET /zones/{zone}/renewal-priceLIVE
Справочник методов
Покупка
POST/domains/purchase
Купить домен
Создаёт production-задачу покупки и может списать средства.
HTTP 202
Поле
Где / тип
Описание
domain
body · string
Обязательное полное доменное имя.
confirmed_price
body · number
Необязательная подтверждённая цена, больше нуля.
Idempotency-Key
header · string
Обязательный ключ одной логической покупки, до 191 символа.
Чужой или неактивный домен даёт 404. Наценка и скидка совпадают с расчётом при продлении в боте. exact=false: итог может измениться до фактического продления. Запрос не продлевает домен.
▶ Попробовать запрос GET /domains/{domain}/renewal-priceLIVE
GET/tasks/{task_id}
Статус задачи
Результат покупки или другой задачи владельца ключа.
{
"status": "error",
"error_code": "ip_not_allowed",
"message": "This IP address is not allowed for the API key.",
"details": {},
"request_id": "3f6d7c71cf784148becf86ef"
}
HTTP-коды
400 — неверный JSON или отсутствует Idempotency-Key.
401 / 403 — ключ, IP или статус аккаунта не позволяют доступ.
404 — маршрут или объект не найден.
409 — конфликт или неопределённый результат исходной покупки.
415 / 422 — неверный Content-Type или отклонённая операция.
429 — лимит; смотрите Retry-After.
500 / 503 — ошибка сервиса или временная недоступность.
Каждый ответ содержит request_id и заголовок X-Request-ID. Передавайте этот ID поддержке. Лимиты на ключ, IP и дорогие операции настраиваются администратором; при 429 дождитесь Retry-After.