Изградете собствена връзка
Каталогът, свободните места, поръчките, билетите и сканиранията ви могат да бъдат взети и от друга програма, а тя може и сама да създава поръчки. По-долу е как става това — първо на прост език, после с кода.
- Базов адрес
- https://passavo.eu/api/v1
- Текуща версия
- 2026-09-20
- Самото описание
- openapi.json
Какво е API и нужно ли ви е?
API е врата отзад на вашия акаунт. Там, където вие цъкате в екрана за управление, една програма може да мине през тази врата, за да прочете данните ви или да запише в тях — без някой да седи пред екран.
Нужно ви е само когато искате нещо, което екранът за управление не прави: числа в собствената ви система, свободни места на собствения ви сайт, билети от каса, която вече имате. Ако можете да го цъкнете, цъкането е по-бързо.
Този, който изгражда връзката — вашият уеб разработчик, счетоводният ви софтуер, някой, който програмира — има нужда от две неща: адреса по-долу и ключ, който създавате сами.
Какво можете да изградите
Свободните места на вашия сайт
Покажете на началната си страница кои обиколки още имат места тази седмица, в собствения ви дизайн, с числа, които са верни точно в този момент.
Да захранвате счетоводството
Оставете счетоводителя или счетоводния софтуер сам да вземе продажбите от миналия месец, вместо всеки месец да изпращате експорт.
Екран на входа
Таблет във фоайето, който показва кой часови слот започва и колко места са още свободни. Едно повикване в минута е достатъчно.
Да продавате от собствената си система
Създайте поръчка от програмата, в която вече работите, изпратете на купувача връзка за плащане и научете чрез уебхук, че е платил.
API-то е част от план Про
Ключове създавате в Настройки → API достъп. Този екран се появява в менюто ви веднага щом организацията ви е на този план; сменяте плана сами в екрана за управление, в Абонамент.
Вижте плановете →Първо безопасен опит
Освен обикновен ключ (pv_live_) можете да създадете тестов ключ (pv_test_). Той работи с истинския ви каталог, часови слотове и капацитет и може да създава и поръчки. Такава тестова поръчка никога не минава през доставчик на плащания — тестова страница симулира плащането —, не се брои в оборота ви и се изтрива след 24 часа. Дотогава тя държи истински места, затова работете най-добре с продукт, създаден специално за целта. Това, което е извън пясъчника, като управлението на уебхук адреси, отказваме на тестов ключ с кода sandbox.
Бърз старт в пет стъпки
От нищо до връзка, която знае, че е платено. Всяка стъпка отнема минути, не дни.
-
Създайте ключ
Отворете Настройки → API достъп в екрана за управление и създайте ключ само с правата, които ви трябват. Пълният токен се вижда веднъж; пазете го както пазите парола.
-
Направете първото повикване
Поискайте GET /me с ключа си в заглавката Authorization. Получавате организацията си, плана си и правата на ключа. Ако това работи, работи и останалото.
-
Създайте поръчка
Изпратете редовете за продажба към POST /orders заедно с Idempotency-Key — тук той е задължителен. Ако връзката се скъса по пътя, същото повикване отново не създава втора поръчка. Полето reserved_until казва до кога местата се държат за вас.
-
Насочете купувача към връзката за плащане
Поискайте връзката за плащане с POST /orders/{id}/checkout, заедно с адреса, на който купувачът попада след плащането. Насочете купувача към тази връзка или я покажете в собствената си страница. Плащането минава през доставчика, който вече сте свързали; щом то постъпи, билетите се създават и изпращат.
-
Оставете уебхук да ви извести
Подгответе адрес на своя сайт и го регистрирайте. Щом нещо бъде платено, отменено или сканирано, го получавате там — не е нужно постоянно да питате дали вече се е случило нещо.
curl
curl "https://passavo.eu/api/v1/me" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/me');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/me', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]
Удостоверяване
Всяко запитване носи ключа ви като bearer токен. Един ключ принадлежи на една организация: тази организация е целият свят на запитването и никога не можете по невнимание да вземете чужди данни с него.
Authorization: Bearer pv_live_YOUR_API_KEY
Обикновен ключ започва с pv_live_, тестов — с pv_test_. Този префикс не е тайна, а помощ: който го види в дневник, веднага знае откъде идва.
Пълният токен съществува един миг, при създаването. След това пазим само необратим отпечатък (sha256) от него. Изгубен значи наистина изгубен: отменете ключа и създайте нов.
Права
Ключ без права не може нищо. Това нарочно е обратното на очакваното: тези ключове се раздават от клиенти на чужди инструменти, и тогава стойността по подразбиране трябва да е „нищо“, а не „всичко“.
- read
- Да чете всичко: вашите локации, продукти, цени, свободни места, отстъпки, ваучери и годишни карти, както и поръчките, билетите, купувачите и сканиранията на входа.
- write
- Да създава поръчки, да ги плаща, анулира и възстановява; да обезсилва или изпраща отново билети; да регистрира влизането на посетители; да издава ваучери и годишни карти.
- webhooks
- Да управлява собствените ви уебхук адреси.
- external_payment
- Да отбелязва като платена поръчка, която струва пари, без доставчик на плащания, например след банков превод или плащане в брой (POST /orders/{id}/mark-paid). Отделно право до write, за да го давате само на каса или счетоводство, които управлявате сами. Потвърждаването на поръчка за нула евро става и без него.
Когато нещо се обърка
Всяка грешка има една и съща форма, с фиксиран код и преведен текст. Четете кода, а не текста: текстът може да бъде пренаписан, кодът — никога.
{
"error": {
"code": "validation_failed",
"message": "…",
"details": {}
}
}
| Код | Статус | Кога |
|---|---|---|
| unauthenticated | 401 | Липсващ, непознат, отменен или изтекъл ключ. |
| forbidden | 403 | Ключът е валиден, но не му е позволено това. |
| plan_required | 403 | Планът на тази организация не включва API. |
| sandbox | 403 | Тестов ключ опитва нещо извън пясъчника, например управление на уебхук адреси. |
| not_found | 404 | Не съществува или не съществува в тази организация. |
| validation_failed | 422 | Самото запитване не е вярно: непознат филтър, невалидна дата. |
| rate_limited | 429 | Твърде много повиквания за една минута. |
| conflict | 409 | Същият Idempotency-Key вече е използван за друго. |
| server_error | 500 | Нещо се обърка от наша страна. |
| slot_unavailable | 409 | Часовият слот е пълен, вече е започнал или е непознат, или не е избран, макар продуктът да изисква такъв. |
| sold_out | 409 | Продуктът е изчерпан или вече не се продава. |
| discount_invalid | 422 | Кодът за отстъпка не съществува, е изчерпан или не важи тук. |
| payment_provider_missing | 409 | Организацията няма свързан доставчик на плащания. |
| not_cancellable | 409 | Поръчката не може да бъде отменена в текущото си състояние. |
| not_refundable | 409 | По тази поръчка няма (повече) нищо за възстановяване или тя е платена извън доставчика на плащания. |
| check_in_duplicate | 409 | Този код вече е регистриран на входа. |
| check_in_invalid | 404 | Този код е непознат. |
| check_in_wrong_day | 409 | Този код е валиден, но не за днес. |
| check_in_cancelled | 409 | Този код принадлежи на отменен или възстановен билет. |
Ред от друга организация винаги връща not_found и никога съобщение, че нямате достъп. Ако тази разлика беше видима, всеки с валиден ключ би могъл да разбере колко продукта има съседът, минавайки през номерата.
Страниране, подреждане и филтри
Списъците идват страница по страница. Не искате страница три, а това, което идва след предишната страница: отговорът носи курсор и вие го изпращате обратно, за да четете нататък. Така списъкът никога не пропуска редове, ако нещо се добави, докато прелиствате.
| Параметър | Какво прави |
|---|---|
| page[size] | Колко реда на страница; по подразбиране 25, най-много 100. |
| page[cursor] | Стойността next_cursor от предишния отговор. Ако е празна, това е била последната страница. |
| sort | По какво се подрежда. Тире отпред обръща реда. |
| filter[…] | Филтриране по поле, което endpoint-ът допуска. |
| include | Включване на връзки в същия отговор, разделени със запетаи. |
Параметър, който endpoint-ът не познава, е грешка, а не мълчание. Който сгреши името на филтър, трябва да го научи — а не незабелязано да получи целия списък нефилтриран.
В отговора нарочно няма общ брой: при десетки хиляди редове това преброяване струва повече от самата страница.
{
"data": [ … ],
"meta": { "next_cursor": "…" }
}
Изпратете два пъти, изпълнете веднъж
Изпращайте Idempotency-Key с всяко повикване, което създава или променя нещо — случаен уникален низ. Ако същото повикване дойде пак със същия ключ, получавате точно същия отговор, без нищо да се случи втори път. При POST /orders той е задължителен.
Idempotency-Key: 6f1c0b4a-6b2f-4a1b-9c7e-1f2d3e4a5b6c
Същият ключ с различно съдържание дава conflict и нищо не се случва. Запазен ключ живее 24 часа и важи за организация.
Версии
Версията е в пътя (/api/v1) и се променя само при прекъсване; втора версия тогава работи до нея, а не през нея. Всяко малко допълнение се датира в заглавката Passavo-Version, сега 2026-09-20.
Какво може да се добави в тази версия: поле, endpoint, нова стойност за подреждане или филтър, нов код за грешка. Затова изградете връзката си така, че непознато поле да не я събаря.
Какво никога не се случва в тази версия: премахване или преименуване на поле, промяна на тип, друго значение на код, или задължителен нов параметър.
Ограничение на повикванията
Един ключ може да прави 120 повиквания в минута. Ограничението важи за ключ, а не за адрес, така че двама клиенти зад един и същ облачен доставчик не си пречат.
Всеки отговор казва докъде сте в X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Ако надхвърлите, получавате rate_limited с Retry-After; тогава просто изчакайте толкова секунди.
Примери с код
Едно и също запитване на четири езика, по група endpoint-и. Заменете YOUR_API_KEY със собствения си ключ — и никога не го слагайте в код, който посетител може да свали.
Акаунт
Кой съм аз и какво мога.
GET /api/v1/me
curl
curl "https://passavo.eu/api/v1/me" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/me');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/me', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/me", headers=headers)
data = response.json()["data"]
Каталог
Локации, продукти и цени.
GET /api/v1/events
curl
curl "https://passavo.eu/api/v1/events" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/events');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/events', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/events", headers=headers)
data = response.json()["data"]
Наличност
Часови слотове и свободни места.
GET /api/v1/products/{id}/slots
curl
curl "https://passavo.eu/api/v1/products/12/slots" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/products/12/slots');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/products/12/slots', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/products/12/slots", headers=headers)
data = response.json()["data"]
Продажби
Отстъпки, ваучери и годишни карти.
GET /api/v1/discounts
curl
curl "https://passavo.eu/api/v1/discounts" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/discounts');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/discounts', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/discounts", headers=headers)
data = response.json()["data"]
Поръчки
Създаване, плащане, отменяне и възстановяване на поръчки.
GET /api/v1/orders
curl
curl "https://passavo.eu/api/v1/orders" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/orders');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/orders', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/orders", headers=headers)
data = response.json()["data"]
Билети
Извличане, анулиране и повторно изпращане на билети.
GET /api/v1/tickets
curl
curl "https://passavo.eu/api/v1/tickets" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/tickets');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/tickets', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/tickets", headers=headers)
data = response.json()["data"]
Вход
Чекиране на входа.
GET /api/v1/check-ins
curl
curl "https://passavo.eu/api/v1/check-ins" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/check-ins');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/check-ins', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/check-ins", headers=headers)
data = response.json()["data"]
Клиенти
Купувачи, обобщени от поръчките.
GET /api/v1/customers
curl
curl "https://passavo.eu/api/v1/customers" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/customers');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/customers', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/customers", headers=headers)
data = response.json()["data"]
Мета
Описанието на самото API.
GET /api/v1/openapi.json
curl
curl "https://passavo.eu/api/v1/openapi.json" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->get('https://passavo.eu/api/v1/openapi.json');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/openapi.json', {
method: 'GET',
headers: {
Accept: 'application/json',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json"}
response = requests.get("https://passavo.eu/api/v1/openapi.json", headers=headers)
data = response.json()["data"]
Уебхук
Съобщения, които изпращаме към вашия сървър.
GET /api/v1/webhook-deliveries
curl
curl "https://passavo.eu/api/v1/webhook-deliveries" \
-H "Authorization: Bearer pv_live_YOUR_API_KEY" \
-H "Accept: application/json"
PHP
use Illuminate\Support\Facades\Http;
$response = Http::acceptJson()
->withToken('pv_live_YOUR_API_KEY')
->get('https://passavo.eu/api/v1/webhook-deliveries');
$data = $response->json('data');
JavaScript
const response = await fetch('https://passavo.eu/api/v1/webhook-deliveries', {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: 'Bearer pv_live_YOUR_API_KEY',
},
});
const { data } = await response.json();
Python
import requests
headers = {"Accept": "application/json", "Authorization": "Bearer pv_live_YOUR_API_KEY"}
response = requests.get("https://passavo.eu/api/v1/webhook-deliveries", headers=headers)
data = response.json()["data"]
Уебхукове
Уебхукът е обратното на повикване: вместо вие постоянно да питате дали е платено, ние изпращаме съобщение на ваш адрес в мига, в който се случи. Това спестява хиляди повиквания на ден и научавате веднага, а не на следващия кръг.
Управлявате адресите си в екрана за управление в Настройки → API достъп, раздел Webhooks, или чрез API с ключ, който носи правото webhooks. Всеки адрес има собствена тайна; с нея проверявате, че съобщението наистина идва от нас.
Проверка на подписа
Всяко съобщение носи заглавката Passavo-Signature във вида t=<unix>,v1=<hmac-sha256>. t е моментът, в който сме подписали; v1 е HMAC-SHA256 с тайната на този адрес върху времевия печат, точка и суровото тяло. Пресметнете го сами и сравнявайте за постоянно време — обикновено сравнение издава чрез времето колко знака са били верни. Отхвърляйте и съобщение, чието t се различава с повече от 300 секунди от собствения ви часовник: така никой не може по-късно да повтори прихванато съобщение.
Проверявайте върху СУРОВОТО тяло, преди вашата рамка да го превърне в json. Повторното съставяне на json подрежда ключовете другояче и тогава никой подпис вече не съвпада.
curl
# De handtekening narekenen vanaf de opdrachtregel.
# T is de waarde van t= uit de header Passavo-Signature,
# het resultaat hoort gelijk te zijn aan de waarde van v1=.
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"
PHP
// De RUWE body, niet $request->all().
$body = $request->getContent();
// "t=1758355200,v1=9f86d0…" uit elkaar halen.
$delen = [];
foreach (explode(',', (string) $request->header('Passavo-Signature')) as $stuk) {
[$sleutel, $waarde] = array_pad(explode('=', trim($stuk), 2), 2, '');
$delen[$sleutel] = $waarde;
}
$t = $delen['t'] ?? '';
$verwacht = hash_hmac('sha256', $t . '.' . $body, $secret);
// hash_equals vergelijkt in constante tijd; de tijdstempel
// houdt een opgevangen bericht tegen dat later opnieuw komt.
if (! ctype_digit($t) || abs(time() - (int) $t) > 300 || ! hash_equals($verwacht, $delen['v1'] ?? '')) {
abort(400);
}
JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: de ruwe body als string, bv. via express.raw().
const header = request.headers['passavo-signature'] ?? '';
const delen = Object.fromEntries(header.split(',').map((s) => s.trim().split('=', 2)));
const verwacht = createHmac('sha256', secret).update(delen.t + '.' + rawBody).digest('hex');
const gekregen = delen.v1 ?? '';
const vers = /^\d+$/.test(delen.t ?? '')
&& Math.abs(Date.now() / 1000 - Number(delen.t)) <= 300;
const geldig = vers
&& verwacht.length === gekregen.length
&& timingSafeEqual(Buffer.from(verwacht), Buffer.from(gekregen));
Python
import hashlib, hmac, time
# raw_body: de ruwe body als bytes, bv. request.get_data() in Flask.
header = request.headers.get("Passavo-Signature", "")
delen = dict(stuk.strip().split("=", 1) for stuk in header.split(",") if "=" in stuk)
t = delen.get("t", "")
verwacht = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
# compare_digest vergelijkt in constante tijd.
geldig = (t.isdigit()
and abs(time.time() - int(t)) <= 300
and hmac.compare_digest(verwacht, delen.get("v1", "")))
Пълният списък със събития и тяхното съдържание е в справочника. →
Какво се променя
Всяка промяна, която си струва да бъде спомената, получава нова датирана версия. Ако връзката ви остане на по-стара дата, тя продължава да работи — датата казва само срещу какво поведение е изградена.
-
2026-09-20
Първата версия
Ключове с права, пясъчник, един формат на отговор и страниране с курсор. Четене: вашата организация, локации, продукти, часови слотове, събития, отстъпки, ваучери и годишни карти, поръчки, билети, купувачи и сканирания. Запис: създаване, плащане, анулиране и възстановяване на поръчки, обезсилване на билети, регистриране на влизания, издаване на ваучери и годишни карти. И изходящи уебхук съобщения, подписани за всеки адрес.