Документация за разработчици
Интеграцията е разделена на две независими части: издаване на JWT в твоя backend и извикване на агента от браузъра. Секретният ключ остава само на сървъра — никога в frontend код.
OpenAPI 3.0 + SDK генерация
Машинно-четима спецификация на всички 16 операции на агента. Генерирайте client SDK за вашия език (C#, TypeScript, Java, Python, …) с openapi-generator или NSwag.
npx @openapitools/openapi-generator-cli generate \
-i https://napi.bg/api/v1/openapi.json \
-g csharp \
-o ./napi-agent-sdkSERVER-SIDE — backend-ът ти издава JWT
Тези endpoint-и се викат от твоя backend (C# / Python / Node /
PHP / Go). Секретният ключ napi_test_… или
napi_live_… остава тук — никога не достига до
браузъра. Вземи си ключ от
Профил → Секретен ключ за НАПИ агент.
POST /api/v1/tokens/issue
Обменете секретен ключ за кратък JWT, обвързан с конкретна
операция и ЕИК. Issuer-ът проверява разрешените ЕИК-ове,
минималната версия на агента и при тестов ключ слага
test: true в JWT-а.
POST /api/v1/tokens/issue HTTP/1.1
Host: napi.bg
Authorization: Bearer napi_test_XXXXXXXXXXXXXXXXX
Content-Type: application/json
{
"typeId": "vat",
"eik": "123456789"
}
→ 200 OK
{
"token": "eyJhbGciOiJSUzI1NiIs...",
"jti": "a3f6b9...",
"expiresAt": "2026-04-27T12:34:56Z",
"issuer": "napi.bg"
}
→ 401 невалиден / revoked / expired секретен ключ
→ 402 изчерпан лимит на ЗЛ за този ключ (по подразбиране 3 ЗЛ
за реален ключ без подписан договор; admin override
чрез ApplicationUser.MaxEiksOverride; неограничен след
signed contract; тестовите ключове не са лимитирани)
→ 400 липсва eik в заявката
Стойностите за typeId съвпадат с операциите в
openapi.json
(напр. vat, dec1_6, debt_payments,
taxpayers). Изброени са и в горния dropdown
„Live example".
Server-side примери
Минимални snippet-и за обмяна на секретен ключ срещу JWT.
Вкарай ги в твоя backend и излагай JWT-а на frontend-а
чрез вътрешен endpoint (напр. POST /napi/token).
SECRET="napi_test_XXXXXXXXXXXXXXXXX"
curl --silent --show-error --fail \
-X POST https://napi.bg/api/v1/tokens/issue \
-H "Authorization: Bearer $SECRET" \
-H "Content-Type: application/json" \
-d '{ "typeId": "vat", "eik": "123456789" }'// Node 18+ (built-in fetch). Хвани го зад твоя endpoint
// и НИКОГА не давай SECRET на frontend-а.
const SECRET = process.env.NAPI_SECRET; // "napi_test_…"
export async function issueNapiJwt(typeId, eik) {
const r = await fetch("https://napi.bg/api/v1/tokens/issue", {
method: "POST",
headers: {
"Authorization": `Bearer ${SECRET}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ typeId, eik })
});
if (!r.ok) throw new Error(`napi.bg ${r.status}: ${await r.text()}`);
return r.json(); // { token, jti, expiresAt, issuer }
}// ASP.NET — регистрирай HttpClient в DI с BaseAddress.
public sealed class NapiTokenClient(HttpClient http, IConfiguration cfg)
{
private readonly string _secret = cfg["Napi:Secret"]!; // "napi_test_…"
public async Task<JsonDocument> IssueAsync(string typeId, string eik)
{
using var req = new HttpRequestMessage(HttpMethod.Post,
"https://napi.bg/api/v1/tokens/issue");
req.Headers.Authorization = new("Bearer", _secret);
req.Content = JsonContent.Create(new { typeId, eik });
using var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
return JsonDocument.Parse(await res.Content.ReadAsStringAsync());
}
}import os, requests
SECRET = os.environ["NAPI_SECRET"] # "napi_test_…"
def issue_napi_jwt(type_id: str, eik: str) -> dict:
r = requests.post(
"https://napi.bg/api/v1/tokens/issue",
headers={
"Authorization": f"Bearer {SECRET}",
"Content-Type": "application/json",
},
json={"typeId": type_id, "eik": eik},
timeout=10,
)
r.raise_for_status()
return r.json() # {"token": "...", "jti": "...", "expiresAt": "...", "issuer": "napi.bg"}<?php
// PHP 8 + curl. $SECRET идва от .env / config — не от потребителя.
$SECRET = getenv('NAPI_SECRET'); // "napi_test_…"
function issue_napi_jwt(string $typeId, string $eik): array {
global $SECRET;
$ch = curl_init('https://napi.bg/api/v1/tokens/issue');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $SECRET",
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['typeId' => $typeId, 'eik' => $eik]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 300) throw new RuntimeException("napi.bg $status: $body");
return json_decode($body, true); // ['token' => …, 'jti' => …, 'expiresAt' => …, 'issuer' => …]
}Live example (автоматично от openapi.json)
Избери операция от dropdown-а. Ако нищо не се зарежда: /api/v1/openapi.json трябва да е достъпен.
HTTP headers
Всички операции минават през POST /api/v1/operations/<typeId>.
Authorization: Bearer <JWT> е задължителен за операции, които
изискват ЕИК; останалите headers са опционални.
| Header | Назначение |
|---|---|
X-Napi-Sender-Id |
Идентификатор на подател (sender) от настройките на агента. Когато липсва и са конфигурирани повече от един подател, агентът показва picker. С единствен подател се ползва автоматично. |
X-Napi-File-Path |
Абсолютен път до файл/папка за операциите, които приемат
local input (напр. dec1_6, vat_submit).
Обикновено идва от резултата на /api/pick-file или
/api/pick-folder. Като алтернатива за browser-driven
flow-и виж _inlineFile / _inlineDir в
секцията „Inline payloads" по-долу.
|
X-Napi-Batch-Id |
Опционален GUID за multi-step бутони.
Първата операция със стойност, която агентът не е виждал
(или TTL-ът от 10 минути е изтекъл), показва
диалог за потвърждение на подателя; всички следващи заявки
със същия GUID в рамките на TTL-а се изпълняват мълчаливо.
Подава се на всяка заявка от batch-а — генерирай
го веднъж в момента на click (crypto.randomUUID())
и преизползвай за цялата последователност. Различен или
липсващ GUID винаги пита наново.
|
Inline payloads (без локален файл)
Browser-side flow-ите често нямат удобен начин да запишат файл на
диска преди да го подадат — нито искат да го правят. Като
алтернатива на X-Napi-File-Path агентът приема
съдържанието на файла директно в JSON body-то под един от двата
ключа. Базата се декодира в RAII-guard-нат temp път и се изтрива
автоматично след handler-а, така че submission кодът не вижда
stale файлове и disk-ът остава ограничен.
_inlineFile — единичен файл
За операции с един входен файл (HFR, Д1/Д6, ETZ 62/123, ОДИТ, Дек7.3).
Записва се в %TEMP%/napi_<random>_<filename>.
{
"eik": "131063188",
"month": 5,
"year": 2026,
"fund": "0",
"_inlineFile": {
"filename": "dec1_6.txt",
"contentBase64": "<base64 на TXT файла>"
}
}
_inlineDir — множество файлове в обща папка
За VAT (изисква deklar.txt + pokypki.txt
+ prodajbi.txt в една папка) и други multi-file
submission-и. Файловете се записват в обща
%TEMP%/napi_<random>/ папка, която се подава
на operation-а вместо single file path.
{
"eik": "131063188",
"month": 5,
"year": 2026,
"_inlineDir": {
"files": [
{ "filename": "deklar.txt", "contentBase64": "..." },
{ "filename": "pokypki.txt", "contentBase64": "..." },
{ "filename": "prodajbi.txt", "contentBase64": "..." }
]
}
}
Правила и precedence
- Когато
X-Napi-File-Pathheader-ът присъства, той има предимство — inline ключовете се игнорират. - В една заявка използвай или
_inlineFile, или_inlineDir. Когато и двете са налични,_inlineFileсе обработва пръв. - Filename-ите се санитизират — path separator-и (
/ \ : * ? " < > |) се махат за защита от path traversal. - Temp файловете/папките се изтриват автоматично при излизане от handler-а — без cleanup отговорност за клиента.
Множествени операции (batch)
Един user-action понякога стартира няколко операции
наведнъж — напр. „покажи всички ДДС файлове за 2026 г. и
свали PDF-ите" (1× vat_list + N× vat_file),
или „подай Дек1/6 + свали входящ номер"
(dec1_6_submit → dec1_6_receipt).
По подразбиране агентът показва диалог за избор на
подател на всяка такава заявка — досадно, когато
всички идват от един и същи click. Batch-ът решава точно
това.
Как работи
- В момента на клика frontend-ът генерира GUID:
const batchId = crypto.randomUUID(); - Същият GUID отива в
X-Napi-Batch-Idheader-а на всяка заявка от поредицата. - На първата заявка агентът показва picker-а и запомня избора срещу този GUID за 10 минути.
- Всички следващи заявки със същия GUID минават мълчаливо с този подател — без диалози.
- Различен GUID или липсващ header → агентът пита наново.
Сценарии
- Справка + сваляне — списък файлове (1 заявка) + сваляне на всеки (N заявки). Едно потвърждение на подателя за целия батч.
- Подаване + квитанция — submit-операция, после извличане на входящ номер / PDF потвърждение.
- Периодичен синк — твоят backend стартира
background задача, която върви през N ЕИК-а с
taxpayers_list; всички споделят един batch ID така че потребителят се пита веднъж.
JS пример (browser-side)
// Един клик стартира 1 списък + N сваляния. Всички споделят
// един и същ batchId, така че агентът пита за подател само
// при първата заявка.
async function downloadAllVatFiles(year) {
const batchId = crypto.randomUUID(); // веднъж на click
// 1) Списък
const list = await callAgent("vat_list",
{ typeId: "vat_list", eik: "123456789" },
{ batchId, params: { year } });
// 2) Парче по парче — същия batchId
for (const f of list.files) {
const pdf = await callAgent("vat_file",
{ typeId: "vat_file", eik: "123456789" },
{ batchId, params: { fileId: f.id } });
saveBlob(pdf);
}
}
async function callAgent(typeId, jwtReq, { batchId, params }) {
// "/napi/token" е ТВОЙ endpoint на твоя backend, който вътрешно
// вика napi.bg/api/v1/tokens/issue със секретния ключ. Това НЕ е
// endpoint на napi.bg — секретният ключ не трябва да напуска твоя
// сървър. Виж секцията „Server-side примери" по-горе.
const { token } = await fetch("/napi/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(jwtReq),
}).then(r => r.json());
const headers = {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
"X-Napi-Batch-Id": batchId, // ← същият GUID за цялата серия
};
const r = await fetch(`http://127.0.0.1:5123/api/v1/operations/${typeId}`, {
method: "POST", headers, body: JSON.stringify(params)
});
if (!r.ok) throw new Error(`agent ${r.status}: ${await r.text()}`);
return r.json();
}
typeId + eik) — JWT-ите НЕ се
преизползват между операции.
Помощни endpoint-и на агента (file / folder picker)
Браузърите по сигурност не дават absolute path през <input type="file">.
Агентът предоставя два помощни POST endpoint-а, които отварят
native OS диалог и връщат пълния път — удобно когато
операцията очаква път до файл/папка (напр. dec1FilePath в
dec1_6). Тези endpoint-и са извън typed
/api/v1/operations/* повърхността и не изискват JWT.
POST http://127.0.0.1:5123/api/pick-file
{ "startDir": "C:/Declarations",
"filter": "TXT файлове (*.txt);;Всички файлове (*)" }
→ 200 { "path": "C:/Declarations/dec1_2026_03.txt" }
POST http://127.0.0.1:5123/api/pick-folder
{ "startDir": "C:/Declarations" }
→ 200 { "path": "C:/Declarations" }
При отказан диалог се връща {"path": ""} с HTTP 200. Диалогът
блокира HTTP handler-а докато потребителят затвори прозореца — заявката
има timeout 600 секунди.
Пример: избор на файл и подаване към dec1_6
Типичен сценарий — потребителят натиска бутон „Подай Дек1/6",
браузърът извиква /api/pick-file за да получи
пълен път, после го подава като X-Napi-File-Path
header към операцията. Същият pattern важи и за
/api/pick-folder при операции, които приемат
директория.
async function submitDec16(jwt) {
// 1) Отвори native OS file dialog
const pickRes = await fetch("http://127.0.0.1:5123/api/pick-file", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
startDir: "C:/Declarations",
filter: "Дек1/6 TXT (*.txt);;Всички файлове (*)"
})
});
const { path } = await pickRes.json();
if (!path) return; // потребителят отказа диалога
// 2) Извикай операцията със selected path в headera
const opRes = await fetch("http://127.0.0.1:5123/api/v1/operations/dec1_6", {
method: "POST",
headers: {
"Authorization": `Bearer ${jwt}`,
"Content-Type": "application/json",
"X-Napi-File-Path": path, // ← пълен път от picker-а
},
body: JSON.stringify({ eik: "123456789", year: 2026, month: 3 })
});
if (!opRes.ok) throw new Error(`agent ${opRes.status}: ${await opRes.text()}`);
return opRes.json(); // submission result
}
Пример: избор на папка (вход за обработка)
async function pickInputFolder() {
const r = await fetch("http://127.0.0.1:5123/api/pick-folder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ startDir: "C:/Declarations" })
});
const { path } = await r.json();
return path || null; // null когато потребителят отказа
}
<input type="file"> (sandbox/sigurnost).
Агентът работи локално и има пълен достъп до файловата
система — затова го молим да отвори native диалог и да ни
върне резултата. Без този обход операциите, които приемат
X-Napi-File-Path, остават неизползваеми от
браузър.