Autonify Public API v1
English: API docs
За разработчика, който свързва нещо с Autonify. Базов адрес
https://app.autonify.bg/api/v1, само server-to-server.
Authorization: Bearer atn_live_01k…_9f3…
Content-Type: application/json
Idempotency-Key: <ваш собствен уникален низ> # задължителен на всеки POST и PATCH
CORS не е конфигуриран и няма да бъде. Заглавка Origin по тези адреси
означава, че ключът стои в браузър — това е изтекъл достъп, не липсваща
функция. Записва се, за да може да бъде забелязано.
Има и втора посока: събития, които изпращаме към вас, за да не се налага интеграцията да пита непрекъснато за четирите неща, които си струват. Това е §9.
1. Ключове
Създавате го от Настройки → Developer API, за фирмата, която е отворена. Ключът се показва веднъж; ако го загубите, ротирайте го, вместо да пишете на поддръжката.
Достъпът до API е една от възможностите, които клиент на Growth или Enterprise
избира — не е нещо, което всеки план носи. Ако акаунтът не я е избрал, ключове
пак се създават и whoami пак отговаря, но всяко друго повикване връща
403 api_not_in_plan, докато не я включат. Sandbox ключовете работят
независимо от това (§6), така че може да разработвате отсега.
atn_live_<key id>_<secret> # книгите на реална фирма
atn_test_<key id>_<secret> # sandbox фирмата
Един ключ носи една фирма през целия си живот. В това API няма параметър за фирма — нито в пътя, нито в заявката, нито в тялото — така че заявка за друго ЕИК не може да бъде изразена. Това не е проверка, която правим; това е заявка, която не може да бъде съставена.
Две следствия, които си струва да знаете, преди да проектирате около тях:
- Едно ЕИК на ключ. Интеграция за осем фирми означава осем ключа, и конфигурацията ви трябва да има къде да държи осем идентификатора.
- Достъпът се преоценява при всяка заявка спрямо същите правила за делегиране и разпределение на служители, които важат и в браузъра. Ако клиентът оттегли делегирането на своя счетоводител, ключът спира при следващото повикване.
Ротацията създава нов ключ и оставя стария да работи седем дни, за да можете да пуснете нова версия без прекъсване. Отмяната е незабавна.
Ключовете изтичат
Всеки ключ носи твърда дата на изтичане, определена при издаването:
| Среда | Валидност |
|---|---|
atn_live_… |
365 дни |
atn_test_… |
90 дни — тестовият ключ е за разработка, не за работа |
След нея всяко повикване отговаря 401 token_expired. Няма постепенност и няма
гратисен период — това е друг часовник, различен от условията за разработчици
по-горе.
Две неща пазят от изненада. whoami връща expires_at при всяко повикване, така
че health check може да следи собствения си ключ; и уведомяваме човека, издал
ключа, както и собствениците на акаунта — 30, 7 и 1 дни преди датата.
Ротирайте, вместо да чакате отказа: ротацията е същото седемдневно застъпване
както винаги, тоест смяната не ви струва прекъсване, а изтичането ви струва
цялото.
Промяна в условията
Условията за разработчици носят версия. Тя се записва върху всеки ключ при издаването му и върху акаунта, който я е приел. Когато условията се променят съществено, версията се вдига и за всеки акаунт, останал на старата, се отваря 75-дневен гратисен период.
През този период за вас не се променя нищо. Промяната е, че всеки отговор, който получавате — 200, 422, 429, какъвто и да е — носи:
X-Autonify-Terms: reaccept-by=2026-10-10
Записвайте този header. Той е единственото място, където новината стига до вашата страна, защото приема условията клиентът, а той обикновено не сте вие.
След тази дата всеки endpoint отговаря 403 terms_reacceptance_required,
включително whoami. Съобщението посочва датата и страницата.
Нищо не се отнема, ротира или преиздава от всичко това. Клиентът отваря Настройки → Developer API и натиска един бутон; още следващата ви заявка получава нормален отговор, със същия ключ, който вече държите. Няма какво да пускате наново и нито един идентификатор не сменя ръцете си.
Тестовите ключове следват същото правило. Едно правило се проектира по-лесно от две, а приемащият е един и същ клиент и в двата случая.
Права
Отмятат се за всеки ключ при създаването му. По подразбиране — само четене.
purchases:read purchases:write
sales:read sales:write
orders:write
bank:read
masterdata:read masterdata:write
filings:read filings:write
stock:read stock:write
books:read
work:read work:write
Ключ, създаден без отметнати права за запис, носи purchases:read,
sales:read, bank:read, masterdata:read, filings:read и stock:read.
work:read / work:write са само за отчитане на работа. Достигат
/billable-entries (§5, „Свършена работа за фактуриране") и никой друг път: ключът на
програма за отчитане на време може да подава часовете, които се фактурират на
клиента, но не може да издава, изпраща или анулира фактури, нито дори да ги
чете. Трите пътя приемат и sales:read / sales:write, така че съществуващ
ключ за продажби продължава да работи непроменен.
books:read не е в този набор, и това е нарочно. Всяко друго четене тук е
документ, от който контрагентът на клиента вече има копие — фактура, която е
издал, покупка, която е получил, банков ред, който вижда. Книгите са целият
регистър, затова се отмятат съзнателно или изобщо не.
filings:write записва изход; не подава нищо. Отбелязва какво е направил
човек през канала на НАП и входящия номер, който му е даден. В това API няма
право, което подава към НАП, защото няма и endpoint, който подава — вж. §7.
Няма право за администрация, фактуриране, ТРЗ, фирми или потребители, защото няма такъв endpoint. Повърхността на API-то е нарочно по-малка от тази на приложението.
Правата на ключа се пресичат с плана при всяка заявка. Ключ с sales:write
на план, който включва само подаване на поръчки, получава
403 ability_not_in_plan и започва да работи, непроменен, след ъпгрейд.
2. Лимити
Единици, не заявки. Плосък брояч на заявки би таксувал еднакво един ред и сто реда.
| Повикване | Единици |
|---|---|
GET /whoami, всяко четене по /filings |
0 |
| Идемпотентно повторение | 0 — обслужва се и над тавана |
| Един запис | 1 |
| Страница от списък (≤ 100) | 5 |
| Всеки запис (write) | 10 |
| Качване на файл | 25 |
Изготвен файл — PDF на фактура, нейният UBL, извлечение на контрагент, съхраненият оригинал — се таксува като списък, не като един запис: струва рендиране или изграждане на XML. Тежестта за файл е нещо друго; тя е цената да разчетем документ вместо вас, а не да върнем документ, който клиентът вече притежава.
Разрешеното е на фирма на ден, плюс дневен таван за целия акаунт, за да не може кантора с много клиенти да умножи лимита на фирма. Създаването на повече ключове за същата фирма не носи допълнителна квота. Burst — 120 заявки в минута на ключ.
Всеки отговор носи:
X-Autonify-Units-Limit: 5000
X-Autonify-Units-Remaining: 4310
X-Autonify-Units-Reset: 2026-07-27T23:59:59+03:00 # полунощ, Europe/Sofia
На тавана получавате 429 quota_exceeded и Retry-After. Никога частичен
или орязан отговор — половин отговор от счетоводно API е по-лош от отказ,
защото кодът ви ще го запише.
Заявка, отказана преди да е свършила работа — 404, невалидно тяло — се възстановява. Таванът не е наказание за печатна грешка.
Две нарочни изключения:
POST /purchasesиPOST /ordersимат 10% гратисна лента. Отказана фактура от доставчик или поръчка от клиент е документ, който съществува юридически, и губи го клиентът, не вие. В лентата повикването успява сX-Autonify-Quota-Warning: grace.- Всяко четене по
/filingsе освободено изцяло — списъкът, списъкът с файлове и самите файлове. Лимит по абонамент никога не спира подаване към НАП, а вашата квота е лимит по абонамент.POST /filings/{id}/transitionе запис и се таксува като такъв: отбелязването на изход е счетоводство, не четене за целите на съответствието.
Безплатно е цена, а не разрешение. Endpoint с нулева тежест пак изисква
правото, което декларира. Ключ, създаден само с orders:write, не може да
изброи подаванията на фирмата, а отказът, който получава, не струва нищо.
3. Идемпотентност
Idempotency-Key е задължителен на всеки POST и на всеки PATCH. Не защото
дублиран ред е разхвърляно, а защото дублирана фактура попада в юридически
непрекъсната номерация, става видима за НАП и не може да бъде тихо изтрита.
-
Същият ключ, същото тяло → първият отговор, повторен, за 0 единици, дори над квотата ви. Повторението е обозначено:
X-Autonify-Idempotent-Replay: true X-Autonify-Units-Charged: 0 -
Същият ключ, различно тяло →
422 idempotency_key_reused. Това е грешка във вашия код — два документа с един ключ — и отговор с id-то на първия документ би оставил втория да изчезне. -
Ключовете се пазят 24 часа. 5xx не се запомня, така че истински повторен опит след сървърна грешка наистина се изпълнява отново.
Помни се само това, което байтовете заслужават. 2xx, 400 и 422 са
присъди за вашата заявка, и едни и същи байтове винаги трябва да получават един
и същ отговор. Всичко останало — 402, 403, 409, 429, 5xx — е факт за
света в този момент: план, който е изтекъл, изразходвано разрешено количество,
заключен период. Тези отговори се освобождават, така че повторният опит,
който документацията ви казва да направите, наистина се изпълнява отново. И в
двата случая нищо не е било създадено — точно това прави освобождаването
безопасно.
Ако два ваши процеса се надпреварват с един ключ, губещият получава
409 idempotency_in_progress, а не втори документ.
4. Формати
Парите са винаги обект, никога число с плаваща запетая:
{ "amount": "1234.56", "currency": "EUR" }
Документът носи суми в собствената си валута и в евро, защото книгите са в евро, а фактурата на доставчика може да не е. Число с плаваща запетая освен това губи стотинки по пътя през JavaScript, на който е написана по-голямата част от интеграциите.
Количеството не е пари. Наличностите и количествата по редове са обикновени
десетични низове — "20.000" — защото три знака след запетаята от килограм не
са сума от нищо.
Датите са YYYY-MM-DD. Времевите отпечатъци са RFC 3339. Всяка граница на
работен ден е Europe/Sofia, включително нулирането на квотата.
Успех:
{ "data": { … }, "meta": { "next_cursor": "01k…", "limit": 50 } }
Грешка:
{ "error": { "code": "duplicate_document", "message": "…", "request_id": "01k…" } }
Някои откази носят и обект details до тези три — id-то, което вече имате,
прехода, който сте поискали, секундите до вдигането на изчакването. §8 казва
кои.
Едно изключение, и то е на фреймуърка. Тяло, което не мине валидацията по полета, се отговаря от Laravel в неговия собствен формат, със съобщения на езика на инсталацията:
{ "message": "Полето дата на издаване е задължително. (and 3 more errors)",
"errors": {
"issued_on": ["Полето дата на издаване е задължително."],
"lines": ["Полето редове е задължително."]
} }
Два endpoint-а валидират на ръка и затова връщат 422 validation_failed в
обичайния плик, с грешките по полета в details: POST /orders и
PATCH /counterparties/{id}. Всичко останало, което валидира тяло, връща
формата на фреймуърка. Обработвайте и двете; статусът е 422 и в двата случая, и
нито един от тях няма да се оправи при повторно изпращане. Разклонявайте по
статуса, не по тялото.
Списъците са с курсор, не със страници. Подавате ?cursor= от
meta.next_cursor, до ?limit=100, и ?updated_since= за инкрементална
синхронизация. Курсор, а не номер на страница, защото тези списъци се променят,
докато ги четете: документ, заведен между страница 2 и страница 3, измества
всеки следващ ред, а отместването би прескочило един ред мълчаливо.
Файлови отговори
Пет endpoint-а връщат файл вместо JSON: PDF-ът на фактурата и нейният UBL, съхраненият оригинал на покупка, извлечението на контрагент и един от файловете на едно подаване.
Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice-0000000042.pdf"
- Суровите байтове. Без обвивка
{"data":…}и без base64 — това, което тези адреси съществуват да подадат, е PDF, който купувачът разпечатва, или XML, който неговият ERP разчита, а обвиването би означавало всеки интегратор да напише една и съща стъпка за декодиране, преди да може да ги ползва. - Отказите пак са JSON. 403, 404 или 409 по тези адреси е обичайният плик
{"error":{…}}, така че обработката на грешки при вас не се разклонява по endpoint. Само 200 е байтове. - Заглавките за квотата пак ги има, и повикването пак се таксува и записва: измерването чете статуса, никога тялото.
Content-Dispositionе винагиattachment, с ASCII име на файл. Няма?inline=1— тук нищо не се показва в страница — а кирилско име би имало нужда от кодиране по RFC 5987, за да оцелее през всеки клиент, затова имената са транслитерирани или по id.
409 или 422 — правилото
Разликата не е в тежестта. Тя е чий факт е това.
- 422 е факт за вашите байтове. Полето липсва, ставката по ДДС не е от
тези, които ЗДДС има, тялото не отговаря на договора. Повторно изпращане на
същото тяло никога не би успяло, затова отговорът се запомня срещу вашия
Idempotency-Key. - 409 е факт за света. Документът вече е осчетоводен, периодът е заключен, проформата е превърната, преходът не е позволен от статуса, в който е подаването, купувачът още няма имейл адрес. Същите байтове успяват, щом фактът се промени, затова отговорът се освобождава и повторният ви опит наистина се изпълнява отново.
Ако някога се чудите как да прочетете отказ: 409 си струва да се повтори, след като вие или клиентът промените нещо. 422 си струва да се поправи в кода ви.
5. Endpoint-и
| Метод | Път | Право | Единици |
|---|---|---|---|
| GET | /whoami |
всяко | 0 |
| GET | /purchases |
purchases:read |
5 |
| GET | /purchases/{id} |
purchases:read |
1 |
| POST | /purchases (JSON) |
purchases:write |
10 |
| POST | /purchases (файл) |
purchases:write |
25 |
| GET | /purchases/uploads/{id} |
purchases:read |
1 |
| POST | /purchases/{id}/approve |
purchases:write |
10 |
| PATCH | /purchases/{id}/fields |
purchases:write |
10 |
| POST | /purchases/{id}/annul |
purchases:write |
10 |
| POST | /purchases/{id}/protocol |
purchases:write |
10 |
| GET | /purchases/{id}/file |
purchases:read |
5 |
| GET | /sales/invoices |
sales:read |
5 |
| GET | /sales/invoices/{id} |
sales:read |
1 |
| GET | /sales/next-number |
sales:read |
1 |
| POST | /sales/invoices |
sales:write |
10 |
| GET | /sales/invoices/{id}/pdf |
sales:read |
5 |
| GET | /sales/invoices/{id}/ubl |
sales:read |
5 |
| POST | /sales/invoices/{id}/send |
sales:write |
10 |
| POST | /sales/invoices/{id}/storno |
sales:write |
10 |
| POST | /sales/invoices/{id}/annul |
sales:write |
10 |
| POST | /sales/proformas/{id}/convert |
sales:write |
10 |
| GET | /billable-entries |
work:read или sales:read |
5 |
| POST | /billable-entries |
work:write или sales:write |
10 |
| DELETE | /billable-entries/{id} |
work:write или sales:write |
10 |
| POST | /orders |
orders:write |
10 |
| GET | /counterparties |
masterdata:read |
5 |
| POST | /counterparties |
masterdata:write |
10 |
| PATCH | /counterparties/{id} |
masterdata:write |
10 |
| GET | /counterparties/{id}/statement |
masterdata:read |
5 |
| GET | /items |
masterdata:read |
5 |
| POST | /items |
masterdata:write |
10 |
| GET | /chart-of-accounts |
masterdata:read |
5 |
| GET | /bank-accounts |
bank:read |
5 |
| GET | /bank-transactions |
bank:read |
5 |
| GET | /warehouses |
stock:read |
5 |
| GET | /stock |
stock:read |
5 |
| GET | /stock/movements |
stock:read |
5 |
| POST | /stock/transfers |
stock:write |
10 |
| POST | /stock/adjustments |
stock:write |
10 |
| POST | /stock/counts |
stock:write |
10 |
| POST | /stock/conversions |
stock:write |
10 |
| GET | /books/trial-balance |
books:read |
5 |
| GET | /books/journal |
books:read |
5 |
| GET | /filings |
filings:read |
0 |
| GET | /filings/{id}/files |
filings:read |
0 |
| GET | /filings/{id}/files/{fileId} |
filings:read |
0 |
| POST | /filings/{id}/transition |
filings:write |
10 |
Id, който принадлежи на друга фирма, отговаря 404 not_found, никога 403.
403 би потвърдил, че id-то съществува някъде, а това е оракул за
съществуване върху всяко ЕИК в платформата. Това важи за всеки адрес по-долу,
без да се повтаря при всеки един.
Започнете оттук
curl https://app.autonify.bg/api/v1/whoami \
-H "Authorization: Bearer $AUTONIFY_KEY"
whoami не струва нищо и работи дори когато планът не включва API, така че
вашият health check съобщава истинската причина, вместо да умре. Това е
endpoint-ът, върху който да построите мониторинга си.
{ "data": {
"company": { "id": "01k…", "eik": "131063188", "name": "Тест ЕООД",
"vat_status": "registered", "vat_number": "BG131063188" },
"environment": "live",
"plan": { "api": "full" },
"abilities": ["purchases:read", "sales:read", "sales:write", "…"],
"token": { "name": "ERP", "masked": "atn_live_01k9m2p1…4T7Q",
"expires_at": "2027-07-27T09:14:02+00:00" },
"quota": { "units_used_today": 690, "units_daily": 5000,
"units_remaining": 4310, "resets_at": "2026-07-27T23:59:59+03:00" }
} }
abilities е действащият набор — това, което ключът носи, стеснено от
плана. Ключ, чието sales:write в момента не може да се използва, казва това
тук, а не едва в мига, в който откаже.
Завеждане на фактура от доставчик
curl -X POST https://app.autonify.bg/api/v1/purchases \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: erp-doc-88213" \
-H "Content-Type: application/json" \
-d '{
"supplier": { "name": "Доставчик ЕООД", "eik": "131063188" },
"document_number": "0000012345",
"document_date": "2026-07-14",
"payment_due_on": "2026-08-13",
"currency": "EUR",
"total_net": "100.00",
"total_vat": "20.00",
"lines": [
{ "description": "Хартия A4", "product_code": "PAP-A4",
"quantity": 10, "unit_price": "10.00", "vat_rate": 20 }
]
}'
Задължителни: supplier.name, document_number, document_date, currency,
total_net, total_vat. По избор: supplier.eik, supplier.vat_number,
payment_due_on, kind (invoice | credit_note | debit_note |
protocol, по подразбиране invoice), expense_account_code, external_ref
и до 500 реда в lines.
expense_account_code — един документ, повече от една сметка. Изпратете го
на ниво документ, за да кажете къде отива целият разход, и на РЕД, за да
отнесете този ред другаде: фактура от наемодателя, която носи наем и
преактувана електроенергия, е един документ с две сметки, а не два документа.
Кодовете са от собствения сметкоплан на фирмата, затова сметка, която не е в
него — или която собственикът е спрял — се отказва, вместо да бъде мълчаливо
заменена. Ако пропуснете и двете, нищо не се променя: документът се
класифицира както досега. Ред, който не посочва сметка, е по сметката на
документа.
Сумите на документа — total_net и total_vat — остават меродавни каквото и
да казват редовете: те са това, което доставчикът е издал, което влиза в
дневника за покупки и с което се кредитира сметка 401. Редовете само описват
как разходът се РАЗПРЕДЕЛЯ, затова не се сверяват със сумите на документа и
списък, който не се събира, дава друго разпределение, а не отказ. Това, което
редовете не описват, се отнася по сметката на самия документ.
Една форма заслужава внимание и не се отказва: документ, чиято собствена
сметка е складова (302–308) и чиито редове оставят остатък. Остатъкът се
дебитира по складовата сметка без ред, който да го заведе в наличностите,
счетоводството и складът се разминават точно с него, а стоката излиза с нулева
себестойност при продажба. Документът се записва и осчетоводява както обикновено
— фактурата на доставчика е факт, който книгите Ви дължат да отразят — а при
осчетоводяването в приложението се появява известие със сумата. Ако искате
остатъкът да изчезне, опишете го на отделен ред или посочете сметка, която не е
складова. Закръгляването по редовете никога не води до известие: допуска се по
една стотинка на ред, защото Σ round(x) ≠ round(Σ x).
payment_due_on е падежът, който документът ПОСОЧВА, и е по избор, защото
падежът не е реквизит по чл. 114 ЗДДС — много фактури не посочват такъв.
Изпращайте го само когато го има на документа: той е това, което позволява на
справката за задълженията да отчете просрочие, а не само възраст, и което дава
дата на сметката в прогнозата за паричния поток вместо приетия 30-дневен срок.
Ако го пропуснете, документът просто няма падеж — никога не изпращайте срок,
който сами сте изчислили.
Един ред приема description, quantity, unit_price, по избор vat_rate,
net, vat — и двете се извеждат от количество × цена, когато липсват —
expense_account_code (виж по-горе) и product_code. Последното е начинът стоката да стигне до склада: ред с код,
който съвпада с артикул, зарежда наличност при осчетоводяването на документа.
Без него покупката е разход и нищо повече.
Отговорът е вашият собствен документ — доставчик, номер, дата, суми, право на данъчен кредит, разходна сметка — след като класификацията на Autonify е минала.
{ "data": {
"id": "01k…",
"kind": "invoice",
"source": "api",
"supplier": { "name": "Доставчик ЕООД", "eik": "131063188", "vat_number": null },
"document_number": "0000012345",
"document_date": "2026-07-14",
"payment_due_on": "2026-08-13",
"currency": "EUR",
"totals": {
"net": { "amount": "100.00", "currency": "EUR" },
"vat": { "amount": "20.00", "currency": "EUR" },
"gross": { "amount": "120.00", "currency": "EUR" },
"eur_net": { "amount": "100.00", "currency": "EUR" },
"eur_vat": { "amount": "20.00", "currency": "EUR" },
"eur_gross": { "amount": "120.00", "currency": "EUR" }
},
"vat_credit_right": "full",
"expense_account_code": "601",
"status": "posted",
"needs_review": false,
"review_reasons": [],
"created_at": "2026-07-14T10:02:11+03:00",
"updated_at": "2026-07-14T10:02:11+03:00",
"lines": [ { "description": "Хартия A4", "product_code": "PAP-A4",
"quantity": "10.000000",
"unit_price": { "amount": "10.00", "currency": "EUR" },
"vat_rate": "20.00",
"net": { "amount": "100.00", "currency": "EUR" },
"vat": { "amount": "20.00", "currency": "EUR" } } ]
} }
source казва откъде са дошли ПОЛЕТАТА, не откъде е дошла заявката: api за
документ, който сте описали, и api_upload за такъв, който нашият четец е
попълнил. Интегратор, който изяснява разминаване, трябва да знае кое от двете
гледа.
Защитата от дубликати е вградена: хеш върху доставчик, номер, дата и сума
означава, че повторно изпращане на същата фактура връща
409 duplicate_document със съществуващото id в details.existing_id, а не
втори документ.
Филтрирайте списъка с ?kind=, ?needs_review=, ?from=, ?to=, плюс
обичайните ?cursor=, ?limit= и ?updated_since=.
Или изпратете файл
Същият endpoint, multipart/form-data, едно поле file. PDF, JPG или PNG, до
10 MB.
curl -X POST https://app.autonify.bg/api/v1/purchases \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: scan-2026-07-27-0041" \
-F "file=@invoice.pdf"
{ "data": {
"id": "01k9…",
"object": "purchase_upload",
"status": "extracting",
"original_name": "invoice.pdf",
"document": null
} }
Получавате 202 и заглавка Location, а не документ — защото документ още
няма. Проверявайте разписката:
curl https://app.autonify.bg/api/v1/purchases/uploads/01k9… \
-H "Authorization: Bearer $AUTONIFY_KEY"
status |
Значение | document |
|---|---|---|
extracting |
На опашка или в момента се разчита. | null |
captured |
Разчетен и заведен. | документът |
needs_review |
Не се е прочело достатъчно, за да бъде заведен. Човек го довършва в приложението. | null |
duplicate |
Този вече го имате. | документът, който дублира |
failed |
Файлът изобщо не можа да бъде прочетен. | null |
Или се абонирайте за purchase.upload.finished (§9) и спрете да питате: то се
изпраща при всеки един от тези изходи, включително failed.
Три неща за този endpoint са нарочни и няма да се променят:
- Разчитането е асинхронно, с обичайния приоритет на акаунта. Няма
?wait=trueи няма да има: проверявайте разписката. В натоварен час това може да са минути, така че не блокирайте плащане в магазина заради него. - Файлът струва 25 единици, не 10. Планирайте качванията отделно от записите.
- Никога не получавате изхода на четеца. Нито суровите полета, нито текста,
нито увереността или рамките. Файл навътре, вашият собствен запис навън.
Ако разчитането е сгрешило, поправете документа — в приложението или през
PATCH /purchases/{id}/fieldsпо-долу — и поправката се помни за този доставчик, така че следващата му фактура се връща по-добре прочетена.
Качването изисква и правото за разчитане на документи (OCR), което е отделна
функция от API-то. Без него получавате 403 ocr_not_in_plan — а завеждането на
същия документ като JSON пак работи, защото лимит по абонамент никога не
спира завеждането на документ, само разчитането му вместо вас.
План, чието месечно количество разчитания е изчерпано, не е отказ: файлът
се приема и получава разписка както винаги, и се връща като needs_review, за
да го довърши човек. Да се загуби фактура от доставчик заради месечен брояч е
единствената форма, която този endpoint не бива да има.
Идемпотентността при качване хешира байтовете на файла, не само тялото, така
че същият ключ с различен скан се отказва (422 idempotency_key_reused),
вместо да получи мълчаливо отговора за първия документ. Повторно изпращане на
същия файл със същия ключ се повтаря, както и трябва.
Поправка, одобрение и анулиране на документ от доставчик
curl -X PATCH https://app.autonify.bg/api/v1/purchases/01k…/fields \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: erp-fix-88213-1" \
-H "Content-Type: application/json" \
-d '{
"supplier": { "name": "Доставчик ЕООД", "eik": "131063188" },
"document_number": "0000001234",
"document_date": "2026-06-11",
"payment_due_on": "2026-07-11",
"total_net": "120.00",
"total_vat": "24.00"
}'
Петте заглавни полета над payment_due_on са задължителни — това е API
половината на екрана за преглед и подава заглавната част като цяло, а не поле по
поле. payment_due_on е изключението: изпратете го, за да зададете или промените
посочения падеж, изпратете го като null, за да кажете, че документът не посочва
такъв, и го пропуснете изцяло, за да запазите каквото е записано. Пропускането
не е същото като null — клиент, писан преди полето да съществува, не бива
мълчаливо да изтрие падеж, който някой е прочел от документа. Връща документа с
meta.corrections_recorded до него и изпълнява същите четири последици, които
изпълнява и екранът в браузъра: снимката на записващия, оттеглената подсказка
от регистъра, основния запис на доставчика и обучението. API поправка, която
прескочи някоя от тях, постепенно би направила корпуса за обучение да означава
„каквото хората са поправяли в браузър“.
409 document_posted— сумите вече са в главната книга. Средството е сторно, което съществува и оставя следа.409 document_annulled— анулиран документ е неизменяем.
POST /purchases/{id}/approve осчетоводява заведен документ и връща документа
със status: "posted". Идемпотентно е в услугата: две одобрения осчетоводяват
веднъж.
POST /purchases/{id}/annul приема задължително reason (до 500 знака) и
записва огледален запис — никога редакция. Документ, който не е стигал до
главната книга, не може да бъде анулиран: 409 annul_refused. (Изхвърлянето на
неосчетоводен документ е решение, което човек взема, докато го гледа, и нарочно
не е в това API.)
POST /purchases/{id}/protocol издава протокола по чл. 117 за придобиване с
обърнато данъчно задължение и връща 201 с протокола като документ за покупка
(kind: "protocol", номер ПРОТ-…). Идемпотентно е — второ повикване връща
вече съществуващия протокол — и се отказва с
409 protocol_requires_reverse_charge, когато документът не е такова
придобиване. Дали е, е решение на класификацията, а не твърдение, което
повикващият може да направи.
GET /purchases/{id}/file подава съхранения оригинал — сканирания файл или
PDF-а, който доставчикът е изпратил. Байтове, по §4. Това е собственият
документ на клиента, който излиза обратно — единственото нещо, за което
забраната върху разчетеното никога не е била. Документ, заведен като JSON, няма
файл: 404 no_file.
Издаване на фактура
curl -X POST https://app.autonify.bg/api/v1/sales/invoices \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: shop-order-5512" \
-H "Content-Type: application/json" \
-d '{
"kind": "invoice",
"issued_on": "2026-07-27",
"buyer": { "name": "Клиент ЕООД", "eik": "831919992" },
"lines": [
{ "description": "Абонамент", "quantity": 1, "unit_price": "50.00", "vat_rate": 20 }
]
}'
kind е invoice, proforma или credit_note. buyer приема или
counterparty_id, или name, плюс по избор eik, vat_number, address. По
избор върху документа: taxable_event_on (по подразбиране issued_on — аванс
или продължаваща доставка изпраща своя), payment_method,
corrects_document_id, external_ref. До 200 реда, всеки с
description + quantity + unit_price + vat_rate, плюс незадължителния
unit (мерна единица до 20 знака, напр. бр. или kg — печата се на
фактурата) и незадължителните item_id и restocked по-долу. Количествата по редовете трябва да са над
нула: отрицателен ред не е начинът една доставка да се намали — за това служи
кредитното известие, то взема следващия номер от същата поредица и е
единственият инструмент, който може и да върне стоката.
vat_rate трябва да е 20, 9 или 0. Това са ставките, които ЗДДС има.
Всичко друго се отказва на входа, защото ред с 10% би произвел фактура, която
начислява 10%, счетоводен запис, който ги кредитира, и ДДС декларация, която
обявява продажбата за нулева ставка — недеклариран данък, а това е посоката,
която НАП санкционира.
Ред, който посочва артикул, движи стока. lines[].item_id е id-то на
приходен Артикул от GET /items и е същата незадължителна връзка, която изпраща
и композиторът в браузъра: фактура или дебитно известие с такъв ред изписва
стоката от складовата аналитичност и осчетоводява себестойността през същия
двигател, през който минава и поръчката. Без него редът е услуга, свободен текст
или стока, която сте решили да не водите — документът осчетоводява приход и ДДС
и нищо не напуска рафта.
Id-то се търси в рамките на фирмата на ключа, а id, което не е нейно, е
422 по това поле, а не мълчаливо прескачане. То е 26 знака, които всеки може да
съчини, и без това стесняване ключ би могъл да изпише от чуждия рафт и да сложи
чужда стока в 611 на тази фирма.
lines[].restocked казва, че стоката се е върнала, и само кредитно известие
може да го каже. Чл. 115 е за СТОЙНОСТ — договорена по-късно отстъпка,
развалена доставка, рекламация, а понякога и истинско връщане — и нищо в
документа не ги различава, затова флагът е по ред и по избор, а мълчанието
никога не движи стока. Ред с връщане връща бройките по себестойността на
самото изписване, на мястото, от което са тръгнали, и до количеството, което то
наистина е взело — затова corrects_document_id е това, което изобщо го
прави възможно.
При всеки друг kind флагът е 422, а не прескочено поле. По-рано се изхвърляше
мълчаливо, докато item_id на същия ред движеше артикула в ОБРАТНАТА посока —
интегратор, който чете 201 за "restocked": true и гледа как наличността
намалява, е бил заблуден два пъти.
corrects_document_id трябва да е фактура на самата тази фирма. Валидиран
като обикновен низ, той би позволил на ключ да закачи кредитно известие за
фактура на друг клиент — известието ляга в тези книги, сочейки през оградата, и
блокира завинаги анулирането на пострадалия. „Не е ваша“ и „не съществува“
отговарят еднакво, така че проверката не издава нищо.
{ "data": {
"id": "01k…",
"kind": "invoice",
"status": "issued",
"annulled_on": null,
"corrects_document_id": null,
"series": "1",
"number": 42,
"issued_on": "2026-07-27",
"taxable_event_on": "2026-07-27",
"buyer": { "name": "Клиент ЕООД", "eik": "831919992" },
"currency": "EUR",
"totals": {
"net": { "amount": "50.00", "currency": "EUR" },
"vat": { "amount": "10.00", "currency": "EUR" },
"gross": { "amount": "60.00", "currency": "EUR" }
},
"vat_breakdown": { "20": { "base": "50.00", "vat": "10.00" } },
"origin": "api",
"created_at": "2026-07-27T12:00:00+03:00",
"updated_at": "2026-07-27T12:00:00+03:00",
"lines": [ … ]
} }
vat_breakdown е по ставки и носи обикновени десетични низове — това е
собствената ДДС разбивка на документа, в същата форма, която чете декларацията,
а кофата с нулева ставка носи и правното основание (basis) до тях.
Номерацията минава през същия брояч, който ползва и браузърът, така че фактура, издадена през API, взема следващия номер от същата непрекъсната поредица като издадената на ръка. Няма отделна поредица за API — това би бил най-бързият начин да се получи празнина, каквато чл. 113 не допуска.
GET /sales/next-number е предварителен преглед. Номерът се заделя при
издаване, така че двама, които питат едновременно, виждат един и същ отговор, а
го получава само единият. Никога не го изписвайте върху документ, който още не
сте издали.
Отказ, който е за състоянието на фирмата — сметкоплан без сметка 702, заключен
период, запис, който не се уравновесява — е 409 invoice_refused, а не 422.
Нищо не е било създадено, така че повторният ви опит изпълнява отново работа,
която никога не се е случвала.
Филтрирайте списъка с ?kind=, ?from=, ?to=.
Животът на фактурата след издаването
И шестте по-долу влизат в същата услуга, която извиква бутонът в браузъра. Сторно, издадено през API, взема следващия номер от същата поредица по чл. 113, осчетоводява същото обратно записване и се отказва от същите пазачи, както и това, натиснато в приложението — защото е същото.
GET /sales/invoices/{id}/pdf — фактурата като PDF, байтове. Изготвя се
при поискване, ако съхраненото копие липсва, така че документ, издаден преди да
е минал рендерът, пак може да бъде изтеглен.
GET /sales/invoices/{id}/ubl — XML по EN 16931 / UBL 2.1, файлът, който
ERP-то на купувача разчита. Изгражда се от записа при всяко повикване, а не се
съхранява: документът е неизменяем след издаване (чл. 116), така че XML-ът е
чиста функция от него. Проформата няма форма на е-фактура —
404 ubl_not_available — а запис, чиито редове вече не възпроизвеждат сумата
му, е 409 ubl_refused.
POST /sales/invoices/{id}/send — изпращане по имейл до купувача. Връща
202:
{ "data": { "id": "01k…", "status": "queued", "to": "kupuvach@example.bg" } }
На опашка, не доставено — доставянето е присъда на доставчика на поща и идва по-късно; нищо тук не може честно да го обещае. Таваните са таваните на браузъра, ключ по ключ: един бюджет на документ и един на фирма на ден, общи с бутона за повторно изпращане, който клиентът може да гледа в момента. Два измервателя върху една пощенска кутия не са измерване.
Един документ стига до купувача си веднъж. Ако приложението вече го е
изпратило — всеки издаден документ с адрес на купувача се изпраща автоматично,
освен ако фирмата не е изключила това — тук се връща 200 и не се изпраща нищо:
{ "data": { "id": "01k…", "status": "already_delivered",
"to": "kupuvach@example.bg", "delivered_at": "2026-08-30T09:12:44+00:00" } }
Това не е грешка и не изразходва разрешеното изпращане: състоянието, което
искате, вече е налице. За да стигне до купувача второ копие, изпратете
{"resend": true} — това е начинът, по който API-то изписва бутона „Изпрати
повторно“ — и отговорът е 202 както по-горе. Без него извикването на този
endpoint веднага след издаване не може да изпрати един номериран документ по
чл. 114 два пъти, от два различни идентификатора на съобщение.
409 no_buyer_email— документът още няма адрес на купувача.429 send_cooldown— изпратен е преди малко.details.retry_afterе в секунди.429 send_daily_limit— фирмата е изчерпала днешното разрешено ръчно изпращане.details.retry_afterе в секунди.403 send_not_in_sandbox— sandbox документи никога не се изпращат по имейл (§6). Отказ, а не тихо нищо: endpoint, който връща 202 и не изпраща нищо, е по-лош за разработка от такъв, който казва „не“.
POST /sales/invoices/{id}/storno — кредитно известие по чл. 115 ЗДДС.
reason е задължително, защото чл. 115, ал. 4 прави основанието част от
документа. По избор quantities кредитира част, ключирано по индекса на реда в
оригиналната фактура; ако липсва, се кредитира цялата фактура. Връща 201 с
известието, чието corrects_document_id сочи фактурата.
POST /sales/invoices/{id}/annul — този документ изобщо не е трябвало да
бъде издаван. Различен правен акт от сторното, оттам и различен адрес:
кредитното известие обявява обратно записване, анулирането не обявява нищо.
Номерът остава изразходван и в двата случая (чл. 78 ППЗДДС). reason е
задължително; връща 200 със status: "annulled". annulled_on не се
приема, както и в браузъра — датата решава в кой ДДС период попада обратното
записване, а да оставим повикващия да я избира означава да му оставим да мести
начисления данък между периоди.
И двете връщат 409 period_locked срещу затворен период, и
409 storno_refused / 409 annul_refused, когато услугата откаже — фактура,
която вече е кредитирана, не може след това да бъде анулирана.
POST /sales/proformas/{id}/convert — проформа → фактура. Истинската
фактура взема следващия номер от поредицата по чл. 114, никога номера на
проформата; онзи брояч законът не чете. Едно превръщане на проформа: второто е
409 convert_refused, с уникален индекс зад него. Връща 201 с новата
фактура.
Свършена работа за фактуриране
Абонаментна фактура, настроена да фактурира свършената работа (на
/recurring-invoices), включва във фактурата за всеки период услугите,
отчетени за клиента в този период — по един ред за всеки запис. С тези три
адреса програма за отчитане на време, helpdesk или ERP предава работата, без
никой да я въвежда отново.
curl -X POST https://app.autonify.bg/api/v1/billable-entries \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: tracker-7731" \
-H "Content-Type: application/json" \
-d '{ "counterparty_id": "01k…", "performed_on": "2026-07-03",
"description": "Консултация по договора", "quantity": "2.5",
"unit": "ч.", "unit_price": "80.00", "vat_rate": 20,
"external_ref": "tracker-7731" }'
{ "data": {
"id": "01k…", "counterparty_id": "01k…", "performed_on": "2026-07-03",
"description": "Консултация по договора", "quantity": "2.5000", "unit": "ч.",
"unit_price": "80.0000", "vat_rate": 20,
"net": { "amount": "200.00", "currency": "EUR" },
"item_id": null, "account_code": "703", "status": "unbilled",
"sales_document_id": null, "billed_at": null, "recurring_invoice_id": null,
"source": "api", "external_ref": "tracker-7731", "…": "…"
} }
POST /billable-entries — задължителни: counterparty_id (контрагент на
фирмата), performed_on (денят, в който работата е свършена, не по-късно от
днешния ден в София), description, quantity (> 0), unit, unit_price
(без ДДС, в евро, до четири знака след десетичната запетая) и vat_rate
(ставка, която продавачът може да начислява — 20, 9 или 0, а за фирма, която не
е регистрирана по ЗДДС, само 0). По избор: item_id (приходен артикул на
фирмата), account_code (702 | 703 | 709; по подразбиране сметката на
артикула, иначе 703) и external_ref (до 100 знака).
descriptionе до 240 знака, защото записът се отпечатва като ред на фактурата03.07.2026 — Консултация по договора, а редът на фактура е до 255 знака навсякъде, откъдето се издава фактура.201, когато записът е създаден. Същиятexternal_refотново връща200със записа, който вече е създаден — изпращайте собствения идентификатор на работата от вашата програма и повторната синхронизация на седмицата никога няма да включи един и същ час във фактура два пъти. Това е в допълнение къмIdempotency-Key(§3), а не вместо него: ключът покрива повторена заявка за едно денонощие, а идентификаторът — една и съща работа, изпратена два пъти.403 recurring_not_in_plan— планът не включва абонаментни фактури. Четенето и изтриването на записи продължават да работят.unit_priceе десетичен низ с четирите знака, които пази редът на фактурата, както при самите редове;netе сума, затова е парично поле (§4).
Какво става със записа след това. Между 1-во и 5-о число на месеца след
периода абонаментът на клиента издава фактурата за периода: по един ред за
всеки нефактуриран запис с дата в периода, последният ден на периода като
дата на данъчното събитие (чл. 25, ал. 4 ЗДДС), а записът става billed — със
sales_document_id и billed_at — в същата транзакция, в която е издаден
документът. Запис с дата в период, чиято фактура вече е издадена, никога не
влиза в по-късна: това би декларирало доставка в грешен период. Той остава
unbilled, а клиентът получава известие и възможност да го издаде с отделна
фактура. Затова изпращайте работата, когато е свършена, с датата, на която е
свършена.
GET /billable-entries — ?counterparty_id=, ?status=unbilled|billed и
?from= / ?to= по performed_on, включително. Страниране с курсор (§4).
DELETE /billable-entries/{id} — изтрива запис, който не е в документ:
200 {"data": {"id": "01k…", "deleted": true}}. Фактурираният запис е описът
на издадена фактура, която чл. 116 забранява да се поправя, затова отговорът е
409 entry_billed с details.sales_document_id; поправката е кредитно
известие (POST /sales/invoices/{id}/storno).
Правото е work:write / work:read — двойката, с която се създава ключът на
програма за отчитане на време и която достига само тези три пътя — или
sales:write / sales:read: записът съществува, за да стане ред на фактура, а
ключ, който може да издаде фактурата, може да каже и какво влиза в нея.
Подаване на поръчка от магазин
curl -X POST https://app.autonify.bg/api/v1/orders \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: shop-5512" \
-H "Content-Type: application/json" \
-d '{
"store_id": "01k…",
"id": "SHOP-5512", "number": "5512", "status": "completed",
"currency": "EUR",
"created_at": "2026-06-10T09:00:00+03:00",
"paid_at": "2026-06-10T09:01:00+03:00",
"payment_method": "card",
"total_gross": "120.00", "total_vat": "20.00",
"lines": [
{ "name": "Чаша", "sku": "CUP-1", "quantity": 2, "unit_price": "50.00",
"vat_rate": 20, "net": "100.00", "vat": "20.00", "gross": "120.00" }
]
}'
Същото завеждане, до което стигат и подписаните webhook-и от магазините, съдено по същия договор, по който се съдят WooCommerce плъгинът, OpenCart плъгинът и Windows конекторът за касови апарати. Втори нормализатор за API повикванията би означавал две представи какво е поръчка, които се разминават, докато магазинът на търговеца и неговият ERP осчетоводят една и съща продажба по два различни начина.
store_id посочва магазина и се търси в рамките на фирмата на ключа —
магазин, който е на някой друг, просто не се намира. Задължителни до него:
id, status, currency, created_at, payment_method, total_gross,
total_vat, lines. По избор: number, paid_at, delivered_at,
business_date (строго Y-m-d), total_discount, buyer. Сумите не може да
са отрицателни — посоката на парите живее в събитието, а отрицателна сума тук
винаги е адаптер, прочел сторно като продажба.
Документът трябва да се схожда сам със себе си. Σ lines.vat трябва да е равно на
total_vat, а Σ lines.gross — на total_gross, и двете с точност до стотинка.
Това не е формалност: книгите кредитират начисленото ДДС по СБОРА НА РЕДОВЕТЕ и
извеждат прихода като total_gross − този сбор, докато документът по чл. 114
взема своите суми от ЗАГЛАВНАТА ЧАСТ, а разбивката по ставки — от редовете. Тяло,
чиито две половини се различават, не се чупи никъде по-нататък — то се осчетоводява,
а разликата отива мълчаливо в справка-декларацията на търговеца. Обърнете внимание,
че total_discount се посочва, но не се изважда: редовете вече са намалени с
отстъпката, точно както ги изпращат WooCommerce и Shopify, така че тя никога не
влиза в сбора. 422 validation_failed посочва и двете суми, и разликата.
Синхронно, докато webhook-ът е на опашка: тук нищо не чака плащане в магазин, а
вие искате id-то на поръчката обратно. Връща 201 с поръчката, сумите ѝ във
валутата на магазина и в евро, и delivered_at — полето, което казва дали
продажбата вече е в книгите. Завеждането е идемпотентно по (магазин, номер на
поръчка): повторно изпращане обновява преходите „платена/доставена“ и никога не
дублира, така че 201 не означава „нова“.
404 not_found— няма такъв магазин на тази фирма.409 store_not_push— Shopify и eMAG магазините се дърпат по курсор, а приемането на подаване би позволило интеграция да вкара поръчки, които истинският конектор после опровергава при следващото си минаване.402 plan_required— няма активен план или пробен период.details.retryableе true: задръжте поръчката и я изпратете отново, щом има. Пауза, не отхвърляне.422 validation_failed— тялото не отговаря на договора, с грешките по полета вdetails. Единственият отказ тук, който никога няма да се оправи при повторение.409 period_locked/409 order_refused— главната книга е отказала.
Тази посока носи гратисната лента (§2), по същата причина, по която я носи
и POST /purchases.
Наличности
Целият този раздел иска план, който включва складовия модул. Без него всеки
маршрут по-долу отговаря 403 inventory_not_in_plan — различен отказ от
api_not_in_plan: планът има API и няма този модул, така че по-голям API лимит
не променя нищо, а клиентът сменя плана. Sandbox ключовете не правят изключение,
защото вратата пак остава затворена за фирмата, за която е ключът. Това, което
работи във всеки план, е СЧЕТОВОДСТВОТО: покупка с lines[].product_code
продължава да завежда стоката в аналитичността, а изпълнена поръчка продължава да
я изписва и да осчетоводява себестойността. Платени са само тези endpoint-и.
GET /warehouses изброява местата, където може да има стока: магазин с каса и
онлайн канал са и двете складове тук, а kind казва кое какво е. Филтър
?kind=.
GET /stock отговаря по артикул, с разбивка по места:
{ "data": [ {
"id": "01k…",
"sku": "CUP-1",
"name": "Чаша",
"quantity_on_hand": "20.000",
"unit_cost": { "amount": "5.00", "currency": "EUR" },
"locations": [ { "warehouse_id": "01k…", "quantity": "8.000" } ],
"updated_at": "2026-06-05T12:00:00+03:00"
} ], "meta": { "next_cursor": null, "limit": 50 } }
quantity_on_hand е винаги общото за фирмата, дори когато ?warehouse_id=
стеснява разбивката под него: двете отговарят на различни въпроси и ви трябват
и двете, за да засечете инвентаризация. unit_cost е среднопретеглената цена
по СС 2 за артикула във валутата на книгите — не по места, защото средна цена
на място би направила пренасянето на кашон от един рафт на друг да отчита
печалба или загуба. ?sku= стеснява до един артикул; склад, който не е на
тази фирма, е 404 not_found.
Отрицателна наличност на едно място е информация, а не грешка, и се съобщава както си е. Магазин, който е продал стока, която книгите още държат в склада, я е преместил, без да го запише, и «магазин −3 / склад +10» изглежда точно така. Общото за фирмата и сметка 304 остават верни, а следващата инвентаризация го решава.
GET /stock/movements?sku= е стоковата карта: всички движения на един
артикул, най-старото първо, с текущото салдо до тях. Така числото по-горе се
доказва, вместо да му се вярва — салдото (balance) на най-новото движение на
артикула е неговото quantity_on_hand, защото се сумира върху цялата история,
в ред по работен ден, преди каквото и да е филтриране и страниране.
meta.quantity_on_hand носи това число на всяка страница, така че може да
проверите колоната, без да я четете до края.
curl "https://app.autonify.bg/api/v1/stock/movements?sku=CUP-1&from=2026-06-01&to=2026-06-30" \
-H "Authorization: Bearer $AUTONIFY_KEY"
{ "data": [ {
"id": "01k…",
"moved_at": "2026-06-05T12:00:00+03:00",
"warehouse_id": "01k…",
"direction": "in",
"quantity": "20.000",
"unit_cost": { "amount": "5.00", "currency": "EUR" },
"value": { "amount": "100.00", "currency": "EUR" },
"balance": "20.000",
"movement_type": null,
"source_type": "purchase_document",
"source_id": "01k…"
} ],
"meta": { "next_cursor": null, "limit": 50, "sku": "CUP-1", "name": "Чаша",
"quantity_on_hand": "20.000",
"from": "2026-06-01", "to": "2026-06-30" } }
sku е задължителен — текущото салдо е на един артикул. SKU, който тези книги
никога не са държали, е празна страница, а не 404, точно както ?sku= на
GET /stock: артикул, който не съществува тук, няма история, а не липсваща
история.
?from= и ?to= са включително, по работен ден, и стесняват какво се показва,
никога какво се брои. Салдо, преизчислено само върху видимите редове, би се
чело като „артикулът е държал толкова“, а би означавало „толкова е минало от 1-во
число“ — същото число с друго значение, върху единствения документ, с който
счетоводителят засича артикул.
value е това, което движението е направило със сметка 304: при изписване —
среднопретеглената цена, която е била в сила тогава, а не преоценка по
днешната. Не го извеждайте наново с умножение — unit_cost е закръглен до
стотинка като всяко друго число за пари тук, докато аналитичността носи четири
знака.
movement_type е записът от номенклатурата на SAF-T, с който редът е
подпечатан, и е null там, където никой не е подпечатвал: получаването по
покупка и обикновеното изписване по поръчка го оставят празно, а стоковият файл
извежда записа от източника и посоката, когато се изгражда. Тук получавате това,
което редът носи, никога предположение какво ще каже файлът.
Два от тези кодове означават нещо различно от всеки друг ред. При 130
«Увеличение от преоценка» или 140 «Намаление от преоценка» обезценка по СС 2 е
преместила СТОЙНОСТ, а не бройки: quantity е "0.000", unit_cost носи целия
размер на обезценката или на нейното възстановяване, value го повтаря, а
balance остава непроменено. Съобщава се, а не се крие, защото този ред е
единствената следа, която артикулът носи, че изобщо е бил обезценен.
Странирането е keyset по (работен ден, id), а не по голото id, което ползва
всеки друг списък тук. Документ със задна дата — брак от юни, въведен през юли —
носи по-късно id от движения, случили се след него, и страниране само по id би
върнало колона със салдо, което подскача. Подавате ?cursor= от
meta.next_cursor както обикновено; курсор, който не е движение на този артикул,
е 404 not_found, а не мълчаливо започване отначало — мълчаливото рестартиране
на синхронизация е начинът едни и същи движения да бъдат внесени два пъти.
?updated_since= тук не се приема, за разлика от всеки друг списък — той се
пренебрегва, вместо да бъде спазен, така че не синхронизирайте по него. Ред за
движение никога не се редактира: историята само расте, а курсорът е мястото, до
което сте стигнали.
Няма повикване, което да зададе количество, и няма да има. Очевидното API,
което всеки иска, е PUT /stock/{sku} {quantity}. То не може да съществува
тук: наличността е аналитичност. Тя се засича със сметка 304, всяко
остойностяване я чете, а стойността ѝ е сумата от движенията, които са я
произвели. Повикване, което ѝ присвоява стойност, чупи тази сума мълчаливо —
книгите казват едно, броенето друго, и от нито едната страна не остава запис
кой запис е бил в разрез.
Затова стоката се движи както се движи в приложението: с документ.
curl -X POST https://app.autonify.bg/api/v1/stock/transfers \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: wms-move-4471" \
-H "Content-Type: application/json" \
-d '{
"from_warehouse_id": "01k…",
"to_warehouse_id": "01k…",
"moved_on": "2026-06-05",
"lines": [ { "sku": "CUP-1", "quantity": "8" } ]
}'
И двата склада трябва да са на тази фирма и да са различни; до 200 реда, всяко
количество над нула. Два реда с един артикул в един документ се сумират от
услугата, а не се записват два пъти. Връща 201:
{ "data": { "document_number": "ПРХ-000004",
"from_warehouse_id": "01k…", "to_warehouse_id": "01k…",
"moved_on": "2026-06-05",
"lines": [ { "sku": "CUP-1", "quantity": "8" } ] } }
Прехвърлянето променя къде е стоката и нищо друго: същото количество, същата средна цена, без счетоводен запис. Поправката е обратното прехвърляне, което е операцията, която наистина се е случила.
409 period_locked— периодът е затворен.409 transfer_refused— факт за наличността в този момент: няма достатъчно в източника, непознат SKU, мярка, която не се преобразува.details.reasonказва кое.
Корекция / брак
POST /stock/adjustments изписва количества със знак, всяко с основание.
curl -X POST https://app.autonify.bg/api/v1/stock/adjustments \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: wms-writeoff-8891" \
-H "Content-Type: application/json" \
-d '{
"adjusted_on": "2026-06-20",
"warehouse_id": "01k…",
"notes": "Счупени при разтоварване",
"lines": [ { "sku": "CUP-1", "quantity_delta": "-3", "reason": "scrap" } ]
}'
adjusted_on и lines са задължителни; warehouse_id по подразбиране е
основното място на фирмата — на търговец с един склад не се задава въпрос с един
отговор — а notes е свободен текст. До 200 реда, всеки с ненулево
quantity_delta с най-много три знака след запетаята, което е собствената
точност на аналитичността.
Основанието е задължително и наборът е затворен:
reason |
Какво казва, че се е случило | SAF-T |
|---|---|---|
scrap |
брак — повредена, развалена, унищожена стока | 160 |
shortage |
липса — няма я, и никой не може да каже къде е отишла | 120 |
surplus |
излишък — на рафта има повече, отколкото книгите държат | 110 |
own_use |
лично ползване — извадена от дейността от собственика ѝ | 180 |
donation |
дарение — предоставена безвъзмездно | 150 |
za_smetka_na_mol |
за сметка на МОЛ — липса, за която някой отговаря | 120 |
Знакът решава счетоводните страни и не може да носи това, което носи
основанието: записа по SAF-T (брак, подаден като «други движения», е невярно
твърдение към НАП) и последицата по чл. 79 ЗДДС, която зависи от причината и
никога от посоката. Основание на свободен текст не би могло нито да се съпостави
с номенклатура, нито да се търси, затова непознато е 422, което никакво
повторение не оправя. surplus е единственото основание, което може да носи
положително количество, а всяко друго в спор със знака си се отказва.
Връща 201 с документа: неговия номер (КОР-…), нетния му ефект върху 304 като
пари, и по ред — количеството по книги, разликата, основанието и
movement_type, който ще види четецът на SAF-T; връща се, защото интегратор,
който засича подаден стоков файл, няма друг начин да го научи.
Едно поле нарочно го няма по този кабел: продажната цена, по която може да се
вдигне начет (чл. 82 ЗЗД / чл. 203 КТ), е поле само в браузъра, така че ред
za_smetka_na_mol, изпратен оттук, се начислява по стойността, която стоката е
носила, и нищо не се кредитира по 709.
409 adjustment_refused— факт за наличността в този момент: повече, отколкото това място държи, непознат SKU, основание в спор със знака си.details.reasonказва кое.409 period_locked— отказва се, никога не се измества. Датата е избрана от повикващия, а преместването на документа в месец, който той не е посочил, би подало ДДС период, който противоречи на вече изпратения.
Инвентаризация
POST /stock/counts заявява какво има по рафтовете на едно място в един ден.
curl -X POST https://app.autonify.bg/api/v1/stock/counts \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: wms-count-2026-06-20" \
-H "Content-Type: application/json" \
-d '{
"counted_on": "2026-06-20",
"warehouse_id": "01k…",
"lines": [ { "sku": "CUP-1", "quantity_on_hand": "17" } ]
}'
Това е повикването, което прилича на API за задаване на количество и е точно
обратното на него. Нищо не се присвоява. Тялото заявява какво има на рафта, а
РАЗЛИКАТА спрямо книгите на това място става корекция с основание (shortage
или surplus), стойност по среднопретеглената цена, счетоводен запис и код по
SAF-T — така аналитичността продължава да е равна на сумата от движенията си.
Ред, който съвпада, не движи нищо и въпреки това се връща в описа, с
quantity_delta "0.000" и без основание: класифицирането му би сложило липса
на всеки артикул, който търговецът някога е преброил вярно. До 500 реда.
Нулата е законен отговор — „рафтът е празен“ е точно това, което казва броенето на изчерпан артикул. SKU, който тялото не назовава, остава без отговор, а не нула. Аналитичността е на фирма, а броенето е на място, така че мълчанието означава „не е броено тук“; зануляване при мълчание би изтрило наличността на всяко друго място в мига, в който един магазин преброи, и точно това позволява да изпратите частичен опис честно.
409 count_refused,details.reasonui.items.count_stale— снимка, по-стара от последното броене на това място, независимо дали то е дошло от този endpoint, от браузъра или от свързана каса. Това е единственият отказ тук, който се чете като грешка и не е: всяка разлика се извежда спрямо ТЕКУЩИТЕ книги, така че повтарянето на стара снимка не е безобидно повторение, а втора, противоположна корекция. Съобщението назовава деня, в който мястото е броено последно.409 count_refusedпокрива и обикновените факти за наличността, а409 period_locked— подадения месец.
Връща 201 със същата форма на документа, която връща и корекцията, с номер
ИНВ-…, и носи всеки ред, който сте изпратили — и движилите се, и съвпадналите
— с counted_quantity до book_quantity.
Преработка
POST /stock/conversions изразходва артикули и произвежда други, като разпределя
една отчетна стойност между тях.
curl -X POST https://app.autonify.bg/api/v1/stock/conversions \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: wms-kit-4471" \
-H "Content-Type: application/json" \
-d '{
"direction": "disassemble",
"warehouse_id": "01k…",
"converted_on": "2026-06-20",
"allocation_method": "sales_value",
"inputs": [ { "sku": "KIT-1", "quantity": "2" } ],
"outputs": [ { "sku": "PART-A", "quantity": "2", "sale_value": "30.00" },
{ "sku": "PART-B", "quantity": "2", "sale_value": "10.00" } ]
}'
Задължителни: direction (disassemble | assemble), warehouse_id,
converted_on и поне по един ред в inputs[] и outputs[] — до 200 реда от
всяка страна — като всеки ред има sku и quantity над нула.
По избор върху документа: allocation_method, residual_method,
item_composition_id (рецепта на същата фирма), input_account_code,
output_account_code, scrap_account_code, notes и до 20 реда в costs[] —
разходи за преработка, поети по СС 2 т. 6.1, всеки с basis
(actual | normal_capacity), account_code, amount и, при втората база,
normal_capacity. По изходящ ред: sale_value, fixed_share, manual_value,
is_by_product, nrv. По входящ ред: abnormal_loss — частта от
изразходваното, изгубена извън нормалните граници (СС 2 т. 7.2 „а“), която
никога не стига до оцелелите артикули.
allocation_method е базата, по която се разпределя входящата стойност —
sales_value (по подразбиране), quantity, fixed_share, by_product или
manual. При разкомплектоване това е преценка, а не аритметична подробност.
residual_method казва как се разпределя остатъкът, след като съпътстващите
продукти са извадени, и приема същите четири пропорционални бази — никога самия
by_product, защото „извадете съпътстващите продукти отново“ не е указание какво
да се прави с останалото.
manual се отказва, когато числата не покриват общата стойност, и точно
този отказ е причината този endpoint да не може да бъде просто „подайте числата,
които вече имате“. Десет и десет, въведени срещу отчетна стойност от сто,
осчетоводяват деветдесет и десет: общата сума на документа излиза вярна, всяка
себестойност по артикул е грешна, и всяка бъдеща себестойност при продажба на
двата артикула е отровена от това. 409 conversion_refused, с details.reason
ui.items.conv_manual_off_total.
Връща 201 с протокола — неговия номер (РАЗК-… за разкомплектоване, КОМП-…
за комплектоване), отчетната стойност, стойността на съпътстващите продукти,
поетия и непоетия разход, абнормения брак, и по един ред на артикул от всяка
страна с role input или output, неговото quantity, unit_cost,
разпределената му value и is_by_product / abnormal_loss_quantity, където
имат смисъл. Σ на value по изходящите редове е равна на total_value до
стотинка, както и Σ по входящите минус абнормения брак плюс поетия разход:
засичането по СС 2, по кабела, а не само в протокола. value е точното число, а
unit_cost се извежда от него — засичайте документа по value.
409 conversion_refused— всичко, което услугата или главната книга откажат: недостатъчно от даден вход на това място, непознат SKU (изходът трябва да е артикул, който книгите вече познават), абнормен брак, по-голям от собствения си ред, разходен ред, чийтоaccount_codeне е активна сметка от клас 6 в собствения сметкоплан на фирмата, базаnormal_capacityбез положителен капацитет или върху документ с повече от един произведен артикул — няма една норма, срещу която да се поеме — и сметкоплан, който не приема записа.details.reasonго има, когато отказът има име.409 period_locked— месецът е подаден.
За разлика от прехвърлянето, преработката осчетоводява — а обикновената преработка не осчетоводява нищо, защото това, което напуска входовете, е точно това, което ляга върху изходите. Записът се появява, когато документът носи нещо, което не е прекласификация: поети разходи за преработка, които влизат в стоката, и абнормен брак, който излиза от нея към сметката за брак — всяко от тях осчетоводено там, където го поставя СС 2. Поправката е обратният документ, който приложението записва; тук няма endpoint за сторниране на преработка.
Стоката влиза през POST /purchases с lines[].product_code и излиза през
продажба. Нито едното, нито другото има нужда от endpoint за наличности.
Книгите
Само четене, и завинаги само четене. Тук няма endpoint, който осчетоводява запис, и не бива да има: главната книга има един-единствен шев за запис, уравновесеността е ограничение в базата, а осчетоводен запис се променя само с друг запис. API, което може да осчетоводява, би било втора врата и в трите.
GET /books/trial-balance?from=&to= — оборотната ведомост. И двете дати са
задължителни, а to не може да е преди from.
{ "data": [ {
"code": "702", "name": "Приходи от продажби на продукция",
"name_en": "Revenue from sales of products", "class": 7, "type": "revenue",
"opening": { "amount": "0.00", "currency": "EUR" },
"debit": { "amount": "0.00", "currency": "EUR" },
"credit": { "amount": "100.00", "currency": "EUR" },
"closing": { "amount": "-100.00", "currency": "EUR" }
} ],
"meta": { "from": "2026-06-01", "to": "2026-06-30", "count": 12,
"totals": { "opening": {…}, "debit": {…}, "credit": {…}, "closing": {…} } } }
Шест числа на сметка, защото това е българската оборотна ведомост: начално салдо, обороти за периода, крайно салдо. Файл само със средната двойка не може да бъде вързан за крайните салда на предходния период, а сметка, която носи салдо, но не е имала движение в прозореца, изобщо би изчезнала от него.
И двете салда са със знак, дебитът е положителен: "-1234.56" по
задължение означава 1234.56 кредит. Браузърът ги разделя в колони Дт и Кт,
защото така се чете и подрежда ведомостта на хартия; читателят на JSON иска
едно число със знак.
Всяко число е пари, в евро — функционалната валута на книгите — включително отрицателните.
GET /books/journal?from=&to= — журналът зад ведомостта, страниран с
курсор.
{ "data": [ {
"id": "01k…",
"entry_id": "01k…", "entry_number": "2026-000123",
"entry_date": "2026-06-10", "entry_status": "posted",
"description": "Фактура 0000000042",
"account_code": "411", "account_name": "Клиенти", "account_name_en": "Trade receivables",
"debit": { "amount": "120.00", "currency": "EUR" },
"credit": { "amount": "0.00", "currency": "EUR" },
"original": null
} ],
"meta": { "next_cursor": "01k…", "limit": 50, "from": "2026-06-01", "to": "2026-06-30" } }
Един ред е една статия, не един запис. Който засича срещу собствената си
главна книга, съпоставя по код на сметка и сума, а вгнездяването на статиите в
записи би направило границата на страница да падне в средата на едно двойно
записване. entry_id и entry_number са на всеки ред, така че прегрупирането
е един ред код за всеки, който иска записите обратно.
Само осчетоводени и сторнирани записи — чернова не е в книгите. original носи
следата в оригиналната валута при валутен запис и е null, когато статията
винаги е била в евро. updated_since се приема за симетрия с всеки друг списък
тук, но осчетоводена статия е неизменяема чрез тригер в базата: тя ще покаже
само новите статии на едно сторниране, никога редакция на стара, защото такива
няма.
Основни данни
GET /counterparties (?search=) и POST /counterparties. Създаването
се съпоставя първо по ЕИК, така че внасянето на един и същ списък с клиенти два
пъти не удвоява всеки контрагент в книгите:
{ "data": { "id": "01k…", "name": "Клиент ЕООД", "created": false } }
201, когато е създало нов, и 200 с created: false, когато е обновило
съвпадението. Задължително: name. По избор: eik, vat_number, address,
country (2 букви), email, iban, kind (client | supplier, по
подразбиране client).
PATCH /counterparties/{id} поправя контрагент, и наистина е PATCH: което
не изпратите, си остава каквото е. Условните правила за цялост се прилагат към
слетия запис, а не само към вашата заявка — фирма пак има нужда от ЕИК, а
физическо лице от идентификатор, независимо дали точно тази заявка е споменала
kind. Приема role (client | supplier), kind
(company | individual), country, name, name_latin, eik,
personal_identifier, vat_number, legal_form, mol_name, address,
email, phone и връща целия контрагент.
kind означава две различни неща по тези два адреса, и си струва да се
назове, преди да ви струва един следобед. При създаването е
client | supplier — какъв ви е контрагентът; стойността ляга върху
флаговете is_client / is_supplier, които четете обратно, а самият запис се
съхранява като company. При PATCH е company | individual — какъв е самият
контрагент — а отношението пътува като role. Запис, създаден през това API,
се PATCH-ва чисто и без двете полета; изпращайте kind при PATCH само когато
наистина прекласифицирате правната форма. Една уговорка: address е незадължителен
при създаването, но задължителен по правилата за слетия запис — запис, създаден
без адрес, трябва да го подаде при първия си PATCH, и 422 го назовава.
- Роля само се добавя, точно както в браузъра: доставчик, който започне да купува, е и двете, а документ, вече осчетоводен срещу старата роля, не бива да остане без нея заради скрипт, който я стеснява.
422 validation_failed, с грешките по полета вdetails.- Няма изтриване и няма сливане, нарочно. И двете унищожават запис, към който сочат документи, а сливането избира кой оцелява — не нещо, на което скрипт може да се вярва в три през нощта.
registry_verifiedе винагиfalseпрез това API и не се записва. Това е твърдение за Търговския регистър, а не поле (§7).
GET /counterparties/{id}/statement?from=&to= — извлечение, документът за
годишното изравняване: какво е фактурирано, какво е платено, какво остава. И
двете дати са задължителни. ?format=csv|pdf (по подразбиране csv),
?lang=bg|en (по подразбиране езикът на притежателя на ключа — извлечението
често отива при контрагента, който може да не чете български).
Байтове, по §4. CSV носи UTF-8 BOM и CRLF, без които Excel показва кирилицата като нечетим текст. Няма JSON форма: това, което услугата изгражда, е подредба на документ — секции, текущо салдо, долен ред, който трябва да излиза — и публикуването ѝ като договор би замразило решение за оформление като обещание към интеграция.
GET /items (?kind=income|expense) и POST /items — каталогът, от
който се попълва ред на фактура. sku по кабела е кодът на артикула, а kind
— неговата посока.
GET /chart-of-accounts — собственият сметкоплан на фирмата, с двете имена,
без страниране (българският примерен сметкоплан е няколкостотин реда и е
ограничен по конструкция). ?active=true|false стеснява; meta.count е общият
брой.
{ "data": [ { "id": "01k…", "code": "702", "name": "Приходи от продажби",
"name_en": "Sales revenue", "class": 7, "type": "revenue",
"is_active": true } ], "meta": { "count": 214 } }
Само четене, и ще си остане така. Сметкопланът е редактируем в приложението, защото човек, който го прави, гледа последиците; скрипт, който добавя или преименува сметка, е на едно изключение от фирма, която вече не може да издаде фактура, защото правилата за осчетоводяване назовават кодове на сметки.
GET /bank-accounts връща сметките с маскиран IBAN — първите четири
знака и последните четири, всичко между тях със звездички. Вие засичате
плащания, не ги нареждате. GET /bank-transactions (?from=, ?to=)
връща осчетоводените редове със сума, име и IBAN на контрагента, reference
(референцията от край до край) и описание.
Подавания
GET /filings (?kind=, ?period=) изброява какво съществува, със
status, due_on, generated_at, submitted_at и nra_incoming_number.
GET /filings/{id}/files изброява файловете на едно подаване:
{ "data": [ { "id": "1", "name": "DEKLAR.TXT", "size": 6 } ],
"meta": { "filing_id": "01k…", "count": 1 } }
id е позицията на файла в списъка с файлове на подаването, и е позиция
нарочно: файловете на подадено подаване са неизменяеми чрез ограничение в
базата, така че нищо не може да ги пренареди и позицията не може да се измести.
size е null, когато редът сочи файл, който вече го няма на диска — съобщено,
а не скрито, защото който засича архив, трябва да различава празен файл от
липсващ.
Списъкът скрива собствения ни доказателствен запис declaration.json: НАП
никога не го иска, а изброяването му би сложило четвърти файл пред човек, който
се кани да качи три. Затова id-тата, които виждате, може да не започват от 0
и да не вървят последователно — четете ги от списъка, не ги брояйте.
Изтеглянето нарочно не прилага този филтър, защото 404 за съхранен файл би било
лъжа към този, който е запазил връзката.
GET /filings/{id}/files/{fileId} подава един, байтове по §4.
И двете са безплатни и достъпни и при план, чието API е изтекло: клиент на
своя таван трябва пак да може да изтегли .txt файловете, които се кани да качи
в НАП. Пак изискват filings:read — безплатното е цена, не разрешение, а
DEKLAR.TXT пътува с POKUPKI.TXT и PRODAGBI.TXT до него, което заедно е целият
дневник за покупки и продажби на фирмата за периода.
POST /filings/{id}/transition записва какво се е случило в НАП.
curl -X POST https://app.autonify.bg/api/v1/filings/01k…/transition \
-H "Authorization: Bearer $AUTONIFY_KEY" \
-H "Idempotency-Key: erp-filing-2026-06-1" \
-H "Content-Type: application/json" \
-d '{ "to": "submitted", "nra_incoming_number": "1234567890" }'
to е submitted, accepted или rejected. nra_incoming_number е
задължително, когато to е submitted. Минава през същата машина на
състоянията като браузъра, съобщава на останалите във фирмата какво е записано
и връща подаването.
Не подава нищо към НАП, не може, и никое право в това API не може. Порталът
е mTLS, като КЕП подписва всяко ръкостискане, така че последната стъпка не се
автоматизира от никого; да се преструваме на обратното в API би било
най-опасната лъжа, която този продукт може да каже. filings:write е право за
водене на отчетност.
409 invalid_transition, когато преходът не е позволен от статуса, в който е
подаването, с details.from и details.to. Същото тяло отново е валидно, щом
подаването стигне статус, от който може да се премести — затова е 409.
409 thin_cap_stale, когато годишна данъчна декларация (gdd) се отбелязва
като submitted, а пренесените лихви по чл. 43 ЗКПО за годината трябва да се
преизчислят: по-ранна година е приключена отново, декларацията е изготвена,
преди пренесените лихви да влязат в нея, или последното приключване на годината
не е завършило. details.key назовава причината. Приключете годината отново и
повторете същата заявка. Същият код се връща и за декларация за година, посочена
като последната, подадена извън Autonify: първо променете този отговор на
страницата „Декларации“. Преместването на подадена декларация в accepted не се
отказва по тази причина.
6. Sandbox
Създайте ключ със среда sandbox.
Кой може да има такъв. Всеки с потвърден имейл адрес. Регистрирайте се, отворете „Разработчици“, създайте ключ. Без покана, без ЕИК, без карта.
Какво чете. Sandbox ключ, издаден по този начин, работи върху примерна фирма, която създаваме за вас: няколко измислени фактури и един контрагент, на същите адреси и със същите отговори като на живо. Нищо, което правите там, не докосва реален документ, и нищо реално не се вижда оттам.
Това е и причината свободното издаване да е безопасно. Преди беше с покана, защото sandbox ключът четеше реалните книги на клиента - тоест всеки, който можеше да си издаде такъв безплатно, можеше да чете данни безплатно. Преместването на данните реши точно това, за което стоеше преградата, затова преградата отпада.
Ако вашият план включва API, sandbox ключовете ви работят върху вашата собствена фирма. Да тествате върху собствените си данни е цялата причина да искате sandbox, а примерна фирма би била стъпка назад.
Реалните ключове са без промяна. За тях все още е нужен план с API или изрично разрешение от клиент, който го има - да ти се вярва да разработваш не е същото като да ти се вярва да работиш на живо - а оттеглянето му спира веднага и вече издадените реални ключове.
Собствен бюджет. Sandbox трафикът не докосва плана на клиента: има отделни броячи и собствен таван, еднакъв за всички.
2 000 единици/ден на фирма 30 000 единици/месец
6 000 единици/ден за акаунта
2 активни sandbox ключа 60 заявки/минута
Размерът е за РАЗРАБОТКА — работен ден писане и повтаряне на интеграция — и съзнателно не за работа на живо. Без нощен пакет, без масов внос, без „засега ще пуснем реалното към теста“. В sandbox няма гратисна лента: тя съществува, защото отказана истинска фактура от доставчик е реален документ, който клиентът губи, а тестовата не е.
atn_test_*се отказва по реалните адреси, аatn_live_*по sandbox адресите — по самата форма на ключа.- Записите в sandbox са истински: номерация, разпознаване на дубликати и разбивка по ДДС се държат точно както ще се държат в реална работа, което е единственото, за което служи един sandbox.
- Sandbox документи никога не се подават към НАП, никога не се изпращат по
имейл и никога не се броят в потреблението или лимитите на клиента.
POST /sales/invoices/{id}/sendвръща403 send_not_in_sandbox, вместо да се преструва.
Какво НЕ е sandbox: изолирана тестова среда. Това е отделна фирма върху същата инсталация, със същия код и същата база. На практика това означава, че тествате точно срещу онова, срещу което ще работите, но също така, че sandbox трафикът е подчинен на същите лимити и същите прозорци за обновяване като всичко останало.
7. Какво това API няма да прави и защо
Списъкът с endpoint-и е обмислен. Не планирайте около появата на тези неща — те липсват по решение, а не защото не са стигнали до реда си:
- Суровият изход на четеца. Може да изпратите файл с покупка; обратно идва вашият собствен запазен документ, никога извлечените стойности, увереността или рамките, и никога синхронно.
- Обогатяване от Търговския регистър. Autonify попълва данните на фирма от регистъра, когато човек въведе ЕИК в приложението. За това няма endpoint и не се предвижда — това не е услуга за масови справки.
- Да изчисли декларация вместо вас. Нито ДДС декларация, нито SAF-T, нито ГДД, нито ГФО. Може да прочетете какво е подадено и да изтеглите файловете, с които е подадено (§5); не може да поискате от Autonify да ги изчисли по заявка.
- Да подаде каквото и да е. Нищо в това API не говори с НАП.
filings:writeзаписва какво е направил човек в портала, а порталът е mTLS, като КЕП подписва всяко ръкостискане — последната стъпка не се автоматизира от никого. - Да подпише с КЕП и да движи пари. Няма endpoint, който подписва документ със сертификата на клиента, и няма такъв, който нарежда плащане. И двете съществуват в приложението, зад човек, и там остават.
- ТРЗ, абонамент, потребители, други фирми. Няма endpoint, няма право.
Какво се промени, и това е разширяване, а не отстъпление. Този раздел
изброяваше „главна книга, оборотна ведомост“ сред липсващите. Не е трябвало:
казаното беше за изчисляването, а се четеше като обещание никога да не се
покажат на клиента собствените му осчетоводени книги. Отказът беше единствената
дупка, която правеше Autonify неизползваем като система за отчетност —
счетоводител не можеше да издърпа оборотна ведомост в собствените си работни
книжа, а данните на клиента се изнасяха само през сваляне в браузър.
books:read я затваря: само четене, изключено по подразбиране, върху
собствената главна книга на клиента и ничия друга. Нищо от горното не е било
преместено, за да ѝ се направи място.
Условията в едно изречение — дългата версия е в Общите условия: може да свържете свой софтуер или софтуера на свой клиент с Autonify; не може да препродавате, преотстъпвате или предоставяте API-то — или каквото и да е, произлязло от него — като услуга на трети лица. Версията, която сте приели, се отпечатва върху ключа при създаването му.
8. Кодове за грешка
| Код | Статус | Какво да направите |
|---|---|---|
unauthenticated |
401 | Изпратете ключа като Bearer token. |
invalid_token |
401 | Грешен ключ или грешна среда за този адрес. |
token_expired / token_revoked |
401 | Ротирайте от Настройки → Developer API. |
token_orphaned |
403 | Човекът, издал ключа, вече не е в акаунта. Издайте нов. |
company_unavailable |
403 | Фирмата е изтрита или делегирането е оттеглено. |
account_suspended |
403 | Акаунтът на клиента е спрян. |
api_not_in_plan |
403 | Планът не включва API. whoami пак отговаря. |
ability_missing |
403 | Ключът не е създаден с това право. |
ability_not_in_plan |
403 | Ключът го има, планът — не. Работи след ъпгрейд, без нов ключ. |
subscription_read_only |
403 | Абонаментът е изтекъл. Всички четения продължават да работят. |
terms_reacceptance_required |
403 | Условията се промениха и гратисният период изтече. Клиентът приема от Настройки → Developer API; съществуващият ви ключ отговаря отново още при следващата заявка. Вижте §1. |
ocr_not_in_plan |
403 | Планът не включва разчитане на документи. Завеждането като JSON работи. |
inventory_not_in_plan |
403 | Планът не включва складовия модул (§ Наличности). Покупките и поръчките продължават да водят складовата аналитичност и себестойността. |
recurring_not_in_plan |
403 | Планът не включва абонаментни фактури, затова не може да се отчита работа за тях. Четенето и изтриването на записи продължават да работят. |
send_not_in_sandbox |
403 | Sandbox документи никога не се изпращат по имейл. |
idempotency_key_required |
400 | Изпращайте по един на всеки POST и PATCH. |
idempotency_key_reused |
422 | Същият ключ, различно тяло. Поправете генерирането на ключове. |
idempotency_in_progress |
409 | Първият опит още се изпълнява. Повторете след малко. |
idempotency_key_invalid |
400 | Ключът е по-дълъг от 255 знака. |
idempotency_key_expired |
409 | Срокът на ключа изтече и не можа да бъде освободен по време на заявката. Повторете или изпратете нов ключ. |
validation_failed |
422 | Тялото не отговаря на договора. details носи грешките по полета. |
not_found |
404 | Няма такъв запис на тази фирма. Никога 403 — вж. §5. |
no_file |
404 | Тази покупка е заведена без файл. |
ubl_not_available |
404 | Този вид документ няма форма на е-фактура. |
ubl_refused |
409 | Редовете на документа вече не възпроизвеждат сумата му. |
duplicate_document |
409 | Вече е заведен. details.existing_id е този, който имате. |
document_posted |
409 | Сумите му са в главната книга. Издайте сторно. |
document_annulled |
409 | Анулиран документ е неизменяем. |
annul_refused |
409 | Анулирането иска осчетоводен документ, или документът вече е кредитиран. |
storno_refused |
409 | Услугата отказа кредитното известие. Съобщението казва защо. |
convert_refused |
409 | Не е проформа или вече е превърната. |
entry_billed |
409 | Записът за свършена работа е в издадена фактура. details.sales_document_id я посочва; поправката е кредитно известие. |
invoice_refused |
409 | Състоянието на фирмата отказа записа. Поправете го и повторете. |
protocol_requires_reverse_charge |
409 | Не е придобиване с обърнато данъчно задължение. |
period_locked |
409 | Счетоводният период е затворен. |
invalid_transition |
409 | Не е позволен от този статус. details.from / details.to. |
thin_cap_stale |
409 | Годишна данъчна декларация, чиито пренесени лихви по чл. 43 трябва да се преизчислят. Приключете годината отново и повторете. details.key. |
no_buyer_email |
409 | Добавете адрес към документа и повторете. |
store_not_push |
409 | Този магазин се синхронизира с дърпане. |
order_refused |
409 | Главната книга отказа поръчката. |
transfer_refused |
409 | Факт за наличността. details.reason го назовава. |
adjustment_refused |
409 | Факт за наличността или основание в спор със знака си. details.reason го назовава. |
count_refused |
409 | Най-често снимка, по-стара от последното броене на това място. details.reason го назовава. |
conversion_refused |
409 | Услугата или главната книга отказа преработката — включително разпределение manual, което не покрива общата стойност. details.reason го назовава, когато отказът има име. |
plan_required |
402 | Няма активен план или пробен период. Задръжте документа; details.retryable е true. |
quota_exceeded |
429 | Изчакайте X-Autonify-Units-Reset. |
send_cooldown / send_daily_limit |
429 | details.retry_after е в секунди. |
api_disabled |
503 | API-то е изключено на тази инсталация. |
9. Известия към ваш адрес (webhooks)
Четири неща си струва да се знаят в мига, в който се случат, а питането за тях струва единици на вас и заявка на нас. Затова може да ви бъде казано.
Адресите се управляват от /developers. До пет на акаунт. Тайната за подписване се показва веднъж, при създаването; ако я загубите, изтрийте адреса и създайте нов.
HTTPS на порт 443, и само публични хостове. Тялото носи id-та на документи на клиент, а заглавката носи HMAC — по обикновен http и двете са четими от всичко по пътя, а подпис, който подслушващият може да копира, не е подпис. Адресът се проверява, когато го запишете, и отново при всяка доставка, защото име, което през март се е разрешавало публично, през април може да сочи 10.0.0.1.
Адресът се създава или закрепен за една фирма — това получава кантора, която го създава, докато е в книгите на клиент — или за всяка фирма на собствения ви акаунт. Не за всяка фирма, до която имате достъп: делегирането е разрешение да свършите работата, а не постоянен абонамент за събитията на клиента.
Събития
| Събитие | Изпраща се, когато | resource.type |
Продължете с |
|---|---|---|---|
sales.invoice.issued |
Документ по чл. 114 е получил номер | sales_invoice |
GET /v1/sales/invoices/{id} |
purchase.upload.finished |
Изпратен от вас файл има присъда | purchase_upload |
GET /v1/purchases/uploads/{id} |
purchase.approved |
Документ от доставчик е одобрен и осчетоводен | purchase |
GET /v1/purchases/{id} |
filing.generated |
Файловете на едно подаване са записани | filing |
GET /v1/filings/{id}/files |
sales.invoice.issued покрива фактури, кредитни и дебитни известия. Проформата
не е данъчен документ, а документът за поръчка (чл. 52о) не носи номер по
чл. 114, затова нито едното не се обявява — който действа по това събитие,
действа върху нещо, което съществува в дневника по ДДС.
purchase.upload.finished се изпраща при всеки изход, включително failed.
Отговорът, който чакате, е също толкова често „не можа да се прочете“, колкото
и „ето документа ви“, а известие само при успех би ви оставило да питате
безкрайно точно в случая, в който питането струва най-много. Ресурсът е
качването, не документ за покупка — при преглед или неуспех документ няма.
По кабела има и пето събитие, ping, което изпраща само бутонът „Тестова
доставка“. За него не може да се абонирате: никой не се абонира за ping, а
адрес, който може, би бил адрес, чиито истински събития никой не проверява.
Неговият resource.type е ping, а resource.id е id-то на самия адрес, така
че никой получател не може да го сбърка с документ, който да отиде да вземе.
Доставката
POST https://hooks.example.bg/autonify
Content-Type: application/json
User-Agent: Autonify-Webhooks/1
X-Autonify-Event: sales.invoice.issued
X-Autonify-Delivery: 01K3H2N7Q9YV6M4W8ZB5R1TCXE
X-Autonify-Signature: t=1785312000,v1=6f1c…a93b
{
"id": "01K3H2N7Q9YV6M4W8ZB5R1TCXE",
"event": "sales.invoice.issued",
"occurred_at": "2026-07-27T12:00:00+03:00",
"company_id": "01K2…",
"resource": { "type": "sales_invoice", "id": "01K3…" }
}
Това е цялото тяло, и тънкостта му е решението. Без суми, без полета на документа, без контрагент. Известието е безплатно, а следващото GET се таксува, така че дебело тяло би било тихо отменяне на измерването — и би направило всеки webhook копие от книгите, което пътува към сървъра на трето лице по наша инициатива, а не по негово искане. Вземете указателя и изтеглете това, което ви трябва.
id е id-то на доставката, не на ресурса, и по него разпознавате
повторенията. То е и в тялото, и в X-Autonify-Delivery, така че може да
отхвърлите повторение, преди да сте разчели каквото и да е.
Проверка на подписа
X-Autonify-Signature: t=<unix секунди>,v1=<hex hmac-sha256>
HMAC се изчислява с тайната на вашия адрес върху низа "{t}.{сурово тяло}".
- Проверявайте върху суровите байтове. Получател, който разчете JSON-а, кодира го наново и хешира това, ще получи различен отговор за същото съобщение: редът на ключовете, екранирането на уникод и изписването на числата се различават между езиците. Вземете тялото точно както е дошло.
- Времевият отпечатък е вътре в подписания низ, а не просто до него, така
че не може да бъде пренаписан по пътя. Налагайте своя собствен прозорец
срещу повторение — пет минути е правилният — като сравнявате
tсъс своя часовник. Ние само поставяме отпечатъка; прозорецът е ваш. v1=е версия. Бъдеща схема ще пътува в същата заглавка до нея, а вашият код ще продължи да четеv1.- Сравнявайте в постоянно време.
hash_equals,crypto.timingSafeEqualили еквивалентът във вашия език.
<?php
$secret = getenv('AUTONIFY_WEBHOOK_SECRET'); // whsec_…
$raw = file_get_contents('php://input'); // СУРОВО, никога json_encode(json_decode(…))
$header = $_SERVER['HTTP_X_AUTONIFY_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts); // t=…&v1=…
$t = (int) ($parts['t'] ?? 0);
$v1 = (string) ($parts['v1'] ?? '');
// Вашият прозорец срещу повторение, не нашият.
if ($t === 0 || abs(time() - $t) > 300) {
http_response_code(400);
exit;
}
if (! hash_equals(hash_hmac('sha256', $t.'.'.$raw, $secret), $v1)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// Разпознавайте повторенията по id-то на ДОСТАВКАТА: повторният опит носи
// оригиналното тяло и нов подпис, така че id-то е единственото постоянно нещо.
if (alreadySeen($event['id'])) {
http_response_code(200);
exit;
}
enqueueForProcessing($event); // после изтеглете ресурса
http_response_code(202);
// Node — тялото трябва да е СУРОВИЯТ буфер:
// app.post('/autonify', express.raw({ type: 'application/json' }), handler)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header).split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // вашият прозорец
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest();
const given = Buffer.from(String(parts.v1 || ''), 'hex');
return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}
Повторни опити и какво струва мъртъв адрес
Успех е 2xx и нищо друго. 3xx не е приемане и никога не се следва: препращането на тяло, подписано за един адрес, към каквото посочи пренасочването би подало събитие на клиент, коректно подписано, към хост, който той никога не е регистрирал.
Отказан опит се повтаря по фиксирана стълбица — 1 минута, 10 минути, 1 час,
6 часа, в рамките на пет опита — след което доставката е мъртва и никой няма
да опита пак. Всеки опит се подписва наново, с нов t, и носи
оригиналното тяло. Затова повторен опит след шест часа е законна доставка с
актуален отпечатък; той не е повторение на стар подпис и вашият прозорец ще го
приеме. Разпознавайте повторенията по id-то на доставката.
Опит, който отнеме повече от 8 секунди, се изоставя. Отговорете първо, обработвайте после.
Десет последователни мъртви доставки изключват адреса, и собствениците на акаунта биват уведомени — интеграция, която е спряла тихо, е по-лоша от такава, която е спряла видимо. Една мъртва доставка вече е пет отказани опита за около осем часа, така че десет от тях не са лош следобед; това е адрес, който не е отговарял с дни. Всеки успех, където и да е, нулира брояча, включително успех по друго събитие: въпросът, на който броячът отговаря, е дали адресът изобщо е жив, а един 200 е пълен отговор на него. Пуснете го отново от /developers, щом отсрещната страна е поправена.
Планът пак решава. Акаунт, чието API е изтекло, запазва записите на адресите си — повторното абониране не бива да е нова интеграция — и междувременно не чува нищо. Безплатни известия към план, който не включва API, биха били измерването, върнато с другата ръка.
Записите за доставките се пазят тридесет дни, което е достатъчно, за да се отговори на „защо не го получих миналата седмица?“. Фактът, който една доставка описва, живее върху самия документ, в книгите, толкова, колкото законът казва.