Bitta grant ikki bosqichdan o'tadi. Sinxron — so'rovingizga darhol javob: 202 va status (yoki 4xx so'rov xato bo'lsa). Asinxron — imtiyoz boksga keyin yetkaziladi va yakuniy holat relay_status da. accepted «bonus berildi» degani emas — haqiqiy natijani relay_status aytadi (so'rab turing yoki webhook oling). Sinov kaliti hech qachon boksga bormaydi — relay_status: sandbox.A grant moves through two stages. Synchronous — the immediate reply to your request: 202 plus status (or a 4xx if the request is malformed). Asynchronous — the benefit is delivered to the box later, and the final state is in relay_status. accepted does not mean “benefit granted” — the real outcome is in relay_status (poll it, or take a webhook). A sandbox key never reaches a box — relay_status: sandbox.Грант проходит две стадии. Синхронная — немедленный ответ на запрос: 202 и status (или 4xx, если запрос неверен). Асинхронная — льгота доставляется на бокс позже, финальное состояние в relay_status. accepted не значит «льгота выдана» — реальный итог в relay_status (опрашивайте или примите webhook). Тестовый ключ никогда не доходит до бокса — relay_status: sandbox.
Siz haydovchi sizning obyektingizda qancha sarflaganini xabar qilasiz. Biz
buning evaziga necha daqiqa bepul parkovka tegishini hal qilamiz va uni
mashinaning ochiq parkovka sessiyasiga ilamiz.
Siz hech qachon daqiqa yubormaysiz va bizning tariflarimizni bilishingiz shart
emas.
Interaktiv spetsifikatsiya: /swagger/partner/ — alohida login va parol
bilan ochiladi; bu API kaliti EMAS, ikkisini birga yuboramiz
Hammasi JSON. Pul har doim tiyin birligida (1 so'm = 100).
Avval sinov manziliga quring. U alohida muhit: o'z ma'lumoti va o'z
kalitlari bilan — biriga berilgan kalit ikkinchisida ishlamaydi.
1. Kalit olish
Kalitni biz beramiz, har hamkorga alohida. Parking-24.uz dagi aloqachingizga
murojaat qiling va qaysi tizimingizga kerakligini ayting — xaridni xabar
qiladigan POS va takliflarni o'qiydigan mobil ilova alohida kalit oladi,
chunki huquqlar (scope) kompaniyaga emas, kalitga biriktiriladi.
Kalit shunday ko'rinadi:
key_id = pk_7f3a9c21
secret = 4b8e... (64 ta hex belgi)
Maxfiy qismni xavfsiz saqlang. Bizda faqat uning xeshi qoladi, shuning
uchun uni qayta ayta olmaymiz. Yo'qolsa yangi kalit beramiz — eskisini tiklab
bo'lmaydi.
Bir vaqtda ikkita kalit faol bo'la oladi. Almashtirishda shundan foydalaning:
yangi kalitni oling, joylang, keyin eskisini bekor qilishni so'rang. Uzilish
talab qiladigan almashtirishni hech kim qilmaydi.
Har kalit faqat o'ziga berilgan amallarga ishlaydi — yozadigan (POS) va
o'qiydigan (ilova) kalitlar alohida. Agar 403 scope_denied ko'rsangiz,
berilgan kalit shu chaqiruv uchun emas; bizga ayting, to'g'ri kalitni beramiz.
Bitta kalit — barcha nuqtalaringiz uchun. Kalit kompaniyaga tegishli, bitta
parkovkaga emas: har so'rovda venue_id bilan qaysi nuqtaga ekanini
ko'rsatasiz, shu bir kalit hammasiga yozadi. Zaryad shoxobchalari (charger)
bizga ko'rinmaydi — ular sizniki; xohlasangiz attributes ichiga charger_id
qo'shing (biz uni faqat saqlaymiz, qoida o'qimaydi). Ya'ni bitta app-kalit →
ko'p parkovka (venue_id) → har parkovkada ko'p charger.
2. Autentifikatsiya
Har so'rovda:
Authorization: Bearer <key_id>.<secret>
Butun sxema shu — TLS ustidan bitta Bearer token, maxfiy qismi ichida.
Xatolar:
HTTP
error.code
Ma'nosi
401
invalid_client
kalit noma'lum, maxfiy qism xato yoki kalit bekor qilingan
401
key_expired
kalitning amal muddati o'tgan
403
scope_denied
kalit to'g'ri, lekin bu chaqiruv uchun huquqi yo'q
invalid_client «bunday kalit yo'q» bilan «maxfiy qism xato» ni ataylab
ajratmaydi va ikkalasiga bir xil vaqtda javob beradi.
Har chaqiruv kalitning «oxirgi ishlatilgan» vaqtini yangilaydi — uxlab qolgan
kalitni ko'rish uchun.
bu hodisaning sizdagi raqami. Haqiqiy idempotentlik kaliti shu — §4 ga qarang
venue_id
ha
MST-CHL-01
kelishilgan nuqta raqami; qaysi parkovkaga tegishli ekanini belgilaydi
plate_text
ha
01A123AA
haydovchi ilovangizda kiritgan holicha. 6-10 belgi, raqam va A-Z
basis_amount_minor
ha
12000000
haydovchi sarflagan summa, tiyinda. Har doim pul — hech qachon daqiqa emas
occurred_at
ha
2026-09-08T14:31:00Z
RFC3339 — haydovchi haqiqatan sarflagan payt (chek yetib kelgan vaqt emas)
attributes
yo'q
{ "kwh": 18.4 }
yozuvda qolishini xohlagan hamma narsa: kVt·soat, chek raqami, band turgan daqiqa. Qoida buni o'qimaydi — u audit va hisobot uchun saqlanadi
Nega pul, daqiqa emas
Ikki birlik ataylab qat'iy. Siz har doim so'm yuborasiz, biz har doim
daqiqa beramiz. «Bir soat bepul» tarif o'zgarganda ham bir soat bo'lib
qolishi kerak, va siz bizning narxlarimizni kuzatib yurishingiz shart emas.
O'girish qoidasi biz tomonda va uni siz hech narsa joylamasdan o'zgartirish
mumkin.
Raqam normalizatsiya qilinmaydi
Raqamni siz yuborgan holicha solishtiramiz — faqat katta harfga o'giramiz
va bo'shliqni olib tashlaymiz. Tuzatmaymiz. Parkovkada yaqin raqam bo'lsa
(bitta belgi farq), imtiyozni o'sha mashinaga jimgina bermaymiz — yozuv
near_miss_needs_review bilan ushlab turiladi va operator ko'rib chiqadi.
Sababi: raqam ilovangizda bir marta ro'yxatdan o'tadi, ya'ni xato doimiy
bo'ladi. Jim tuzatish uni har tashrifda begona mashinaga berib yurardi.
202 — «qabul qildik va yozdik», «haydovchida bepul daqiqa bor» EMAS.
Parkovkaga yetkazish keyin bo'ladi va u muvaffaqiyatsiz tugashi yoki rad
etilishi mumkin. Haqiqiy natijani relay_status aytadi; uni so'rab turing (§5).
status — bizning birinchi bosqichdagi qarorimiz:
status
message
Ma'nosi
accepted
—
qoida daqiqa berdi, yetkazishga navbatga qo'yildi
rejected
venue_unknown
venue_id sizga tegishli emas
rejected
venue_disabled
nuqta bor, lekin o'chirilgan
accepted
no_benefit_for_amount
summa birinchi pog'onadan past — 0 daqiqa, va bu to'g'ri javob
So'rov xatolari (4xx, {"error":{...}}):
error.code
Sababi
invalid_request
tana o'qilmadi yoki to'g'ri JSON emas
plate_text_required
plate_text yo'q
invalid_plate_format
0-9A-Z dan 6-10 belgi emas
invalid_amount
basis_amount_minor manfiy
occurred_at_required
occurred_at yo'q
invalid_partner_ref
partner_ref yo'q yoki shakli noto'g'ri
4. Xavfsiz qayta urinish
Idempotentlik kaliti — partner_ref. Bir xilini ikki marta yuborsangiz
birinchi natija qaytadi va message da
duplicate partner_ref — returning the original result turadi. Hech narsa
yaratilmaydi va ikki marta sanalmaydi. Status baribir 202.
Idempotency-Key sarlavhasi qabul qilinadi, lekin sizni himoya qiladigan narsa
— partner_ref. Uni haqiqiy hodisa uchun barqaror va yagona qiling; o'z
tranzaksiya raqamingiz eng tabiiy tanlov. Qayta urinishda yangisini
yaratmang.
Tarmoq xatosida va 5xx da qayta urinib ko'ring — 5xx bizning tomondagi
o'tkinchi holat, sizniki emas. Bir necha soniya kuting va o'sha
partner_ref bilan qayta yuboring (idempotentlik ikki marta sanashdan
saqlaydi); to'xtovsiz sikl o'rniga oshib boruvchi kutish qo'ying (masalan
1s → 2s → 4s). Muhimi: 5xx da error.code ga tayanmang — 502/503/504
oldingi proksidan kelib, JSON {error:{…}} konvertini olib kelmasligi mumkin;
faqat status kodiga qarab qayta urinish qarorini bering. 4xx da urinmang:
tana xato va uni takrorlash yordam bermaydi.
Chastota chegarasi
Har kalit uchun daqiqasiga 600 ta so'rov. Byudjet kalitga tegishli,
hamkorga emas — ya'ni kassangiz va mobil ilovangiz bitta byudjet uchun
kurashmaydi.
Chegaradan oshsa 429 keladi, error.code da rate_limited va sarlavhada
Retry-After (sekundlarda). O'shancha kuting — darhol qayta urinish keyingi
oynani ham yoqib yuboradi.
Daqiqasiga 600 — sekundiga o'nta, ya'ni normal integratsiya buni sezmaydi. U
noto'g'ri sozlangan siklni to'xtatish uchun, sizga kvota sotish uchun emas.
5. Natijani o'qish
Natijani o'qish — ixtiyoriy. Minimal integratsiya 202 ni yuborib
qo'yaqolsa bo'ladi (fire-and-forget) — imtiyozni baribir biz yetkazamiz. Bu
yerni so'rab turing yoki webhook oling (§6) faqat yetkazishni tasdiqlamoqchi
bo'lsangiz — masalan haydovchiga bepul parking ilanganini ko'rsatish uchun.
GET /partner/v1/benefits/grants/{partner_ref}
GET /partner/v1/benefits/grants?venue_id=&plate_text=&relay_status=&page=&page_size=
Siz faqat o'z yozuvlaringizni ko'rasiz: hamkor kodi tokendan olinadi, so'rovdan
emas.
Muhim maydon — relay_status:
relay_status
Ma'nosi
Yakunimi?
pending
navbatda, yoki parkovkaga yetib bo'lmadi va qayta urinladi
yo'q
applied
parkovka daqiqalarni ochiq sessiyaga iladi
ha
rejected
parkovka rad etdi — relay_reason ga qarang
ha
pending_operator
parkovkada odam qaroriga qo'yildi
siz uchun ha; natija biz tomonda hal bo'ladi
expired
qayta urinish oynasida yetkazilmadi
ha
not_applicable
yetkaziladigan narsa yo'q — so'rovni biz o'zimiz rad etdik; status va message ni o'qing
ha
sandbox
sinov kaliti, ya'ni parkovkaga umuman yuborilmaydi (§9)
ha
Yakuniy BO'LMAGAN yagona qiymat — pending. So'rab turadigan bo'lsangiz,
faqat shu qiymat uchun qaytish ma'noli.
Parkovka rad etganda relay_reason:
Sabab
Ma'nosi
parking_session_not_found
mashina hozir ichkarida emas
near_miss_needs_review
o'xshash raqam ichkarida; operator tasdiqlashi kerak
session_cap_reached
bu tashrifda bepul daqiqa chegarasi to'lgan
venue_unknown
parkovka bu nuqtani konfiguratsiyasida hali olmagan
sponsor_balance_exhausted
qoplovchi balans bo'sh; operator qaroriga qo'yildi
location_unknown, tenant_resolution_failed
marshrutlash hal bo'lmadi — bizga ayting
relay_ttl_exceeded
parkovka qayta urinish oynasi davomida yetib bo'lmas qoldi
Rad etish — normal natija, xato emas. Eng ko'p uchraydigani
parking_session_not_found: haydovchi shunchaki hozir bizda turgan emas. Bunga
ogohlantirish qo'ymang.
So'rash o'rniga xabar olish
Bizga HTTPS manzil bering — grant yakuniy relay_status ga yetganda o'sha
manzilga POST qilamiz:
Bu tana — turtki, ma'lumot emas. Unda ishonib saqlaydigan hech narsa
yo'q: grantni yuqoridagi so'rov bilan qayta o'qing va haqiqat deb o'shani
oling.
Shuning uchun imzo yo'q: xabarda soxtalashtirishga arziydigan narsa yo'q va
takrorlangan yoki soxta chaqiruv hujumchiga hech nima bermaydi. Shu sababdan
yo'qolgan xabar ham ma'lumot yo'qolishi emas — siz baribir so'rab bilasiz.
Har qanday 2xx bilan javob bering. Qolganida besh marta qayta urinamiz,
keyin to'xtatamiz va o'z tomonimizda belgilaymiz. Takrorni qabul qiling:
u dizayn bo'yicha zararsiz.
Rad etilgan yozuvlar saqlanadi va ularni o'qish foydali: hech qachon mos
kelmaydigan raqam — odatda ilovangizda noto'g'ri ro'yxatdan o'tgan raqam.
6. Takliflarni ko'rsatish
GET /partner/v1/offers?parking_uid=PRK-AIRPORT-1 (scope: offers.read)
GET /partner/v1/parkings?page=1&page_size=50 (scope: parkings.read)
parking_uid — bizda allaqachon ishlatadigan ommaviy lokatsiya raqami, yangi
identifikator emas.
Javob — tuzilgan ma'lumot, tayyor matn emas: hamkor nomi, summa-daqiqa
qoidasi va bitta tashrifdagi chegara. Uni o'z tilingizda va o'z ohangingizda
chiqarasiz.
Shaxsiy progress yo'q. «Siz 80 000 sarfladingiz, yana 20 000 qoldi» deb
ayta olmaymiz — buning uchun sizning jonli xarid holatingiz kerak, u esa bizda
yo'q. Biz taklifni ko'rsatamiz, progressni siz ko'rsatasiz.
E'lon qilish har nuqta uchun alohida va sukut bo'yicha yopiq. Hamkor o'z
taklifini uchinchi tomon ilovasida ko'rsatishni istamasligi mumkin, shuning
uchun nuqta ochiq deb belgilanmaguncha bu yerda hech narsa chiqmaydi.
Noma'lum parking_uid uchun bo'sh ro'yxat qaytadi, 404 emas — katalogingdagi
har parkovka uchun so'rasang ham loglaring xatoga to'lmaydi.
GET /parkings har qatorda has_offers beradi, shunda ro'yxatda belgi qo'yish
uchun har parkovkaga alohida /offers chaqirish kerak bo'lmaydi.
7. Versiya siyosati
/v1/ barqaror. Buzuvchi o'zgarishda /v2/ ochiladi va v1 kelishilgan muddat
ishlab turadi.
Maydon qo'shish buzuvchi emas. Olib tashlash yoki ma'nosini o'zgartirish —
buzuvchi.
8. Xatolar
Har xato bir xil konvertda:
{ "error": { "code": "scope_denied", "message": "this key does not carry benefits.write", "trace_id": "..." } }
Mantiqni code bo'yicha quring. message odam uchun, matni o'zgarishi
mumkin va umuman bo'lmasligi ham mumkin — bir qancha xatolar, jumladan
invalid_client va invalid_plate_format, faqat code va trace_id bilan
keladi. Yo'qligini bo'sh satr deb oling va uni haydovchiga xom ko'rsatmang.
trace_id siz yuborgan X-Trace-Id sarlavhasini qaytaradi. Uni yuboring —
bizga muammo haqida aytganingizda aynan shu qiymat kerakli so'rovni topishga
imkon beradi. Yubormasangiz maydon bo'sh qaytadi.
9. Prodga chiqish
Sizga sandbox deb belgilangan kalit beramiz va siz shunga qurasiz.
venue_id ro'yxati va o'girish qoidasi kelishiladi.
/offers da ko'rinishini xohlagan nuqtalaringizni e'lon qilamiz.
Prod kaliti beriladi, siz joylaysiz, biz sandbox kalitini bekor qilamiz.
Sandbox kaliti nima qiladi
Sandbox kaliti bilan yozilgan fakt haqiqiy parkovkaga hech qachon
yetkazilmaydi. Qolgan hammasi haqiqiy: kalit tekshiriladi, tana
validatsiyadan o'tadi, qoida ishlaydi va haqiqiy daqiqa qaytaradi,
partner_ref idempotentligi ham prodagi kabi. Faqat oxirgi bo'g'in
ulanmaydi.
Ajratish oson: sandbox granti javobida "sandbox": true va yakuniy
relay_status — sandbox bo'ladi. U hech qachon applied bo'lmaydi va
muddati ham o'tmaydi.
Ya'ni butun shartnomani — autentifikatsiya, huquqlar, validatsiya xatolari,
pog'ona hisobi, qayta urinish — haqiqiy mashinaga bepul vaqt berish xavfisiz
sinab ko'rasiz.
Alohida sandbox manzili yo'q: bir xil manzil, boshqa kalit.
Namunalar — curl bilan har bir holat
Quyida har bir holat: nima yuborasiz va nima qaytadi — dev muhitida jonli olingan. O'z javoblaringizni shu bilan solishtiring. Prod bir xil, faqat host api.parking-24.uz. $KEY — yozuvchi kalitingiz (benefits.write), $READ_KEY — o'quvchi (benefits.read+…). Har javob siz yuborgan X-Trace-Id ni qaytaradi (bu yerda qisqalik uchun tushirilgan).
Interactive spec: /swagger/partner/ — opens with a separate username and
password; that is not your API key, and we send you both together
Everything is JSON. Money is always in minor units (1 so'm = 100).
Build against the test host first. It is a separate environment with its
own data and its own keys — a key issued for one does not work on the other.
1. Getting a key
Keys are issued by us, per partner. Ask your Parking-24.uz contact and say which
of your systems needs one — the POS that reports purchases and the mobile app
that reads offers get separate keys, because scopes are attached to the
key, not to your company.
Keep the secret safe. We store only its hash, so we cannot tell it to you
again. If it is lost we issue a new key — we cannot recover the old one.
Two keys can be active at the same time. Use that when rotating: create the
new key, deploy it, then ask us to revoke the old one. A rotation that
requires downtime is a rotation nobody performs.
Each key works only for the actions it was issued for — the writing (POS) and
reading (app) keys are separate. If you get 403 scope_denied, the key you were
given is not for that call; tell us and we will issue the right one.
One key covers all your venues. The key belongs to your company, not to a
single parking: each request names its venue_id, and that one key writes to
all of them. Chargers are invisible to us — they are yours; put a charger_id
in attributes if you want (we only store it, the rule never reads it). So
one app key → many parkings (venue_id) → many chargers per parking.
2. Authentication
Every request carries:
Authorization: Bearer <key_id>.<secret>
That is the whole scheme — a single Bearer token, secret included, over TLS.
Failures:
HTTP
error.code
Meaning
401
invalid_client
key id unknown, secret wrong, or key revoked
401
key_expired
the key passed its expiry date
403
scope_denied
the key is valid but lacks the scope for this call
invalid_client deliberately does not distinguish "no such key" from "wrong
secret", and both take the same time to answer.
Every call updates the key's last seen timestamp, so we can spot keys that
have gone quiet.
your id for this event. This is the real idempotency key — see §4
venue_id
yes
MST-CHL-01
the venue id we agreed with you; identifies which parking it belongs to
plate_text
yes
01A123AA
as the driver entered it in your app. 6-10 chars, digits and A-Z
basis_amount_minor
yes
12000000
what the driver spent, in minor units. Always money — never minutes
occurred_at
yes
2026-09-08T14:31:00Z
RFC3339 — when the driver actually spent (not when the receipt reached you)
attributes
no
{ "kwh": 18.4 }
anything you want on the record: kWh, receipt number, idle minutes. The rule never reads it — it is kept for audit and reporting
Why money and not minutes
Two units are fixed on purpose. You always send so'm; we always grant
minutes. "One free hour" must stay one hour when our tariff changes, and
you should not have to track our pricing. The conversion rule lives on our
side and can be changed without you deploying anything.
The plate is not normalised
We match the plate exactly as you send it, uppercased and trimmed. We do
not repair it. If a nearby plate is in the parking (edit distance 1) we do
not silently grant the benefit to that car — the grant is held with
near_miss_needs_review and an operator looks at it.
The reason is that the plate is registered once in your app, so a typo is
permanent: silent repair would hand the benefit to a neighbouring car on
every single visit.
202 means "accepted and recorded" — not "the driver has free minutes".
Delivery to the parking happens afterwards and can still fail or be refused.
relay_status is what tells you the real outcome; poll it (§5).
status is our first-stage decision:
status
message
Meaning
accepted
—
the rule produced minutes; queued for delivery
rejected
venue_unknown
venue_id is not one of yours
rejected
venue_disabled
the venue exists but is switched off
accepted
no_benefit_for_amount
the amount is below the first tier — 0 minutes, and that is a valid answer
Request errors (4xx, {"error":{...}}):
error.code
Cause
invalid_request
body is not readable or not valid JSON
plate_text_required
plate_text missing
invalid_plate_format
not 6-10 chars of 0-9A-Z
invalid_amount
basis_amount_minor is negative
occurred_at_required
occurred_at missing
invalid_partner_ref
partner_ref missing or malformed
4. Retrying safely
partner_ref is the idempotency key. Send the same one twice and you get
the first result back, with message set to
duplicate partner_ref — returning the original result. Nothing is created
and nothing is double-counted. The status stays 202.
Idempotency-Key is accepted as a header for client-side retry bookkeeping,
but partner_ref is what actually protects you. Make it stable and unique per
real-world event — your own transaction id is the natural choice. Do not
generate a fresh one on retry.
Retry on network errors and on 5xx — a 5xx is a transient fault on our
side, not yours. Wait a few seconds and resend with the same partner_ref
(idempotency keeps it from counting twice); back off progressively rather than
hammering (e.g. 1s → 2s → 4s). Important: do not parse error.code on a
5xx — 502/503/504 come from the front proxy and may not carry the JSON
{error:{…}} envelope; decide to retry from the status code alone. Do not
retry on 4xx: the body is wrong and repeating it will not help.
Rate limit
Each key gets 600 requests per minute. The budget is per key, not per
partner, so your POS and your mobile app do not compete for the same one.
Over the limit you get 429 with an error.code of rate_limited and a
Retry-After header in seconds. Wait that long — retrying immediately just
burns the next window.
600/min is ten per second: a normal integration never notices it. It exists to
stop a misconfigured loop, not to sell you a quota.
5. Reading the outcome
Reading the outcome is optional. A minimal integration can fire-and-forget
the 202 — we deliver the benefit regardless. Poll here, or take a webhook
(§6), only when you want to confirm delivery — for example to show the driver
that free parking was applied.
GET /partner/v1/benefits/grants/{partner_ref}
GET /partner/v1/benefits/grants?venue_id=&plate_text=&relay_status=&page=&page_size=
You only ever see your own rows: the partner is taken from the token, not
from the query.
relay_status is the field that matters:
relay_status
Meaning
Final?
pending
queued, or the parking was unreachable and it will be retried
no
applied
the parking attached the minutes to an open session
yes
rejected
the parking refused it — see relay_reason
yes
pending_operator
held for a human decision at the parking
yes for you; the outcome is settled on our side
expired
never delivered within the retry window
yes
not_applicable
nothing to deliver — we rejected the request ourselves; read status and message
yes
sandbox
a sandbox key, so it is never sent to a parking (§9)
yes
pending is the ONLY non-final value. If you are polling, that is the only
one worth coming back for.
relay_reason when the parking refuses:
Reason
Meaning
parking_session_not_found
the car is not currently inside
near_miss_needs_review
a similar plate is inside; an operator must confirm
session_cap_reached
this visit already has the maximum free minutes
venue_unknown
the parking has not yet received this venue in its config
sponsor_balance_exhausted
the funding balance is empty; held for an operator
location_unknown, tenant_resolution_failed
routing could not be resolved — tell us
relay_ttl_exceeded
the parking stayed unreachable past the retry window
A rejection is a normal outcome, not an error. The commonest one by far is
parking_session_not_found — the driver simply is not parked with us right
now. Do not alert on it.
Getting told instead of polling
Give us an HTTPS URL and we will POST to it when a grant reaches a final
relay_status:
That body is a nudge, not data. It carries nothing you should trust or
store — read the grant back with the call above and treat that as the truth.
That is why there is no signature: there is nothing in the message worth
forging, and a replayed or spoofed call gains an attacker nothing. It is also
why a lost callback is not lost data — you can always poll.
Answer with any 2xx. We retry five times on anything else, then stop and
flag it on our side. Handle duplicates: a repeat is harmless by design.
Rejected rows are kept, and they are worth reading: a plate that never
matches is usually a plate registered wrongly in your app.
6. Showing offers
GET /partner/v1/offers?parking_uid=PRK-AIRPORT-1 (scope: offers.read)
GET /partner/v1/parkings?page=1&page_size=50 (scope: parkings.read)
parking_uid is the public location id you already use with us — no new
identifier.
The response is structured data, not ready-made text: partner name, the
sum-to-minutes rule, and the per-visit cap. Render it in your own language and
tone.
No personal progress. We cannot tell a driver "you spent 80 000, 20 000 to
go" — that needs your live purchase state, which we do not hold. We show the
offer; the progress is yours to show.
Publication is per venue and closed by default. A partner may not want its
offer surfaced in a third-party app, so nothing appears here until that venue
is explicitly published.
An unknown parking_uid returns an empty list, not 404 — you can ask for
every parking in your catalogue without filling your logs with errors.
GET /parkings returns has_offers per row, so you can flag rows in a list
without one /offers call per parking.
7. Versioning
/v1/ is stable. A breaking change opens /v2/ and v1 keeps running for an
agreed period.
Adding a field is not a breaking change. Removing one, or changing what it
means, is.
8. Errors
Every error uses the same envelope:
{ "error": { "code": "scope_denied", "message": "this key does not carry benefits.write", "trace_id": "..." } }
Branch on code. message is for humans, may be reworded, and may be
absent entirely — several errors, invalid_client and
invalid_plate_format among them, carry only code and trace_id. Treat a
missing message as an empty string; never show it raw to a driver.
trace_id echoes the X-Trace-Id header you send. Send one — when you report
a problem to us, that value is what lets us find the exact request. If you do
not send it, the field comes back empty.
9. Going live
We issue a key marked as sandbox and you build against it.
We agree the venue_id list and the conversion rule.
We publish the venues you want visible in /offers.
We issue the production key; you deploy it and we revoke the sandbox one.
What a sandbox key does
A grant reported with a sandbox key is never delivered to a real parking.
Everything else is real: your key is authenticated, the body is validated, the
conversion rule runs and returns actual minutes, and partner_ref idempotency
behaves exactly as in production. Only the last link is left unconnected.
You can tell them apart: a sandbox grant comes back with "sandbox": true and
a terminal relay_status of sandbox. It never becomes applied, and it
never expires.
So you can build and test the whole contract — auth, scopes, validation
errors, tier maths, retries — without risking free parking for a real car.
There is no separate sandbox host: the same base URL, a different key.
Examples — every case with curl
For each case: what you send and what comes back — captured live on the test host. Match your own responses against these. Production is identical, only the host is api.parking-24.uz. $KEY is your write key (benefits.write), $READ_KEY a read key (benefits.read+…). Every response echoes the X-Trace-Id you send (omitted here for brevity).
Вы сообщаете, сколько водитель потратил на вашем объекте. Мы решаем,
сколько бесплатных минут парковки это даёт, и привязываем их к открытой
парковочной сессии автомобиля.
Вы никогда не присылаете минуты и вам не нужно знать наши тарифы.
Интерактивная спецификация: /swagger/partner/ — открывается по отдельному
логину и паролю; это не ваш API-ключ, мы присылаем и то и другое
Всё в JSON. Деньги всегда в минорных единицах (1 сум = 100).
Сначала разрабатывайте на тестовом адресе. Это отдельная среда со своими
данными и своими ключами — ключ от одной не работает в другой.
1. Получение ключа
Ключи выдаём мы, отдельно каждому партнёру. Обратитесь к вашему контакту в
Parking-24.uz и скажите, какой из ваших систем нужен ключ: касса, сообщающая о
покупках, и мобильное приложение, читающее предложения, получают разные
ключи — права (scope) привязаны к ключу, а не к компании.
Храните секрет надёжно. У нас хранится только его хеш, поэтому назвать его
повторно мы не сможем. Если он потерян — выдадим новый ключ, старый
восстановить нельзя.
Одновременно могут быть активны два ключа. Пользуйтесь этим при ротации:
создайте новый, разверните, затем попросите отозвать старый. Ротацию, которая
требует простоя, никто не делает.
Каждый ключ работает только для тех действий, для которых выдан — ключи на
запись (POS) и на чтение (приложение) отдельные. Если вы получили
403 scope_denied, выданный ключ не для этого вызова; сообщите нам, и мы
выдадим правильный.
Один ключ — на все ваши объекты. Ключ принадлежит компании, а не одной
парковке: в каждом запросе вы указываете venue_id, и этот один ключ пишет во
все. Зарядные станции нам не видны — они ваши; при желании положите
charger_id в attributes (мы только храним его, правило его не читает). То
есть один app-ключ → много парковок (venue_id) → много зарядок на
парковке.
2. Аутентификация
В каждом запросе:
Authorization: Bearer <key_id>.<secret>
Это вся схема — один Bearer-токен с секретом внутри, поверх TLS.
Ошибки:
HTTP
error.code
Значение
401
invalid_client
ключ неизвестен, секрет неверен или ключ отозван
401
key_expired
срок действия ключа истёк
403
scope_denied
ключ верен, но не имеет права на этот вызов
invalid_client намеренно не различает «такого ключа нет» и «секрет неверен»,
и отвечает на оба за одинаковое время.
Каждый вызов обновляет отметку «последнее использование» — чтобы видеть
уснувшие ключи.
ваш идентификатор события. Это и есть реальный ключ идемпотентности — см. §4
venue_id
да
MST-CHL-01
согласованный с нами идентификатор объекта; определяет, к какой парковке он относится
plate_text
да
01A123AA
как водитель ввёл в вашем приложении. 6-10 символов, цифры и A-Z
basis_amount_minor
да
12000000
сколько потратил водитель, в минорных единицах. Всегда деньги — никогда не минуты
occurred_at
да
2026-09-08T14:31:00Z
RFC3339 — когда водитель действительно потратил (а не когда чек дошёл до вас)
attributes
нет
{ "kwh": 18.4 }
всё, что хотите сохранить в записи: кВт·ч, номер чека, минуты простоя. Правило это не читает — хранится для аудита и отчётности
Почему деньги, а не минуты
Две единицы зафиксированы намеренно. Вы всегда присылаете сумы, мы всегда
выдаём минуты. «Один час бесплатно» должен остаться часом и при смене
тарифа, а вам не нужно следить за нашими ценами. Правило пересчёта — на нашей
стороне, и его можно изменить без выкладки на вашей.
Номер не нормализуется
Мы сравниваем номер ровно так, как вы его прислали — только верхний
регистр и обрезка пробелов. Мы его не исправляем. Если на парковке есть
похожий номер (отличие в один символ), мы не выдаём льготу этой машине
молча: запись удерживается с near_miss_needs_review, и её смотрит оператор.
Причина: номер регистрируется в вашем приложении один раз, поэтому опечатка
постоянна. Молчаливое исправление отдавало бы льготу чужой машине при
каждом визите.
202 означает «приняли и записали», а НЕ «у водителя есть бесплатные
минуты». Доставка на парковку происходит позже и может не пройти или быть
отклонена. Реальный итог показывает relay_status; опрашивайте его (§5).
status — наше решение на первом шаге:
status
message
Значение
accepted
—
правило дало минуты, поставлено в очередь на доставку
rejected
venue_unknown
venue_id вам не принадлежит
rejected
venue_disabled
объект есть, но выключен
accepted
no_benefit_for_amount
сумма ниже первой ступени — 0 минут, и это корректный ответ
Ошибки запроса (4xx, {"error":{...}}):
error.code
Причина
invalid_request
тело не читается или не является корректным JSON
plate_text_required
нет plate_text
invalid_plate_format
не 6-10 символов из 0-9A-Z
invalid_amount
basis_amount_minor отрицательный
occurred_at_required
нет occurred_at
invalid_partner_ref
partner_ref отсутствует или неверного формата
4. Безопасные повторы
Ключ идемпотентности — partner_ref. Отправите тот же дважды — вернётся
первый результат, а в message будет
duplicate partner_ref — returning the original result. Ничего не создаётся и
не считается дважды. Статус остаётся 202.
Заголовок Idempotency-Key принимается, но защищает вас именно partner_ref.
Делайте его стабильным и уникальным на реальное событие — ваш собственный
идентификатор транзакции подходит лучше всего. Не генерируйте новый при
повторе.
Повторяйте при сетевых ошибках и 5xx — 5xx это временный сбой на нашей
стороне, не на вашей. Подождите несколько секунд и отправьте снова с тем же
partner_ref (идемпотентность не даст засчитать дважды); наращивайте паузу,
а не долбите подряд (например 1s → 2s → 4s). Важно: не полагайтесь на
error.code при 5xx — 502/503/504 приходят от фронтового прокси и могут
не нести JSON-конверт {error:{…}}; решение о повторе принимайте только по
коду статуса. При 4xx не повторяйте: тело неверно, и повтор не поможет.
Лимит частоты
На каждый ключ — 600 запросов в минуту. Бюджет привязан к ключу, а не к
партнёру: ваша касса и мобильное приложение не делят один лимит.
При превышении приходит 429, в error.code — rate_limited, а в заголовке
Retry-After (в секундах). Подождите столько — немедленный повтор просто
сожжёт следующее окно.
600 в минуту — это десять в секунду: нормальная интеграция этого не замечает.
Лимит нужен, чтобы остановить зациклившуюся интеграцию, а не чтобы продавать
вам квоту.
5. Чтение результата
Чтение итога — необязательно. Минимальная интеграция может просто
отправить 202 и забыть (fire-and-forget) — льготу мы доставим в любом
случае. Опрашивайте здесь или примите webhook (§6) только если хотите
подтвердить доставку — например показать водителю, что бесплатная парковка
применена.
GET /partner/v1/benefits/grants/{partner_ref}
GET /partner/v1/benefits/grants?venue_id=&plate_text=&relay_status=&page=&page_size=
Вы видите только свои записи: партнёр берётся из токена, а не из запроса.
Главное поле — relay_status:
relay_status
Значение
Финально?
pending
в очереди либо парковка была недоступна и попытка повторится
нет
applied
парковка привязала минуты к открытой сессии
да
rejected
парковка отказала — см. relay_reason
да
pending_operator
передано на решение человека на парковке
для вас да; итог решается на нашей стороне
expired
не доставлено в течение окна повторов
да
not_applicable
доставлять нечего — мы отклонили запрос сами; читайте status и message
да
sandbox
sandbox-ключ, поэтому на парковку не отправляется (§9)
да
Единственное НЕ финальное значение — pending. Если вы опрашиваете, только
за ним и стоит возвращаться.
relay_reason при отказе парковки:
Причина
Значение
parking_session_not_found
машины сейчас нет внутри
near_miss_needs_review
внутри похожий номер; нужно подтверждение оператора
session_cap_reached
на этот визит лимит бесплатных минут уже исчерпан
venue_unknown
парковка ещё не получила этот объект в конфигурации
sponsor_balance_exhausted
баланс покрытия пуст; передано оператору
location_unknown, tenant_resolution_failed
маршрутизация не разрешилась — сообщите нам
relay_ttl_exceeded
парковка оставалась недоступной дольше окна повторов
Отказ — нормальный итог, а не ошибка. Самый частый —
parking_session_not_found: водитель просто сейчас не стоит у нас. Не ставьте
на него алерт.
Уведомление вместо опроса
Дайте нам HTTPS-адрес — мы отправим на него POST, когда начисление придёт к
терминальному relay_status:
Это тело — сигнал, а не данные. В нём нет ничего, чему стоит доверять или
что стоит сохранять: перечитайте начисление запросом выше и считайте истиной
именно его.
Поэтому здесь нет подписи: в сообщении нечего подделывать, а повтор или
подделка ничего не дают атакующему. По той же причине потерянное уведомление —
не потеря данных: вы всегда можете опросить.
Отвечайте любым 2xx. На всё остальное мы повторим пять раз, затем остановимся
и отметим это у себя. Будьте готовы к дубликатам: повтор безвреден по замыслу.
Отклонённые записи сохраняются, и их полезно читать: номер, который никогда
не совпадает, обычно неверно зарегистрирован в вашем приложении.
6. Показ предложений
GET /partner/v1/offers?parking_uid=PRK-AIRPORT-1 (scope: offers.read)
GET /partner/v1/parkings?page=1&page_size=50 (scope: parkings.read)
parking_uid — тот же публичный идентификатор локации, который вы уже
используете. Новый идентификатор не нужен.
Ответ — структурированные данные, а не готовый текст: имя партнёра,
правило «сумма → минуты» и лимит на визит. Вы выводите это своим языком и в
своей интонации.
Личный прогресс не отдаём. Сказать водителю «вы потратили 80 000, осталось
20 000» мы не можем — для этого нужно ваше актуальное состояние покупок,
которого у нас нет. Мы показываем предложение, прогресс показываете вы.
Публикация — по объектам и по умолчанию закрыта. Партнёр может не хотеть,
чтобы его предложение показывалось в стороннем приложении, поэтому здесь
ничего не появится, пока объект не опубликован явно.
Неизвестный parking_uid возвращает пустой список, а не 404 — можно
спрашивать по всем парковкам вашего каталога, не засоряя логи ошибками.
GET /parkings отдаёт has_offers в каждой строке, чтобы отмечать строки в
списке без отдельного /offers на каждую парковку.
7. Версионирование
/v1/ стабилен. Ломающее изменение открывает /v2/, а v1 работает
согласованный срок.
Добавление поля не является ломающим изменением. Удаление или изменение
смысла — является.
8. Ошибки
Все ошибки в одном конверте:
{ "error": { "code": "scope_denied", "message": "this key does not carry benefits.write", "trace_id": "..." } }
Стройте логику по code. message — для человека, формулировка может
меняться, и его может не быть вовсе: ряд ошибок, в том числе
invalid_client и invalid_plate_format, приходят только с code и
trace_id. Считайте отсутствие пустой строкой и не показывайте его
водителю как есть.
trace_id возвращает присланный вами заголовок X-Trace-Id. Присылайте его —
именно это значение позволяет нам найти конкретный запрос, когда вы сообщаете
о проблеме. Если не прислать, поле вернётся пустым.
9. Выход в прод
Мы выдаём ключ, помеченный как sandbox, и вы разрабатываете на нём.
Согласуем список venue_id и правило пересчёта.
Публикуем объекты, которые вы хотите видеть в /offers.
Выдаём боевой ключ, вы его разворачиваете, мы отзываем sandbox-ключ.
Что делает sandbox-ключ
Факт, отправленный sandbox-ключом, никогда не доставляется на реальную
парковку. Всё остальное настоящее: ключ проверяется, тело валидируется,
правило пересчёта выдаёт реальные минуты, идемпотентность по partner_ref
работает как в проде. Не подключено только последнее звено.
Отличить просто: у sandbox-начисления в ответе "sandbox": true, а
relay_status — терминальный sandbox. Оно никогда не станет applied и
никогда не истечёт.
То есть вы можете отработать весь контракт — аутентификацию, права, ошибки
валидации, расчёт по ступеням, повторы — не рискуя выдать бесплатную парковку
реальной машине.
Отдельного sandbox-хоста нет: тот же адрес, другой ключ.
Примеры — каждый случай через curl
Для каждого случая: что вы отправляете и что приходит — снято вживую на тестовом хосте. Сверяйте свои ответы с этими. Прод идентичен, отличается только хост api.parking-24.uz. $KEY — ваш ключ записи (benefits.write), $READ_KEY — ключ чтения (benefits.read+…). Каждый ответ возвращает отправленный вами X-Trace-Id (здесь опущен для краткости).