BGPVR Ръководство за оператора
Ръководство за оператора

Интеграции и API Pro

Нужен е лиценз Pro или по-висок.

Оставете системата си за фактуриране, онлайн магазина или сайта за членство сами да създават, подновяват и спират профили в BGPVR. Тази глава описва точно какво предлага BGPVR за това днес и какво все още не може.

На тази страница
  1. За кого е
  2. Два входа, едни и същи правила
  3. API ключове: създаване, пазене, отнемане
  4. Заявката за създаване (provisioning)
  5. Какво прави всяко събитие
  6. Една и съща заявка два пъти
  7. Отговори и грешки
  8. Примери с curl
  9. Примери на PHP (WHMCS)
  10. Приемане на подписани уебхукове
  11. Форум Discourse Pro
  12. Сигурност
  13. Още не е налично (планирано)
  14. Внимание
Тази глава описва BGPVR 2.9.318: първо обновете

Версия 2.9.318 съдържа поправки по сигурността на интеграциите и затяга правата на служителите, затова обновете до 2.9.318, преди да свържете система за фактуриране. Също поправено във версия 2.9.318:

  • при включени приветствени имейли създаване с имейл адрес се проваляше, след като клиентът вече беше записан, и паролата се губеше;
  • подновяване без package_id премахваше пакета на клиента;
  • спиране на несъществуващо потребителско име отговаряше 200 "disabled" (вече 404);
  • при Pro натискането на „Запази“ в Модули → Регистрация изключваше използването на кодове за покана;
  • двата бутона за проба на уебхука работеха само веднага след Regenerate, а „Disable Test Client“ не спираше тестовия клиент.

За кого е

За оператори, които продават достъп през друга система (WHMCS, HostBill, Blesta, онлайн магазин, платежна платформа, сайт за членство) и не искат да въвеждат всеки клиент на ръка в конзолата. Тази система извиква BGPVR, когато някой плати, поднови, спре да плаща или напусне. Ако искате плащанията да ги приема самият BGPVR, тази глава не ви трябва: използвайте Настройки → Billing (план Business).

Два входа, едни и същи правила

API ключПодписан уебхук
АдресPOST /api/ext/clients/provisionPOST /api/webhooks/provision
Как извикващият доказва кой еЗаглавка X-API-KeyЗаглавка X-Webhook-Signature (HMAC-SHA256 на тялото)
ПланAPI ключове: Pro и нагореУебхукове: Pro и нагоре
Превключвател в конзолатаНяма: работи, щом има ключ„Enable Webhook Auto-Provisioning“ трябва да е включено
Ограничение на скоростта60 заявки в минута на ключ30 заявки в минута на изпращащ адрес
Подходящ заHooks в WHMCS, ваши скриптове и магазинПлатформи, които могат да пращат само подписани уебхукове

И двата приемат една и съща JSON заявка и връщат едни и същи отговори. Използвайте API ключ, освен ако платформата ви може да праща само подписани уебхукове.

Какво може една заявка днес: да създаде клиент; да поднови или активира отново клиент; да върне клиент на предишен пакет; да спре клиент (никога не го изтрива); или вместо профил да върне кодове за покана за еднократна употреба. Нищо друго: все още няма как през API да се изброяват, четат, търсят, редактират или изтриват клиенти (вижте Още не е налично в края).

API ключове: създаване, пазене, отнемане

Само профилът на администратора може да вижда, създава и изтрива API ключове (а по-надолу и да генерира нов таен ключ за уебхукове и да използва бутоните за проба); входовете на служители получават „Permission denied“. Какво още остава само за администратора, е описано в Служебни профили в справочника на конзолата по-долу.

  1. В страничното меню отворете Настройки → раздел API & Security → карта API & Integrations → Backend API Keys → Generate New API Key.
  2. В Key Name го кръстете на системата, която ще го ползва (например WHMCS), и натиснете Save.
  3. Прозорецът API Key Created показва ключа само веднъж. Копирайте го в системата за фактуриране веднага. BGPVR пази само еднопосочен хеш на ключа със сол, така че никой не може да го покаже отново. Загубен ключ не може да се възстанови: изтрийте го и направете нов.
  • Формат: bgpvr_k1_, следван от 48 шестнадесетични знака (общо 57 знака). Списъкът показва само първите 16 знака, името, „Last used“ и значка Active.
  • Какво може ключът: да създава, подновява и спира клиенти и да прави кодове за покана през адреса по-горе. Нищо друго: не може да чете данни за клиенти, да влиза в конзолата или да променя настройки. Всички ключове получават едно и също право; няма редактор на правата за отделен ключ.
  • Колко: Pro 3, Business 10, Enterprise Неограничено. Ключовете, създадени от мобилното приложение за администратори, се виждат в същия списък като „Admin Mobile: <устройство>“ и се броят към лимита; изтриването на такъв ключ отписва съответния телефон (най-късно когато изтече 12-часовата му сесия). Тези ключове не могат да създават клиенти.
  • Ограничение на скоростта: 60 заявки в минута на ключ; 61-вата получава 429. След 10 грешни ключа за минута от един адрес следващите грешни опити получават 429 вместо 401. Зад обратно прокси на същата машина или в локалната мрежа всеки извикващ се брои със собствения си адрес, стига проксито да го подава в X-Forwarded-For. Броячите са в паметта и започват отначало при рестарт на BGPVR. При масов внос една заявка в секунда е безопасна.
  • Отнемане: иконата кошче до ключа → Click again to delete. Ключът се изтрива веднага; следващото извикване с него получава 401 Invalid API key. Изтрит ключ не може да се включи отново.

Проверка на ключ (двата примера с curl по-долу използват тези две променливи):

BGPVR=https://tv.example.com           # your BGPVR address
KEY='paste-your-API-key-here'          # Settings → API & Security → Backend API Keys
curl -s "$BGPVR/api/ext/clients/provision" -H "X-API-Key: $KEY"
{
  "service": "BGPVR External Client Provisioning API",
  "status": "ok"
}

Заявката за създаване (provisioning)

POST https://вашият-сървър/api/ext/clients/provision със заглавките X-API-Key: <ключ> и Content-Type: application/json. Тялото трябва да е един JSON обект. Подписаният уебхук по-долу приема абсолютно същото тяло.

event — текст, задължително
Какво се е случило; вижте Какво прави всяко събитие по-долу. Ако липсва: грешка 400.
user.username — текст, задължително (освен в режим покани)
Потребителското име на клиента и начинът, по който BGPVR намира съществуващ клиент (точно съвпадение, малки и главни букви се различават). Пращайте го като текст: число проваля заявката с 500. Тук BGPVR не проверява знаците, затова ползвайте букви, цифри и _ . -. По-късно не може да се смени през API. Ако липсва: грешка 400.
user.email — текст
Записва се с малки букви. Не бива да принадлежи на друг клиент (409). При подновяване заменя записания адрес само ако е изпратен. Ако липсва: няма.
user.display_name — текст
Име, показвано в конзолата. При подновяване се обновява само ако е изпратено. Ако липсва: няма.
password — текст, на най-горното ниво
Използва се само при създаване; никога не сменя съществуваща парола. Ако липсва, BGPVR прави случайна парола от 12 знака като Kq7m-T3xa-Wn9P или използва потребителското име, когато Настройки → Forum Sync → Auto-Generate Password е „No“. Пращайте я като текст: списък или обект получава грешка 400. Дължината не се проверява, така че пращайте силна парола.
provisioning_mode (или mode, fulfillment) — client или invite
invite връща кодове за покана, вместо да създава профил. Ако липсва: client.
count — число, 1–50
Режим покани: колко кода. Ако липсва: 1.
prefix — текст
Режим покани: начало на всеки код. Само букви и цифри, превръща се в главни, най-много 6. Ако липсва: BGP.
source — текст
Етикет, който се записва в дневника за сигурност на клиента. Ако липсва: external_api или signed_webhook.
plan.package_id — число
Пакетът, в който да бъде клиентът. Трябва да съществува. При създаване непознат номер, null или нещо, което не е цяло число, се пропуска без грешка и клиентът остава без пакет; при подновяване клиентът запазва пакета си. Ако липсва: при създаване Default Provisioning Package, при подновяване текущият пакет на клиента.
plan.duration_days — число, 0–3650
Дата на изтичане = днес + толкова дни (по датата на сървъра). 0 = без дата на изтичане. Ако липсва: Default Expiry (30).
plan.max_connections — число, 1–100
Колко потока едновременно. Ако липсва: стойността на пакета, иначе 1.
plan.allow_timeshift, plan.allow_dvr, plan.allow_vod — 0 или 1
Гледане назад, запис и VOD за този клиент. Ако липсва: стойността на пакета, иначе 1.
plan.stream_profile_id — число
Налага профил на потока. Не се проверява, затова ползвайте съществуващ. Ако липсва: профилът на пакета.
plan.locker_mode — текст
Режим на Security Locker. Ако липсва: режимът на пакета.
plan.expires_in_days — число, 1–3650
Режим покани: колко дни кодовете могат да се използват. Ако липсва: Default Expiry (30) или 1 ден, когато Default Expiry е 0.

Всяко поле от plan може да се прати и на най-горното ниво; обектът plan има предимство. Число извън границите се довежда до най-близката граница; в числовите полета без package_id всичко, което не е число, взема стойността по подразбиране. Стойностите по подразбиране се задават в Настройки → Forum Sync (Default Provisioning Package, Default Expiry, Auto-Generate Password) и важат и за API, и за подписания уебхук, не само за форума. При подновяване липсващо поле запазва текущата стойност на клиента, ако пакетът не се сменя.

Как да намерите номера на пакет

Страницата Пакети все още не показва номерата. Или изберете Default Provisioning Package в Настройки → Forum Sync и не пращайте package_id, или прочетете номера от страницата: десен бутон върху картата на пакета → Inspect (Проверка); id="pkg-card-3" означава пакет 3. plan.package_id и plan.package_name в отговора потвърждават кой пакет е използван.

Какво прави всяко събитие

user_subscribed, user_activated, user_added (трите са еднакви)
Няма клиент с това име: Създава клиента, активен, с плана. 201, "action": "created"; отговорът съдържа паролата.
Клиентът съществува: Подновява или активира отново: статус active, изтичане = днес + duration_days, имейлът и името се обновяват, ако са изпратени, паролата не се променя. package_id на друг съществуващ пакет премества клиента в него и неизпратените лимити идват от този пакет. Иначе клиентът запазва пакета си и всеки неизпратен лимит; само клиент без пакет или в безплатния пакет се премества в Default Provisioning Package (ако е зададен). 200, "reactivated".
user_unsubscribed
Няма клиент с това име: 200, "no_change"
Клиентът съществува: Отменя смяна на пакет: връща клиента в пакета, който е имал, преди предишна заявка да го премести в друг; ако няма такъв, в безплатния пакет (Free Tier), избран в „Регистрация и препоръки“ (страница от план Business). Статусът не се променя: активен клиент остава активен, спрян остава спрян. 200, "downgraded", или "no_change", когато няма къде да бъде преместен.
user_deactivated, user_removed (двете са еднакви)
Няма клиент с това име: 404, {"error": "Client not found", "username": …}; нищо не се променя. (Поправено във версия 2.9.318: по-старите версии отговаряха 200 "disabled".)
Клиентът съществува: Статус disabled: спрян на всички приложения и устройства. Пакетът се връща както при user_unsubscribed. Клиентът никога не се изтрива. 200, "disabled" (или "downgraded", когато и пакетът е върнат). По-късно го активирайте отново с user_activated.
всичко друго
400, Unknown event: …
Подновявания: смятайте дните сами

duration_days се брои от днес, а не от текущата дата на изтичане: клиент, който поднови 10 дни по-рано с "duration_days": 30, получава 30 дни, а не 40. За да запазите оставащите дни, пращайте броя дни до новата платена дата. exp_date 2026-11-05 означава, че достъпът спира в 00:00 ч. сървърно време на 5 ноември. Поправено във версия 2.9.318: подновяване без package_id запазва пакета и лимитите на клиента. По-старите версии премахваха пакета, затова при тях винаги пращайте целия план.

Една и съща заявка два пъти

  • Създаване с вече съществуващо име никога не прави дубликат: това е подновяване (200 reactivated). Затова повторен опит след изтекло време за отговор е безопасен, но в отговора на повторния опит няма парола; имаше я само в първия. Ако е изгубена, сменете паролата от конзолата.
  • Повторно подновяване в същия ден дава същата дата на изтичане: дните не се добавят два пъти.
  • Повторно спиране не променя нищо повече.
  • Режимът покани не е безопасен за повторение: всяко извикване прави нови кодове.

Отговори и грешки

Отговорът при създаване (от примера с curl по-долу; паролата и токенът са скрити):

{
  "access_token": "<скрито>",
  "account_url": "https://tv.example.com/account",
  "action": "created",
  "exp_date": "2026-11-05",
  "password": "<скрито>",
  "plan": {
    "allow_dvr": 1,
    "allow_timeshift": 1,
    "allow_vod": 1,
    "duration_days": 30,
    "expires_in_days": 30,
    "locker_mode": "",
    "max_connections": 2,
    "package_id": 3,
    "package_name": "Premium",
    "stream_profile_id": null
  },
  "username": "john_doe",
  "welcome_email_sent": null
}
  • action: created, reactivated, disabled, downgraded, no_change или invite_created.
  • password: само в отговора created. Запазете я и я покажете на клиента.
  • access_token: собственият токен на клиента за неговия профил (отваря страницата на профила и плейлистата). Пазете го като парола; обикновените интеграции нямат нужда от него.
  • exp_date: "" означава без дата на изтичане. plan: реално приложените стойности, след тези по подразбиране; проверете, че package_id не е null.
  • welcome_email_sent: null, когато приветствените имейли са изключени, "skipped_no_email", true или false. false означава, че имейлът не е изпратен; клиентът е създаден и отговорът пак съдържа паролата.
HTTPТялоПричина
400{"error": "Invalid JSON payload"}Тялото не е валиден JSON.
400{"error": "Missing username in payload"}Липсва user.username (режим client).
400{"error": "Unknown event: user_paid"}event липсва или е изписано грешно.
400{"error": "password must be a string"}password е списък или обект.
401{"error": "API key required (X-API-Key header)"}Липсва заглавка X-API-Key.
401{"error": "Invalid API key"}Грешен или изтрит ключ.
403{"error": "API key lacks permission: clients"}Използван е ключ на устройство от мобилното приложение за администратори.
403{"current_tier": "free", "error": "This feature requires a Pro or Enterprise license.", "feature": "api_keys", "upgrade_url": "https://bgpvr.com/pricing"}Планът не включва API ключове (на адреса за уебхукове: "feature": "webhooks").
403{"current_count": 100, "current_tier": "pro", "error": "Pro tier allows 100 users. Upgrade to increase the limit.", "limit": 100, "upgrade_url": "https://bgpvr.com/pricing"}Създаването би минало лимита на абонати за плана. Брои се всеки клиент, включително спрените. Подновяванията и кодовете за покана продължават да работят.
404{"error": "Client not found", "username": "john_doe"}user_deactivated или user_removed за несъществуващо потребителско име; нищо не е променено.
409{"error": "Email already exists"}Имейлът принадлежи на друг клиент.
429{"error": "Too many requests. Please try again later."}Ограничение на скоростта; изчакайте минута.
500HTML страница за грешкаТялото е JSON, но не е обект (например списък), или username не е текст.

Примери с curl

Всеки пример е изпълнен срещу тестов сървър BGPVR 2.9.318 на 6 октомври 2026 г.; отговорите са истинските, показани форматирани, с подменен адрес на сървъра и скрити тайни стойности. Пакет 3 е пакет с име „Premium“.

Създаване на клиент за 30 дни в пакет 3 (отговорът е в Отговори и грешки по-горе):

curl -s -X POST "$BGPVR/api/ext/clients/provision" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"event": "user_subscribed",
       "user": {"username": "john_doe", "email": "john@example.com", "display_name": "John Doe"},
       "plan": {"package_id": 3, "duration_days": 30, "max_connections": 2}}'

Подновяване или повторно активиране: същото събитие и име. Тук планът е изпратен отново; без package_id клиентът запазва пакета и лимитите си. Изпълнено в същия ден като създаването, датата на изтичане остава същата:

curl -s -X POST "$BGPVR/api/ext/clients/provision" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"event": "user_subscribed",
       "user": {"username": "john_doe"},
       "plan": {"package_id": 3, "duration_days": 30, "max_connections": 2}}'
{
  "action": "reactivated",
  "exp_date": "2026-11-05",
  "plan": {
    "allow_dvr": 1,
    "allow_timeshift": 1,
    "allow_vod": 1,
    "duration_days": 30,
    "expires_in_days": 30,
    "locker_mode": "",
    "max_connections": 2,
    "package_id": 3,
    "package_name": "Premium",
    "stream_profile_id": null
  },
  "username": "john_doe"
}

Спиране (профилът се пази и може да се активира отново с горното извикване):

curl -s -X POST "$BGPVR/api/ext/clients/provision" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"event": "user_deactivated", "user": {"username": "john_doe"}}'
{"action": "disabled", "package_id": null, "username": "john_doe"}

Кодове за покана вместо профил, например за магазин, който продава кодове:

curl -s -X POST "$BGPVR/api/ext/clients/provision" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"event": "user_subscribed", "provisioning_mode": "invite", "count": 2, "prefix": "SHOP",
       "plan": {"package_id": 3, "duration_days": 30, "expires_in_days": 14}}'
{
  "action": "invite_created",
  "codes": [
    "SHOP-G68QT-YNDD9",
    "SHOP-2TEPP-JURMX"
  ],
  "count": 2,
  "expires_at": "2026-10-20 15:18:16",
  "invite_url": "https://tv.example.com/invite",
  "plan": {
    "allow_dvr": 1,
    "allow_timeshift": 1,
    "allow_vod": 1,
    "duration_days": 30,
    "expires_in_days": 14,
    "locker_mode": "",
    "max_connections": 2,
    "package_id": 3,
    "package_name": "Premium",
    "stream_profile_id": null
  }
}

Клиентът използва кода на https://вашият-сървър/invite, като избира потребителско име, парола от поне 8 знака и имейл; профилът получава плана, записан с кода. Код, който не е използван в рамките на expires_in_days, спира да работи (expires_at е в UTC). Всеки код работи веднъж. Използването на кодове е включено по подразбиране. Спира, ако „Invite redemption“ (Регистрация и препоръки) или „Публична самостоятелна регистрация“ (Модули → Регистрация) бъде изключено. И двата превключвателя са от план Business: при Pro страницата „Регистрация и препоръки“ е скрита, а „Публична самостоятелна регистрация“ е показана сива и изключена, но кодовете, създадени през API, пак се използват. Поправено във версия 2.9.318: при Pro натискането на Запази в картата Модули → Регистрация вече не изключва използването на кодове. Ако по-стара версия вече го е изключила по този начин, конзола с Pro не може да го включи отново: свържете се с нас.

Примери на PHP (WHMCS)

Същите извиквания от PHP 7.4 или по-нов с разширението curl. Запазете веднъж този помощен файл:

<?php
// bgpvr_client.php — send provisioning requests to BGPVR (PHP 7.4+ with the curl extension).
const BGPVR_URL     = 'https://tv.example.com';     // your BGPVR address
const BGPVR_API_KEY = 'paste-your-API-key-here';    // Settings → API & Security → Backend API Keys

/** POST one provisioning request. Returns [HTTP status, decoded JSON answer]. */
function bgpvr_provision(array $payload): array
{
    $ch = curl_init(BGPVR_URL . '/api/ext/clients/provision');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($payload),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json', 'X-API-Key: ' . BGPVR_API_KEY],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
    ]);
    $raw    = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $answer = is_string($raw) ? json_decode($raw, true) : null;
    return [$status, is_array($answer) ? $answer : ['error' => "No JSON answer (HTTP $status)"]];
}
<?php
require __DIR__ . '/bgpvr_client.php';

// 1. Create a client (if the username already exists, this renews it instead)
[$status, $r] = bgpvr_provision([
    'event' => 'user_subscribed',
    'user'  => ['username' => 'jane_roe', 'email' => 'jane@example.com', 'display_name' => 'Jane Roe'],
    'plan'  => ['package_id' => 3, 'duration_days' => 30, 'max_connections' => 2],
]);
echo $status, ' ', $r['action'] ?? $r['error'], ' ', $r['exp_date'] ?? '', PHP_EOL;
// Save $r['password'] now: BGPVR returns it only in this answer.

// 2. Renew or reactivate: same event, same username, the whole plan again
[$status, $r] = bgpvr_provision([
    'event' => 'user_subscribed',
    'user'  => ['username' => 'jane_roe'],
    'plan'  => ['package_id' => 3, 'duration_days' => 30, 'max_connections' => 2],
]);
echo $status, ' ', $r['action'] ?? $r['error'], ' ', $r['exp_date'] ?? '', PHP_EOL;

// 3. Switch the client off (the account is kept and can be reactivated later)
[$status, $r] = bgpvr_provision(['event' => 'user_deactivated', 'user' => ['username' => 'jane_roe']]);
echo $status, ' ', $r['action'] ?? $r['error'], PHP_EOL;

// 4. Sell codes instead of accounts: two single-use invite codes
[$status, $r] = bgpvr_provision([
    'event' => 'user_subscribed', 'provisioning_mode' => 'invite', 'count' => 2, 'prefix' => 'SHOP',
    'plan'  => ['package_id' => 3, 'duration_days' => 30, 'expires_in_days' => 14],
]);
echo $status, ' ', isset($r['codes']) ? implode(', ', $r['codes']) : $r['error'], PHP_EOL;

Резултат:

201 created 2026-11-05
200 reactivated 2026-11-05
200 disabled
201 SHOP-TDZ2D-8XLZN, SHOP-EC3EA-ZRQ4P

WHMCS. Все още няма готов модул на BGPVR за WHMCS, но един файл с hooks (функции, които WHMCS извиква при събития) свързва двете системи. С duration_days 0 BGPVR не дава дата на изтичане на профила и решава WHMCS: когато спре (suspend) просрочена услуга, клиентът се спира и в BGPVR, а при възстановяване (unsuspend) се включва отново. Потребителското име в BGPVR е whmcs + номерът на услугата в WHMCS, така че всяка услуга е един клиент в BGPVR.

<?php
// WHMCS: save as includes/hooks/bgpvr.php, and bgpvr_client.php as includes/bgpvr/bgpvr_client.php.
// The hooks follow WHMCS's Create / Suspend / Unsuspend / Terminate actions, so the product
// needs a module (the built-in "Autorelease" module is enough).
if (!defined('WHMCS')) {
    die('This file cannot be accessed directly');
}
require_once dirname(__DIR__) . '/bgpvr/bgpvr_client.php';

// WHMCS product ID => BGPVR plan. duration_days 0 = no expiry date in BGPVR:
// WHMCS suspends an overdue service, and that switches the client off in BGPVR.
const BGPVR_PRODUCTS = [
    12 => ['package_id' => 3, 'duration_days' => 0, 'max_connections' => 2],
];

function bgpvr_whmcs(string $event, array $p): void
{
    $plan = BGPVR_PRODUCTS[(int) $p['pid']] ?? null;
    if ($plan === null) {
        return;                                     // not a BGPVR product
    }
    $payload = ['event' => $event, 'source' => 'whmcs',
    'user'  => ['username' => 'whmcs' . $p['serviceid']], 'plan' => $plan];
    if ($event === 'user_subscribed') {
        $c = $p['clientsdetails'];
        $payload['user']['email'] = $c['email'];
        $payload['user']['display_name'] = trim($c['firstname'] . ' ' . $c['lastname']);
        if (!empty($p['password'])) {
$payload['password'] = $p['password'];  // the service password WHMCS shows the customer
        }
    }
    [$status, $r] = bgpvr_provision($payload);
    if ($status === 409 && isset($payload['user']['email'])) {
        unset($payload['user']['email']);           // email already on another BGPVR client: retry without it
        [$status, $r] = bgpvr_provision($payload);
    }
    logActivity("BGPVR $event whmcs{$p['serviceid']}: HTTP $status " . ($r['action'] ?? $r['error']));
}

add_hook('AfterModuleCreate',    1, function ($vars) { bgpvr_whmcs('user_subscribed',  $vars['params']); });
add_hook('AfterModuleUnsuspend', 1, function ($vars) { bgpvr_whmcs('user_activated',   $vars['params']); });
add_hook('AfterModuleSuspend',   1, function ($vars) { bgpvr_whmcs('user_deactivated', $vars['params']); });
add_hook('AfterModuleTerminate', 1, function ($vars) { bgpvr_whmcs('user_removed',     $vars['params']); });

Какво получи BGPVR, когато се задействаха четирите hooks за услуга 1042 (и за втора услуга на същия клиент, чийто имейл вече беше зает):

logActivity: BGPVR user_subscribed whmcs1042: HTTP 201 created
logActivity: BGPVR user_deactivated whmcs1042: HTTP 200 disabled
logActivity: BGPVR user_activated whmcs1042: HTTP 200 reactivated
logActivity: BGPVR user_removed whmcs1042: HTTP 200 disabled
logActivity: BGPVR user_subscribed whmcs1043: HTTP 201 created
  • Изпробвано срещу BGPVR със заместител на add_hook() на WHMCS, а не в истинска инсталация на WHMCS: пробвайте го на тестов WHMCS, преди да го пуснете.
  • Terminate спира клиента, но не го изтрива, така че той продължава да се брои към лимита на абонати; изтрийте го от Клиенти, когато искате мястото обратно.
  • Смяна на пакета в WHMCS (Change Package) не се обработва от този файл.
  • BGPVR позволява един клиент на имейл адрес, затова при втора услуга на същия клиент има повторен опит без имейл.

Приемане на подписани уебхукове

За системи, които пращат подписани уебхукове, вместо да използват API ключ.

  1. Настройки → API & Security → API & Integrations: отметнете Enable Webhook Auto-Provisioning и натиснете Save в заглавната лента на картата. („Enable forum webhook provisioning“ във Forum Sync е същият превключвател.) Докато е изключено, всяко извикване получава 403 {"error": "Webhooks are disabled"}.
  2. Полето Webhook URL показва адреса: https://вашият-сървър/api/webhooks/provision.
  3. Вземете тайния ключ. След като е записан, полето Webhook Secret показва само ********: BGPVR никога не показва отново записан ключ, а всяка инсталация започва със случаен ключ, който никой не е виждал. Натиснете Regenerate, после още веднъж на Click again to confirm и копирайте новата стойност от полето, преди да напуснете страницата. Старият ключ спира да работи в същия момент.

Как се подписва. Пратете заглавка X-Webhook-Signature: sha256=<подпис>, където подписът е:

  • HMAC-SHA256, записан като 64 шестнадесетични знака с малки букви;
  • с ключ тайната стойност точно както я показва конзолата: нейните 64 знака като текст (UTF-8). Не превръщайте шестнадесетичния ключ първо в байтове;
  • изчислен върху точните байтове на тялото на заявката. Подписвайте низа, който пращате; ако библиотека сглоби JSON наново (интервали, ред на ключовете), подписът вече не съвпада.

Няма времеви печат, така че BGPVR не може да различи повторено изпращане от нова заявка: пращайте уебхукове само по HTTPS. Лимитът е 30 заявки в минута на изпращащ адрес (зад обратно прокси: собственият адрес на изпращача от X-Forwarded-For).

Пример стъпка по стъпка. С примерния ключ 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef и тялото от 102 байта {"event":"user_subscribed","user":{"username":"demo_user"},"plan":{"package_id":3,"duration_days":30}} заглавката е X-Webhook-Signature: sha256=426c18084d33552ed2ebee9ae3a604556e05d5f79077dbb5c6806519aa297522. Проверете своя код с тези стойности. С curl (с BGPVR, зададено както по-горе):

SECRET='0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'   # example only
BODY='{"event":"user_subscribed","user":{"username":"demo_user"},"plan":{"package_id":3,"duration_days":30}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
echo "$SIG"
curl -s -X POST "$BGPVR/api/webhooks/provision" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=$SIG" \
  --data-binary "$BODY"
426c18084d33552ed2ebee9ae3a604556e05d5f79077dbb5c6806519aa297522
{
  "access_token": "<скрито>",
  "account_url": "https://tv.example.com/account",
  "action": "created",
  "exp_date": "2026-11-05",
  "password": "<скрито>",
  "plan": {
    "allow_dvr": 1,
    "allow_timeshift": 1,
    "allow_vod": 1,
    "duration_days": 30,
    "expires_in_days": 30,
    "locker_mode": "",
    "max_connections": 2,
    "package_id": 3,
    "package_name": "Premium",
    "stream_profile_id": null
  },
  "username": "demo_user",
  "welcome_email_sent": null
}

На PHP същото тяло дава същия подпис:

<?php
// Send a signed webhook to BGPVR (for senders that sign requests instead of using an API key).
$bgpvr  = 'https://tv.example.com';
$secret = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';  // example only
$body   = json_encode([
    'event' => 'user_subscribed',
    'user'  => ['username' => 'demo_user'],
    'plan'  => ['package_id' => 3, 'duration_days' => 30],
]);
$signature = 'sha256=' . hash_hmac('sha256', $body, $secret);  // sign the exact bytes you send

$ch = curl_init($bgpvr . '/api/webhooks/provision');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json', 'X-Webhook-Signature: ' . $signature],
    CURLOPT_RETURNTRANSFER => true,
]);
echo $signature, PHP_EOL, curl_exec($ch), PHP_EOL;
sha256=426c18084d33552ed2ebee9ae3a604556e05d5f79077dbb5c6806519aa297522
{"access_token":"<скрито>","account_url":"https://tv.example.com/account","action":"created","exp_date":"2026-11-05","password":"<скрито>","plan":{"allow_dvr":1,"allow_timeshift":1,"allow_vod":1,"duration_days":30,"expires_in_days":30,"locker_mode":"","max_connections":2,"package_id":3,"package_name":"Premium","stream_profile_id":null},"username":"demo_user","welcome_email_sent":null}

Ако след подписването се промени дори един байт от тялото, отговорът е 401 {"error": "Invalid signature"}; същото става, когато ключът е грешен или не е зададен.

Двата бутона за проба

Create Test Client и Disable Test Client в същата карта карат BGPVR да подпише пробна заявка със записания таен ключ и да я провери като истински уебхук, така че работят по всяко време. Те създават, подновяват или спират клиент с име ui_webhook_test; изтрийте го от Клиенти, когато приключите. Ако ваш истински клиент се казва ui_webhook_test, бутоните показват грешка и не го променят. Поправено във версия 2.9.318: при по-старите версии бутоните работеха само веднага след Regenerate, а „Disable Test Client“ не спираше тестовия клиент.

Форум Discourse Pro

Членство в група в Discourse може да дава достъп до BGPVR. В Настройки → Forum Sync: отметнете Enable forum webhook provisioning, копирайте Discourse Webhook URL (https://вашият-сървър/api/webhooks/discourse) в нов уебхук в Discourse със събитията за добавен и премахнат потребител от група. Въведете дълъг случаен таен ключ в Discourse Webhook Secret, натиснете Save и въведете същата стойност в полето Secret на уебхука в Discourse. Полето показва ********, щом има записан ключ (BGPVR не го показва отново); ако Discourse няма собствен ключ, се използва ключът на подписания уебхук. Discourse сам подписва уебхуковете си (X-Discourse-Event-Signature).

  • Задайте в Provisioning Group IDs вашата платена група. Стойността по подразбиране 41 е група във форума на самия BGPVR. Provisioning Group Names (по желание) дава достъп и по име на група, освен по номер. Броят се само имената, които сте въвели там; оставете полето празно, за да важат само номерата. След обновяване до 2.9.318 проверете това поле: обновяването еднократно изчиства имената, които по-старите версии попълваха по подразбиране, така че въведете отново името на вашата група, ако достъпът трябва да идва от името ѝ. Group Package Map (41=forum, vip=Premium) дава на всяка група собствен пакет; иначе се използва Default Provisioning Package.
  • Присъединяването към групата създава или подновява клиента с Default Expiry (0 = без дата на изтичане). Други групи и други събития получават отговор 200 „ignored“.
  • Напускането на групата връща клиента в предишния му пакет или в безплатния пакет. То не спира клиента, макар текстът в картата да казва обратното. Ако няма нито един от двата пакета и Default Expiry е 0, напусналият член запазва достъпа си: оставете срок на изтичане (Default Expiry над 0) или при Business изберете безплатен пакет.
  • Discourse Forum URL, Discourse API Key и Discourse API Username се използват само за да се намерят потребителското име и имейлът на члена, когато уебхукът не ги съдържа. Provisioning Mode може да раздава кодове за покана вместо профили.
  • Отговорът на BGPVR при присъединяване съдържа паролата на новия клиент, а Discourse пази отговорите на уебхуковете в дневника на събитията си, където администраторите на форума могат да ги прочетат.

Сигурност

  • Само HTTPS. Ключът, подписът и паролата на новия клиент минават през тези заявки.
  • BGPVR няма списък с разрешени IP адреси за API ключове или уебхукове. Ако сървърът за фактуриране има постоянен адрес, разрешете само на него достъп до /api/ext/ и /api/webhooks/ в обратното прокси или защитната стена.
  • Давайте на всяка система собствен ключ, кръстен на нея, и дръжте ключовете само на сървъри: никога в уеб страница, приложение или публично хранилище. „Last used“ показва ключове, които вече никой не ползва; изтрийте ги.
  • Смяна на ключ без прекъсване: създайте нов ключ, превключете системата на него, после изтрийте стария. Новият таен ключ за уебхукове действа веднага, така че обновете изпращача незабавно.
  • Пазете паролите, които идват в отговорите, толкова грижливо, колкото всяка клиентска парола.
  • Служителските входове не могат да управляват API ключовете и тайния ключ за уебхукове и виждат тайните настройки маскирани. Въпреки това давайте вход в конзолата, включително профили на служители, само на хора, на които имате доверие.

Още не е налично (планирано)

  • Пълен API за управление на клиенти: изброяване, търсене и четене на клиенти, смяна на пакет, удължаване с N дни, спиране и възстановяване, нова парола, насрочено изтриване, списък на пакетите. Кой е свързан в момента ще дойде след това.
  • API ключове със собствени права и списък с разрешени IP адреси.
  • Изходящи уебхукове: BGPVR да съобщава на системата ви за фактуриране, че клиент е изтекъл, подновил се, създаден е от конзолата или е сменил пакет.
  • Готов модул за WHMCS.
  • Справочник OpenAPI, който разработчиците да заредят в своите инструменти.

Внимание

  • Имената се сравняват точно: John_Doe и john_doe са двама различни клиенти.
  • Потребителските имена са общи за всички източници: конзолата, форума, магазина и всяка друга система. Създаване или подновяване със съществуващо име поема този клиент: включва го отново, дори да сте го спрели от конзолата, и заменя имейла му, така че кодовете за смяна на парола отиват на новия адрес. Дайте на всяка система собствена представка за имената (например shop_) и не оставяйте клиентите сами да избират имена.
  • Датите на изтичане са по датата на сървъра; expires_at на поканите е в UTC.