ბაზა მხოლოდ www-ით.botai.ge
(www-ს გარეშე) გადამისამართებას აბრუნებს (308), და
HTTP-კლიენტების უმეტესობა გადამისამართებისას Authorization-ს
არ გადააქვს.
ახალი გასაღები, ძველის გაუქმება. გაუქმება მომდევნო მოთხოვნიდანვე მოქმედებს.
CORS
არ არსებობს, განზრახ: გასაღები წერს, ამიტომ მხოლოდ სერვერიდან გამოიყენება, ბრაუზერიდან — არა.
უარი
401 unauthorized — გასაღები აკლია, უცნობია, გამრუდებულია ან გაუქმებულია.
რომელი გასაღები
სად იქმნება
კორპორატიული API
ასისტენტი → ასისტენტის პარამეტრები → სხვა → API წვდომა
პროდუქციის API
ასისტენტი → პროდუქცია → API გასაღები
უფლებები
წესი. უფლება გასაღებზე სახელით ეწერება; იერარქია და
ვარსკვლავი არ არსებობს. უფლებების გარეშე გასაღები (პროდუქციის ეკრანზე
გაცემული) /products-სა და /payments-ზე ღიაა,
კორპორატიულზე — დახურული.
უფლება
დონე
რას ხსნის (/api/v1-ის შემდეგ)
სიის გარეშე გასაღები
knowledge:read
ასისტენტის დონე
GET /knowledge
დახურულია
knowledge:write
ასისტენტის დონე
POST / PATCH /knowledge
დახურულია
conversations:read
ასისტენტის დონე
GET /conversations, /conversations/{channel}/{userId}, /contacts, /handoffs
არაუმეტეს 200. name — სავალდებულო; description, price, priceUnit, durationMinutes — არა. price თავისუფალი ტექსტია („40 ₾“, „50 ₾-დან“); durationMinutes — დადებითი მთელი.
მთლიანად იცვლება
name-ით ედრება, რეგისტრისა და დასაწყისსა თუ ბოლოში მდგარი ცარიელი სიმბოლოების გაუთვალისწინებლად: დამთხვევა ახლდება, დანარჩენი ემატება
faq
array
არაუმეტეს 300. question და answer — ორივე სავალდებულო.
მთლიანად იცვლება
question-ით ედრება, იგივე წესით
rules
array of string
არაუმეტეს 100. ცარიელი სტრიქონი არ შეიძლება.
მთლიანად იცვლება
ემატება, თუ უკვე არ დევს; არაფერი იშლება
documents
array
არაუმეტეს 10 ერთ მოთხოვნაში; ასისტენტს სულ 10 წყარო აქვს და კაბინეტში ატვირთულიც ითვლება. id — სავალდებულო, A-Za-z0-9_.:-, 1–64 სიმბოლო; label — ნაგულისხმევად id; text — string, არაუმეტეს 8 000 სიმბოლო.
იცვლება; API-ით ჩაწერილი, მაგრამ დაუსახელებელი დოკუმენტი იშლება
id-ით ახლდება; დანარჩენს ხელს არ ვახლებთ
services[].description და services[].price მხოლოდ string-ია; სხვა ტიპის მნიშვნელობა ჩუმად გამოიტოვება, შეცდომა არ ბრუნდება.
services[].price არასოდეს ითვლება რიცხვად: ასისტენტი მას სიტყვასიტყვით ციტირებს. პროდუქტის ფასი — მთელი თეთრი — პროდუქციის API-შია.
services[].priceUnit — ფასის ერთეული, დახურული სიიდან: visit, procedure, night, month, person, item, hour, from (საწყისი ფასი). სხვა მნიშვნელობას ვერ წავიკითხავთ და invalid_service-ით ვაბრუნებთ; ერთეულს თვითონ არ ვირჩევთ, რადგან ეს თქვენს ფასზე განცხადება იქნებოდა. თუ ფასი უბრალოდ ამ ერთი რამის ფასია, ნუ გამოგვიგზავნით — ასისტენტი მაშინ მხოლოდ ციფრს იტყვის.
თავის ზღვარს ზემოთ (200 / 20 / 300 / 100 / 10), ან ასისტენტის ცოდნის წყაროები სულ 10-ს აღემატება.
413
document_too_long
დოკუმენტის text 8 000 სიმბოლოზე გრძელია.
502
write_failed
ჩაწერის ნაწილი შესრულდა, ნაწილი — არა. პასუხში: applied, failed.
ვალიდაცია ჩაწერამდე გადის: 400 და 413-ის შემდეგ არაფერი შეცვლილა.
ცარიელი სხეული ({}) ბაზას არ ცლის — 400 nothing_to_write. ცარიელი მასივი ("rules": []) კი სრულდება და თავს ცლის.
8 000 სიმბოლოზე გრძელი დოკუმენტი აქ უარყოფილია და არ იჭრება; გაყავით ნაწილებად. კაბინეტში კი ასე არ არის: იქ ატვირთული ფაილი მიიღება, პირველი 8 000 სიმბოლო ინახება და წყაროს ბარათი ამბობს, რამდენი წავიკითხეთ და რამდენი გადაეცემა ასისტენტს. აქ უარვყოფთ იმიტომ, რომ ამ მისამართს პროგრამა ეძახის და დაყოფა შეუძლია.
ერთი ასისტენტის ყველა წყაროდან — API-ით ჩაწერილიდან და კაბინეტში ატვირთულიდან ერთად — ასისტენტს ჯამში 14 000 სიმბოლო გადაეცემა, ახლებიდან დაწყებული. ამ ზღვარს ზემოთ დარჩენილი დოკუმენტი ინახება, მაგრამ პასუხებში არ მონაწილეობს.
502 write_failed: კონფიგურაცია ერთი ჩაწერით შედის, დოკუმენტები — ცალ-ცალკე. არაფერი წაშლილა; იგივე მოთხოვნა უსაფრთხოდ მეორდება.
POST და PATCH ერთი მისამართია: ბაზის დაცლა მხოლოდ მეთოდის არჩევით ხდება, სხეულში დროშა არ არსებობს.
PATCH/api/v1/knowledgeknowledge:write
დასახელებული თავი არსებულში ერთვება; არაფერი იშლება. სხეული და შეცდომები — როგორც POST-ზე.
channel, userId, lastMessage, lastRole (user ან assistant), lastTimestamp (ISO 8601), messageCount.
returned, limit, offset
integer
რამდენი მოვიდა; გამოყენებული გვერდის ზომა; საიდან.
total
integer
სიის სიგრძე. countsComplete: false-ზე — ქვედა ზღვარი.
truncated
boolean
true — მეტი არსებობს, ან countsComplete: false.
nextOffset
integer
მხოლოდ თუ მომდევნო გვერდი არსებობს: გაიმეორეთ ?offset= ამ რიცხვით.
countsComplete
boolean
false — სკანირება ზღვარზე გაჩერდა (იხ. ქვემოთ).
countsComplete: false ერთადერთი ზღვარია, რომელსაც offset ვერ გასცდება. სია ასისტენტის შენახული შეტყობინებების სკანირებით შენდება და სკანირება 20 000 სტრიქონზე ჩერდება. მის იქით total და ყოველი messageCount ქვედა ზღვარია, ყველაზე ძველი მიმოწერა სიაში არაა.
null — კლიენტი ჯერ კიდევ ელოდება. „შეტყობინება გაიგზავნა“ და „ვიღაცამ აიღო“ ორი სხვადასხვა ფაქტია.
returned, limit
integer
truncated
boolean
true — ყველაზე ძველები ვერ ჩაეტია.
nextBefore
string
მხოლოდ truncated: true-ზე: გაიმეორეთ ?before= ამ დროით.
შეცდომები
სტატუსი
error
როდის
400
invalid_request
before ISO 8601 არ არის.
before ჩათვლითია, ამიტომ სასაზღვრო სტრიქონი ხელახლა მოვა: დუბლიკატები id-ით გაფილტრეთ. ორ მოთხოვნას წამის მეათასედამდე ერთი და იგივე createdAt შეიძლება ჰქონდეს, ამიტომ ზღვარი ჩათვლითია.
truncated: true და nextBefore არ არის — გვერდი ერთმა დროის ნიშნულმა აავსო, კურსორი ვერ წაინაცვლებს.
3. ჯავშნები
ჯავშნების წაკითხვა, დაკავებული დრო, ახალი ჯავშანი და გაუქმება · უფლებები bookings:read, bookings:write · ასისტენტის დონე · გეგმა: კორპორატიული.
მაღაზიის სიტყვებით; არა-null — ჯავშანი უარყოფილია და cancelled: true-ცაა.
attended
boolean | null
true — ადამიანმა მონიშნა „მოვიდა“; false — „არ მოვიდა“; null — არავის არაფერი დაუფიქსირებია.
createdAt
string
ISO 8601.
შეცდომები
სტატუსი
error
როდის
400
invalid_request
from ან toYYYY-MM-DD არ არის.
attended: null „არ მოვიდა“ არ არის და ასე არ უნდა შემოიტანოთ.
nextFrom-ის თარიღის სტრიქონები ხელახლა მოვა: დუბლიკატები id-ით გაფილტრეთ. truncated: true და nextFrom არ არის — გვერდი ერთმა დღემ აავსო: ითხოვეთ ვიწრო from / to.
GET/api/v1/bookings/busybookings:read
დაკავებული დრო — და არაფერი იმაზე, ვინ დაიკავა. განრიგისთვის, რომელიც კითხულობს „როდის მცალია“. hours ბლოკი კვირის სამუშაო საათებსაც აბრუნებს, რომ POST /bookings-ის ორი უარი — closed_day და outside_hours — წინასწარ შემოწმდეს.
date, time, service, staffName (string | null), pendingApproval (boolean). მოქმედი ჯავშნები: გაუუქმებელი და უარყოფილი არა; დადასტურების მომლოდინეც შედის.
returned, limit
integer
limit ყოველთვის 500.
truncated
boolean
true — ფანჯარა 500 ჯავშანზე დიდია.
nextFrom
string
მხოლოდ truncated: true-ზე.
source
string
bookings — მხოლოდ ჩვენი ცხრილი.
hours
object
ბიზნესის სამუშაო კვირა: timeZone (ყოველთვის Asia/Tbilisi), mustFinishInside და week. შეიძლება საერთოდ არ მოვიდეს — იხილეთ ქვემოთ.
hours.week
object
შვიდი გასაღები: mon…sun. თითოეულს აქვს state: open (მაშინ opens და closes, HH:MM), closed, ან unknown.
hours.mustFinishInside
boolean
ყოველთვის true: ჯავშანი სამუშაო საათებში უნდა დასრულდეს, და არა მხოლოდ დაიწყოს.
შეცდომები
სტატუსი
error
როდის
400
invalid_request
from ან toYYYY-MM-DD არ არის.
ჯერ truncated შეამოწმეთ. მოჭრილი დრო დაკავებულია და არა თავისუფალი: მასში ჩაჯავშნა ორმაგი დაჯავშნაა.
truncated: true — ითხოვეთ უფრო ვიწრო ფანჯარა: ?from= = nextFrom. nextFrom არ მოვიდა — გვერდი ერთმა დღემ აავსო, ითხოვეთ დღე-დღე.
source: "bookings" ნიშნავს, რომ სია მხოლოდ ჩვენს ცხრილს ასახავს. თუ ასისტენტს Google Calendar აქვს მიბმული, ავტორიტეტი ის კალენდარია და მასში შეიძლება იდგეს ჩანაწერი, რომელიც ჩვენ არასოდეს გვინახავს (მაგ. ხელით ჩაწერილი). წაიკითხეთ როგორც „ნამდვილად დაკავებული“, არასოდეს როგორც „ნამდვილად თავისუფალი“.
იმავე კალენდარს თქვენც თუ უკავშირდებით, ჩვენი ჩანაწერის ამოცნობა — Google Calendar.
საათები კვირაა და არა თარიღების სია. ცალკეული დღის გამონაკლისი (უქმე, ერთჯერადი დახურვა) ამ პროდუქტში არ არსებობს, ამიტომ შვიდი გასაღები სრული პასუხია და from/to-ზე არ იზრდება. თარიღის კვირის დღედ გადაყვანა Asia/Tbilisi-ში ხდება — opens და closes ამავე ზონის კედლის საათია, არა UTC.
unknown არ ნიშნავს „დაკეტილს“. ის ნიშნავს, რომ მფლობელს ამ დღეზე გასაგები არაფერი უწერია. ასეთ დღეს ჯავშანს არ ვაუქმებთ საათების მიზეზით: closed_day და outside_hours მხოლოდ იმაზე გაიცემა, რაც მფლობელმა მართლა აკრიფა. სამივე მდგომარეობა სხვადასხვაა და ერთმანეთში არ ითარგმნება.
hours თუ საერთოდ არ მოვიდა — ასისტენტის პარამეტრები ვერ წავიკითხეთ. ეს არაა „საათები არ აქვს“: არარსებობა ნიშნავს „არ გითხარით“, და ამ დროს დღეზე ვერაფერს დაასკვნით. დღიური მაინც სწორია — კონფიგურაციის წაკითხვის შეცდომა მას არ აგდებს.
საათების წყარო იგივეა, რაც ჩაწერის უარისა — ერთი ფუნქცია, ერთი პასუხი. ამიტომ აქ გამოქვეყნებულ საათებში ჩაწერა closed_day-ს ან outside_hours-ს ვერ დააბრუნებს; slot_taken კი შეუძლია, რადგან ის დროზეა და არა დღეზე.
POST/api/v1/bookingsbookings:write
ახალი ჯავშანი. ასისტენტის საკუთარი პარამეტრი „ჯავშანს ჩემი დადასტურება სჭირდება“ ამ მოთხოვნაზეც მოქმედებს.
სხეულის ველები
პარამეტრი
ტიპი
სავალდებულო
ნაგულისხმევი · ზღვარი
შენიშვნა
userId
string
დიახ
—
კლიენტის იდენტიფიკატორი თქვენს სისტემაში; ამით უკავშირდება ჯავშანი კონტაქტსა და მიმოწერას.
true — ჯავშანი მფლობელის დადასტურებას ელოდება და კალენდარში ჯერ არ ჩანს. მნიშვნელობა ასისტენტის პარამეტრიდან მოდის (ნაგულისხმევად ჩართულია) და მოთხოვნით არ იცვლება.
შეცდომები
სტატუსი
error
როდის
400
invalid_body
სხეული ობიექტი არაა.
400
invalid_booking
userId, service, date ან time აკლია ან გამრუდებულია; field ამბობს, რომელი.
404
assistant_not_found
გასაღების ასისტენტი აღარ არსებობს.
409
slot_taken
დრო უკვე დაკავებულია — ჩვენს დღიურში ან კალენდარში. არაფერი დაჯავშნულა; აირჩიეთ სხვა.
409
closed_day
ბიზნესი იმ დღეს დაკეტილია. იმავე თარიღზე ხელახლა ცდა იმავე პასუხს დააბრუნებს. წინასწარ: hours.week-ში ამ დღის state არის closed.
409
outside_hours
დრო სამუშაო საათებს სცდება, ან ჯავშანი მათ დასრულებამდე არ სრულდება. იმავე დროზე ხელახლა ცდა იმავე პასუხს დააბრუნებს. წინასწარ: hours.week-ის opens/closes.
დაკავებულობა ყოველ ჩაწერაზე მოწმდება — 2026-09-20-მდე მხოლოდ მაშინ მოწმდებოდა, როცა ასისტენტს Google Calendar ჰქონდა მიბმული და pendingApprovalfalse იყო, ანუ ყველაზე დატვირთული შემთხვევა — დასადასტურებლად მდგარი მოთხოვნების რიგი — სწორედ ის იყო, რასაც არაფერი ამოწმებდა. ახლა ჩვენი საკუთარი ჯავშნები და Google ერთი დაკავებული ნაკრებია, და დასადასტურებელი მოთხოვნაც იკავებს საათს. GET /bookings/busy კვლავ სწრაფი წინასწარი შემოწმებაა, მაგრამ სავალდებულო აღარაა.
დრო სამუშაო საათებს ედრება, და ჯავშანი მთლიანად უნდა ჩაეტიოს: დაკეტილ დღეს closed_day, საათებს გარეთ — outside_hours. ორივე 409-ია და ორივე ჯიუტია — იმავე დროზე მეორედ მოთხოვნა იმავე პასუხს დააბრუნებს. სერვისი ასისტენტის სერვისების სიას და თარიღი წარსულს კვლავ არ ედრება.
ორივე წინასწარ მოწმდება, 2026-09-20-დან: GET /bookings/busy დღიურთან ერთად hours ბლოკს აბრუნებს — კვირის შვიდი დღე, თითოეული open / closed / unknown, Asia/Tbilisi-ის კედლის საათით. closed დღე closed_day-ია; open დღის საზღვრებს გარეთ ან მათში ვერდასრულებადი ჯავშანი — outside_hours. unknown დღეზე საათების გამო უარს არ ვამბობთ. ეს ტექსტი აქამდე ამბობდა, რომ წინასწარი შემოწმება შეუძლებელია — იმ დღეს ასეც იყო.
პასუხი ჯავშნის id-ს არ შეიცავს. გასაუქმებლად იპოვეთ ის GET /bookings-ში (userId, date, time).
ჯავშნის გაუქმება, როცა კლიენტმა ის თქვენს სისტემაში გააუქმა: ჯავშანი გაუქმებულად აღინიშნება, Google Calendar-ის ჩანაწერი იშლება, შეხსენება ჩერდება, GET /bookings/busy დროს აღარ ითვლის.
პარამეტრები
პარამეტრი
ტიპი
სავალდებულო
ნაგულისხმევი · ზღვარი
შენიშვნა
bookingId
integer
დიახ (გზაში)
—
id, რომელსაც GET /bookings აბრუნებს. სხეული არ არის.
მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/bookings/91/cancel \
-H "Authorization: Bearer $CORPORATE_API_KEY"
id, channel, userId, service, date, time, staffName.
შეცდომები
სტატუსი
error
როდის
400
invalid_request
bookingId დადებითი მთელი არაა.
404
booking_not_found
ამ ასისტენტს ასეთი ჯავშანი არ ეკუთვნის. სხვისი და არარსებული id ერთნაირად პასუხობს.
409
ambiguous_booking
თარიღი, დრო, სერვისი და — სადაც არის — ტელეფონი ერთზე მეტ მოქმედ ჯავშანს აღწერს (მაგ. ორი სპეციალისტი, ერთი დრო, ტელეფონის გარეშე), ან იმ დღეს 500-ზე მეტი მოქმედი ჯავშანია. არაფერი გაუქმებულა.
არაფერი იშლება, ამიტომ ეს DELETE არაა: ჯავშანი cancelled: true-ით რჩება და GET /bookings მას ისევ აბრუნებს — დასწრების კითხვა და კვირის ანგარიში ამ სტრიქონს კითხულობს.
გამეორება უსაფრთხოა: უკვე გაუქმებულ ჯავშანზე (გააუქმეთ თქვენ, მოთხოვნის მეორე ასლმა ან მფლობელმა კაბინეტში) პასუხია 200 და alreadyCancelled: true, შეცდომა არა.
ambiguous_booking-ზე გააუქმეთ კაბინეტში ან მოგვწერეთ. ჯავშნის შექმნისას ტელეფონის მითითება ამ დამთხვევას თავიდან აგარიდებთ.
4. ასისტენტები — ანგარიშის დონე
ანგარიშის ასისტენტების სია, შექმნა, პარამეტრების შეცვლა · უფლებები assistants:read, assistants:write · ანგარიშის დონე: გასაღები წვდება ანგარიშის ყველა ასისტენტს (ორი დონე) · გეგმა: კორპორატიული.
ყველა მისამართზე ასევე: 401, 403 (მათ შორის no_account), 429, 500 — შეცდომები და ლიმიტები.
GET/api/v1/assistantsassistants:read
ყველა ასისტენტი, რომელსაც ეს ანგარიში ფლობს. არ იფურცლება.
clientId, name, active (boolean), language, tone, requireBookingApproval (boolean).
returned
integer
რამდენი ასისტენტი დაბრუნდა.
total
integer
რამდენს ასახელებს ანგარიშის ჩანაწერი.
truncated
boolean
true — returnedtotal-ზე ნაკლებია.
სხვაობა returned-სა და total-ს შორის ნიშნავს, რომ ანგარიში ისეთ ასისტენტს ასახელებს, რომელიც აღარ არსებობს. ხელახალი მოთხოვნა ამას არ ცვლის — მოგვწერეთ.
POST/api/v1/assistantsassistants:write
ასისტენტის შექმნა. გამორთული იქმნება: ჯერ გამართეთ (ცოდნა და არხები — კაბინეტში), მერე ჩართეთ PATCH-ით.
სხეულის ველები
პარამეტრი
ტიპი
სავალდებულო
ნაგულისხმევი · ზღვარი
შენიშვნა
clientId
string
დიახ
—
პატარა ლათინური ასოები, ციფრები და ერთმაგი დეფისი; დიდი ასო პატარად გადაიქცევა. მუდმივია — მოგვიანებით არ იცვლება.
name
string
დიახ
—
სახელი, რომლითაც ეს ფილიალი წარადგენს თავს.
active
boolean
არა
false
ჩართულად შესაქმნელად უნდა იყოს true; სხვა ყველა მნიშვნელობა — გამორთული.
templateName
string
არა
—
საწყისი შაბლონის სახელი; გამოტოვებულზე ნაგულისხმევი შაბლონი გამოიყენება. უცნობი სახელი — create_failed.
სხეულში ისეთი ველია, რომელსაც ეს მისამართი არ ცვლის; პასუხში fields ჩამოთვლის. არხის მონაცემები (Telegram-ის ბოტის ტოკენი, Meta-ს გვერდის ტოკენი) აქედან განზრახ არ იცვლება — ისინი კაბინეტში რჩება.
400
nothing_to_write
სხეულმა არც ერთი ველი არ დაასახელა.
400
invalid_request
ველის მნიშვნელობა არასწორი ტიპისაა ან ცარიელია; field ამბობს, რომელი.
404
assistant_not_found
ამ ანგარიშს ასეთი ასისტენტი არ ეკუთვნის.
409
write_conflict
წაკითხვასა და ჩაწერას შორის კონფიგურაცია შეიცვალა. არაფერი ჩაწერილა; წაიკითხეთ და გაიმეორეთ.
5. ხარჯის მიკუთვნება — ანგარიშის დონე
შეტყობინებები და ტოკენები ასისტენტების მიხედვით · უფლება usage:read · ანგარიშის დონე · გეგმა: კორპორატიული.
ყველა მისამართზე ასევე: 401, 403 (მათ შორის no_account), 429, 500 — შეცდომები და ლიმიტები.
GET/api/v1/usageusage:read
შეტყობინებები და ტოკენები ასისტენტების მიხედვით.
პარამეტრები
პარამეტრი
ტიპი
სავალდებულო
ნაგულისხმევი · ზღვარი
შენიშვნა
from
string
არა
მიმდინარე თვის პირველი დღე
YYYY-MM-DD, ჩათვლით.
to
string
არა
დღეს
YYYY-MM-DD, ჩათვლით. ერთი მოთხოვნა მაქსიმუმ 92 დღეს ფარავს.
რაც პასუხებმა ჩვენ დაგვიჯდა. გაზომილი გამოძახებების ჯამია.
unmeasuredCalls
integer
calls-ის ის ნაწილი, რომლის ტოკენებიც ვერ გავზომეთ. ეს უფასო გამოძახებები არაა.
byModel[]
array
იგივე ციფრები მოდელების მიხედვით: provider, model, calls, ოთხი ტოკენის ველი, unmeasuredCalls. ცარიელია, თუ გამოძახება არ ყოფილა.
შეცდომები
სტატუსი
error
როდის
400
invalid_request
from ან toYYYY-MM-DD არ არის, ან fromto-ზე გვიანაა.
400
window_too_long
ფანჯარა 92 დღეზე გრძელია. პასუხში maxDays.
404
assistant_not_found
clientId ამ ანგარიშს არ ეკუთვნის.
413
window_too_large
იმ ფანჯარაში ერთ მოთხოვნაზე მეტი გამოძახებაა (40 000-მდე იკითხება). ითხოვეთ უფრო მოკლე ფანჯარა; ჯამს, რომელიც თვის შუაში ჩუმად შეწყვეტდა თვლას, არ ვაბრუნებთ.
messages და ტოკენები არასოდეს ერევა: ერთი ინვოისის ერთეულია, მეორე — ჩვენი ხარჯი. გრძელი ცოდნის ბაზის მქონე ასისტენტი შეტყობინებაზე რამდენჯერმე მეტ ტოკენს ხარჯავს, ვიდრე ერთსტრიქონიანი კითხვა-პასუხის ბოტი.
ფულადი ველი (cost) არც ერთ ვალუტაში არ არსებობს: ტოკენის ფასს არსად ვაქვეყნებთ.
6. პროდუქციის API
კატალოგის ჩაწერა: პროდუქტი, ვარიანტი, ფასი, მარაგი · უფლება products:write · ასისტენტის დონე · დანამატი „პროდუქციის სინქრონიზაცია“ (403 addon_required მის გარეშე).
ყველა მისამართზე ასევე: 401, 403 (addon_required, scope_required), 429, 500 — შეცდომები და ლიმიტები.
პირობა
მნიშვნელობა
ბაზა
https://www.botai.ge/api/v1/products
დანამატი
„პროდუქციის სინქრონიზაცია“ იმ ასისტენტზე, რომელშიც წერთ. მის გარეშე ყოველი მოთხოვნა — 403 addon_required. კორპორატიულში შედის; სტარტსა და ბიზნესზე ცალკე იყიდება.
უფლება
products:write. უფლებების სიაში სახელის გარეშე — 403 scope_required, კატალოგს არაფერი ეშლება. უფლებების სიის გარეშე გაცემული გასაღები (პროდუქციის ეკრანზე) ისევ წერს. დანამატი და უფლება ორი ცალკე შემოწმებაა: ორივეს დადებითი პასუხი სჭირდება.
ასისტენტის კატალოგის წყარო. გამოტოვებულზე გამოიყენება ამ ასისტენტის საკუთარი „კატალოგი API-დან“ წყარო (პირველ მოთხოვნაზე იქმნება). პასუხში ბრუნდება. სხვისი — 404 source_not_found.
products[]-ის ველები
ველი
ტიპი
სავალდებულო
ზღვარი
შენიშვნა
id
string | integer
დიახ
—
პროდუქტის id თქვენს სისტემაში: მისით ვცნობთ პროდუქტს შემდეგ ჩაწერაზე და ვახლებთ მას; მეორე ასლი არ იქმნება. სინონიმი — externalId.
name
string
დიახ
500 სიმბოლო
PATCH-ზეც სავალდებულოა.
description
string
არა
20 000 სიმბოლო
category
string
არა
500 სიმბოლო
imageUrl
string
არა
2 000 სიმბოლო
სინონიმი — image.
status
string
არა
—
active (ნაგულისხმევი), hidden, archived. პროდუქტის სტატუსი ვარიანტებიდან გამოითვლება: ერთი აქტიური ვარიანტი პროდუქტს აქტიურად ტოვებს.
variants
array
არა
100
თუ პროდუქტს ერთი ფორმა აქვს, გამოტოვეთ: sku, ფასი, მარაგი და attributes თვით პროდუქტზე იწერება.
products[].variants[]-ის ველები
ველი
ტიპი
სავალდებულო
ზღვარი
შენიშვნა
id
string | integer
პირობით
—
ვარიანტის id; თუ არაა — sku. ერთვარიანტიან პროდუქტს შეუძლია პროდუქტის id-ის გამოყენება; რამდენიმე ვარიანტიანს — არა: id ან sku სავალდებულოა. სინონიმი — externalId.
sku
string
არა
200 სიმბოლო
priceTetri
integer | string | null
არა
2 147 483 647
ფასი; წესები — ქვემოთ. სინონიმი — price.
salePriceTetri
integer | string | null
არა
2 147 483 647
ფასდაკლებული ფასი; ითვლება მხოლოდ ძირითად ფასთან ერთად. სინონიმი — salePrice.
stock
integer | string | null
არა
2 147 483 647
მარაგი; წესები — ქვემოთ.
status
string
არა
—
როგორც პროდუქტზე.
attributes
object
არა
20 გასაღები
მნიშვნელობა — string ან რიცხვი: {"ზომა": "42", "ფერი": "შავი"}.
დაარქივებული პროდუქტები — ისინი, რომლებიც ამ წყაროში იყო და მოთხოვნაში არ დასახელდა.
variants
integer
ჩაწერილი ვარიანტები.
warnings[]
array
code, message (ქართულად), index (პროდუქტის ნომერი, თუ ვრცელდება).
შეცდომები
სტატუსი
error
როდის
400
invalid_body
სხეული ობიექტი არაა; products მასივი არაა; sourceId დადებითი მთელი არაა (field).
400
empty_catalogue
products ცარიელია.
400
invalid_product
პროდუქტი ან ვარიანტი გამრუდებულია. index — პროდუქტის ნომერი მასივში (0-დან), field — ველის გზა, მაგ. variants[0].priceTetri.
400
duplicate_product_id, duplicate_variant_id
ერთი id ერთსა და იმავე მოთხოვნაში ორჯერ (ერთი პროდუქტის შიგნით — ვარიანტისა).
400
too_many_variants
ერთ პროდუქტზე 100-ზე მეტი ვარიანტია.
404
source_not_found
ამ ასისტენტს ასეთი წყარო არ ეკუთვნის.
413
too_many_products
ერთ მოთხოვნაში 500-ზე მეტი პროდუქტი.
413
too_many_variants
ერთ მოთხოვნაში 5 000-ზე მეტი ვარიანტი.
413
body_too_large
სხეული 1 000 000 ბაიტზე დიდია; პასუხში maxBytes.
502
write_failed
ნაწილი ჩაიწერა, ნაწილი — არა. პასუხში ჩანს, რა ჩაიწერა (created, updated, variants); არაფერი დაარქივებულა.
ცარიელი POST კატალოგს არ ცლის (empty_catalogue): ეს თითქმის ყოველთვის თქვენი ექსპორტის შეცდომაა. გასაყიდიდან მოსახსნელი პროდუქტი გამოგზავნეთ "status": "archived"-ით.
500-ზე დიდი კატალოგი: გაგზავნეთ გვერდები PATCH-ით, გასაყიდიდან მოსახსნელი პროდუქტები კი — ბოლოს, PATCH-ით "status": "archived"-ით. ბოლო გვერდზე POST ყველაფერს დაარქივებდა, გარდა ამ გვერდისა.
502 write_failed: იგივე მოთხოვნა უსაფრთხოდ მეორდება — პროდუქტი id-ით ახლდება.
PATCH/api/v1/productsproducts:write
ნაწილობრივი განახლება: მხოლოდ დასახელებული პროდუქტი ახლდება ან ემატება; არაფერი არქივდება. სხეული და შეცდომები — როგორც POST-ზე.
PATCH-ის პასუხში ველი არ არის: PATCH არაფერს არქივებს.
დანარჩენი
როგორც POST-ზე.
7. გადახდა
ერთი მოთხოვნა ხსნის შეკვეთას და აბრუნებს გადახდის ბმულს ან მის QR-ს · უფლება payments:write · ასისტენტის დონე · წვდომის პირობა იგივეა, რაც პროდუქციის API-ს: დანამატი „პროდუქციის სინქრონიზაცია“.
ყველა მისამართზე ასევე: 401, 403 (addon_required, scope_required), 429, 500 — შეცდომები და ლიმიტები.
პირობა
მნიშვნელობა
მისამართი
POST https://www.botai.ge/api/v1/payments
დანამატი
„პროდუქციის სინქრონიზაცია“ იმ ასისტენტზე, რომელშიც ყიდით — იგივე, რაც პროდუქციის API-ს; ახალი პირობა აქ არაა. მის გარეშე — 403 addon_required.
უფლება
payments:write. უფლებების სიაში სახელის გარეშე — 403 scope_required, შეკვეთა არ იხსნება და გადახდის ბმული არ იქმნება. უფლებების სიის გარეშე გაცემული გასაღები ყიდის. უფლებები გაცემისას ფიქსირდება.
ფული
მთელი თეთრი: 18999 = 189.99 ₾.
ვალუტა
ის, რაც კატალოგშია. ბარათით იხდება GEL, USD, EUR, GBP; გადაყვანა არსად ხდება.
სიხშირე
30 გაყიდვა წუთში ასისტენტზე; 120 მოთხოვნა წუთში IP-დან.
სხეულის ველები
ველი
ტიპი
სავალდებულო
ზღვარი
შენიშვნა
variantId
string | integer
დიახ
128 სიმბოლო
ვარიანტის id კატალოგში — პროდუქციის API-ის id (ან sku).
productId
string | integer
არა
128 სიმბოლო
მხოლოდ ავიწროებს: საჭიროა, როცა ერთი variantId ამ მაღაზიაში ორ პროდუქტზეა.
quantity
integer
არა
1–1000 · ნაგულისხმევი 1
method
string
არა
online (ნაგულისხმევი), cod
cod — მიტანისას გადახდა.
orderRef
string
არა
A-Za-z0-9_-, 4–64 სიმბოლო
შეკვეთის ნომერი და იდემპოტენტურობის გასაღები. გამოტოვეთ — ჩვენ დავარქმევთ.
customer
object
არა
name 120, phone 40, email 160
მყიდველი. გრძელი მნიშვნელობა იჭრება.
description
string
არა
200 სიმბოლო
ბარათით გადახდაზე Quickpay-ს ეგზავნება. გრძელი იჭრება.
note
string
არა
500 სიმბოლო
ინახება შეკვეთაზე. გრძელი იჭრება.
amountTetri
integer
არა
—
მხოლოდ შესამოწმებლად: კატალოგის ჯამს უნდა ემთხვეოდეს.
currency
string
არა
—
მხოლოდ შესამოწმებლად: კატალოგის ვალუტას უნდა ემთხვეოდეს.
POST/api/v1/paymentspayments:write
ხსნის შეკვეთას და ქმნის გადახდას მაღაზიის საკუთარ Quickpay-ანგარიშზე. online-ზე ბრუნდება გადახდის ბმული და მისი QR.
ერთი ველი გამრუდებულია; field ამბობს, რომელი. amountTetri მთელი თეთრია — 149.99 აქ შეცდომაა.
403
addon_required
ასისტენტს პროდუქციის დანამატი არ აქვს.
403
scope_required
გასაღებს payments:write არ აქვს; scope ასახელებს მას.
404
variant_not_found
ამ ასისტენტის კატალოგში ასეთი ვარიანტი არაა. სხვისი ვარიანტისთვისაც იგივე პასუხია.
409
variant_ambiguous
ერთი id-ით ამ მაღაზიაში ორი ვარიანტი მოიძებნა. დააზუსტეთ productId-ით.
409
variant_not_for_sale, variant_unpriced
ვარიანტი გაყიდვიდან მოხსნილია, ან ფასი მას არ უწერია.
409
amount_mismatch, currency_mismatch
სხეულმა თანხა ან ვალუტა დაასახელა და კატალოგს არ დაემთხვა. არაფერი შექმნილა.
409
amount_not_chargeable
ჯამი (ფასი × რაოდენობა) ნულია ან 2 147 483 647 თეთრს აღემატება.
409
currency_not_chargeable
ამ ვალუტით ბარათის გადახდა არ იქმნება. chargeableIn ჩამოთვლის, რითი იქმნება.
409
cod_disabled
მაღაზია მიტანისას გადახდას არ იღებს. methods — რა დარჩა: ["online"], ან [], როცა ბარათით გადახდაც მომართული არაა (მაშინ გადახდის გზას მაღაზია თავად ადასტურებს).
409
payment_account_missing, payment_key_unreadable
Quickpay ჯერ არ დაუკავშირებიათ, ან შენახული გასაღები ვეღარ იკითხება. მფლობელის მოსაგვარებელია.
409
order_ref_taken
ამ ნომერზე უკვე სხვა გაყიდვაა გახსნილი. აირჩიეთ ახალი ნომერი.
400, 404, 409, 429, 502
payment_provider
Quickpay-მ უარი თქვა ან არ პასუხობს. failure ასახელებს მიზეზს, message ქართულად წერია; currency — თუ უარი ვალუტას ეხება.
502
order_not_saved
გადახდა შეიქმნა, ჩვენთან კი ვერ ჩაიწერა. გაიმეორეთ იმავე orderRef-ით.
201 — შეკვეთა ახლა შეიქმნა. 200 — იმავე orderRef-ზე იგივე გაყიდვა უკვე გახსნილია: იმავე ბმულს იღებთ და მეორე გადახდა არ იქმნება. იგივე orderRef სხვა გაყიდვაზე — 409 order_ref_taken.
cod ბმულს არ ქმნის: შეკვეთა awaiting_offline-ით იხსნება და თანხას მაღაზიის კურიერი იღებს. ხერხი ღიაა მხოლოდ მაშინ, როცა მფლობელს კაბინეტში ჩართული აქვს; სხვა შემთხვევაში — 409 cod_disabled.
თანხას ვარიანტის ფასი ადგენს, თქვენი მოთხოვნა — არა: სხვა ფასი არსად არსებობს. ცარიელი amountTetri ამ მისამართის სასურველი გამოყენებაა.
502 order_not_saved: orderRef იდემპოტენტურობის გასაღებია, ამიტომ გამეორება იმავე გადახდას აბრუნებს და მეორეს არ ქმნის. თუ orderRef არ გამოგიგზავნიათ, ის პასუხშია: გაიმეორეთ მისით.
ამ მისამართის message ქართულადაა (დანარჩენ API-ში — ინგლისურად).
POST/api/v1/payments/quickpay/webhook/{clientId}
ეს მისამართი ბანკისაა: Quickpay გვირეკავს, თქვენ — არასოდეს. გასაღები და დანამატი აქ არ მოწმდება, ერთადერთი კარი ხელმოწერაა. ასაგები არაფერია.
მისამართს გადახდის შექმნისას Quickpay-ს ჩვენ თვითონ ვაძლევთ. გადახდის შედეგი კაბინეტში, შეკვეთების ეკრანზე ჩანს.
ჩვენგან თქვენკენ ერთადერთი მოთხოვნა იგზავნება — ჯავშნის ვებჰუკი.
შეცდომები და ლიმიტები
error — მდგრადი კოდი; message — ინგლისურად
(გადახდაზე ქართულად); field, index — სადაც
პრობლემა კონკრეტულ შენატანშია. იხ. ასევე: 7. გადახდა,
შეცდომების დამუშავება.
ერთი ჩანაწერი გამრუდებულია. index და field ამბობს, რომელი. extraPrices-ში სტრიქონს label-იც სჭირდება და price-იც; ნახევრად შევსებულს არ ვაგდებთ — მთელ მოთხოვნას ვუარყოფთ.
400
duplicate_document_id
ერთსა და იმავე მოთხოვნაში ერთი id ორჯერ მოვიდა.
400
invalid_booking
ჯავშნის სავალდებულო ველი აკლია ან გამრუდებულია.
400
invalid_request
პარამეტრი, გზის ნაწილი ან სავალდებულო ველი გამრუდებულია: ჯავშნის id, რომელიც დადებითი მთელი არაა; name, რომელიც POST /assistants-ს აკლია.
400
invalid_product
პროდუქციის API: ერთი პროდუქტი ან ვარიანტი გამრუდებულია. index და field ამბობს, რომელი.
400
empty_catalogue
პროდუქციის API: products ცარიელია. ცარიელი POST კატალოგს არ ცლის — ეს თითქმის ყოველთვის თქვენი ექსპორტის შეცდომაა. გასაყიდიდან მოსახსნელს "status": "archived"-ით გამოგზავნით.
400
duplicate_product_id / duplicate_variant_id
პროდუქციის API: ერთი id ერთსა და იმავე მოთხოვნაში ორჯერ.
400
invalid_client_id
clientId დასაშვები სახის არაა.
400
create_failed
ასისტენტი ვერ შეიქმნა: clientId სხვა ანგარიშს უკავია, ან ასეთი templateName არ არსებობს. მიზეზს პასუხი არ ასახელებს.
400
unsupported_field
PATCH /assistants-ს ისეთი ველი გაეგზავნა, რომელსაც ის არ ცვლის.
400
window_too_long
ხარჯის ფანჯარა 92 დღეზე გრძელია.
401
unauthorized
გასაღები აკლია, უცნობია, გამრუდებულია ან გაუქმებულია.
403
plan_required
ანგარიში კორპორატიულ გეგმაზე არაა.
403
scope_required
გასაღებს არ აქვს ის უფლება, რომელსაც ეს მისამართი ითხოვს. scope ასახელებს მას.
403
addon_required
ასისტენტს პროდუქციის დანამატი არ აქვს.
403
no_account
გასაღების ასისტენტს მფლობელი ანგარიში არ ჰყავს, ამიტომ ანგარიშის დონის მისამართები მისთვის მიუწვდომელია.
403
bot_limit_reached
ანგარიშის ასისტენტების ლიმიტს ზემოთ.
404
assistant_not_found
ამ ანგარიშს ასეთი ასისტენტი არ ეკუთვნის.
404
booking_not_found
ამ ასისტენტს ასეთი ჯავშანი არ ეკუთვნის.
404
source_not_found
ამ ასისტენტს ასეთი კატალოგის წყარო არ ეკუთვნის.
409
write_conflict
POST / PATCH /knowledge და PATCH /assistants/{clientId}: წაკითხვასა და ჩაწერას შორის ასისტენტის კონფიგურაცია შეიცვალა. არაფერი ჩაწერილა — არც კონფიგურაცია, არც დოკუმენტები. წაიკითხეთ ახლანდელი მდგომარეობა და გაგზავნეთ ხელახლა.
409
client_id_taken
POST /assistants: ამ ანგარიშს ასეთი clientId-ის ასისტენტი უკვე ჰყავს.
409
slot_taken
დრო შემოწმებასა და ჩაწერას შორის სხვამ დაიკავა. არაფერი დაჯავშნულა.
409
closed_day
ბიზნესი იმ დღეს დაკეტილია — ეს რბოლა არაა და ხელახლა ცდა არ გამოასწორებს. წინასწარ ჩანს GET /bookings/busy-ის hours ბლოკში.
409
outside_hours
დრო სამუშაო საათებს სცდება — ეს რბოლა არაა და ხელახლა ცდა არ გამოასწორებს. წინასწარ ჩანს GET /bookings/busy-ის hours ბლოკში.
409
ambiguous_booking
გაუქმება ერთზე მეტ ჯავშანს შეეხებოდა. არაფერი გაუქმებულა.
პროდუქციის API: ერთ მოთხოვნაში 500-ზე მეტი პროდუქტი.
400 / 413
too_many_variants
პროდუქციის API: ერთ პროდუქტზე ან ერთ მოთხოვნაში ვარიანტების ზღვარს ზემოთ.
413
window_too_large
იმ ფანჯარაში იმაზე მეტი გამოძახებაა, ვიდრე ერთ მოთხოვნას წაკითხვა შეუძლია.
429
rate_limited
წუთში ძალიან ბევრი მოთხოვნა ამ ასისტენტისთვის. retryAfterMs ამბობს, რამდენს დაელოდოთ.
429
too_many_attempts
წუთში ძალიან ბევრი მოთხოვნა ამ IP-დან, დათვლილი მანამ, სანამ გასაღებს შევხედავთ.
500
internal_error
ჩვენია. გაიმეორეთ.
500
contacts_unreadable
GET /contacts: საკონტაქტო ბაზა ვერ წავიკითხეთ. ეს განზრახ არ არის ცარიელი სია — „კონტაქტები არ არის" და „ვერ წავიკითხეთ" სხვადასხვა ფაქტია. გაგრძელდა — მოგვწერეთ.
502
write_failed
ჩაწერის ნაწილი შესრულდა, ნაწილი — არა; არაფერი წაშლილა. პასუხი ამბობს, რა ჩაიწერა: applied და failed (ცოდნის ბაზა), created და updated (კატალოგი). იგივე მოთხოვნა უსაფრთხოდ მეორდება.
502
attach_failed
ასისტენტი შეიქმნა, მაგრამ ანგარიშზე ვერ მივამაგრეთ. ეს არ გაიმეოროთ — მოგვწერეთ; გამეორება მხოლოდ create_failed-ს დააბრუნებს.
უარყოფილი სხეული არაფერს წერს. ვალიდაცია პირველ
ჩაწერამდე მუშაობს: 400-ის შემდეგ ყველაფერი ისეა, როგორც იყო
— იმავე მოთხოვნის წესრიგში მყოფი თავებიც არ ჩაწერილა.
ლიმიტი
კორპორატიული API
პროდუქციის API
სხეული
512 000 ბაიტი
1 000 000 ბაიტი
მოთხოვნა წუთში, ასისტენტზე
120
60
მოთხოვნა წუთში, IP-დან
120
120
სიის გვერდი
500 სტრიქონი
500 პროდუქტი
ორი 429 — ორი განცალკევებული მთვლელი: too_many_attempts —
მისამართზე (გასაღების შემოწმებამდე), rate_limited —
ასისტენტზე. ერთის გადავსება მეორეს არ ხარჯავს.
შეცდომების დამუშავება
გამეორების წესი კოდზეა და არა სტატუსზე: ერთი და იგივე 502
ხან მეორდება, ხან არა.
სტატუსი და error
გამეორება?
მოქმედება
429rate_limited, too_many_attempts
დიახ
დაელოდეთ პასუხის სხეულის retryAfterMs მილიწამს და გაიმეორეთ იგივე მოთხოვნა.
500internal_error
დიახ
გაიმეორეთ ექსპონენციალური პაუზით (0,5 წმ, 1, 2, 4…), შეზღუდული რაოდენობით. POST /payments-ზე ყოველთვის გაგზავნეთ orderRef: იგივე orderRef იგივე გადახდას აბრუნებს და მეორეს არ ქმნის.
500contacts_unreadable
მოგვიანებით
ჩვენი მხარის ხარვეზია. პასუხი ცარიელ სიად ნუ ჩაითვლება. თუ გაგრძელდა — მოგვწერეთ.
502write_failed
დიახ, უცვლელად
ჩაწერა ნაწილობრივ შესრულდა, არაფერი წაშლილა. პასუხი ამბობს, რა ჩაიწერა; მოთხოვნა იდემპოტენტურია.
502order_not_saved
დიახ, იმავე orderRef-ით
გადახდა შეიქმნა, ჩვენთან ვერ ჩაიწერა. იმავე orderRef-ით გამეორება იმავე გადახდას აბრუნებს. თუ orderRef არ გამოგიგზავნიათ, ის პასუხშია: გაიმეორეთ მისით.
502attach_failed
არა
ასისტენტი შეიქმნა და ანგარიშზე ვერ მიემაგრა. გამეორება create_failed-ს დააბრუნებს. მოგვწერეთ.
409write_conflict
წაკითხვის შემდეგ
არაფერი ჩაწერილა. GET /knowledge ან GET /assistants და გაგზავნეთ ხელახლა.
409slot_taken
არა, უცვლელად
არაფერი დაჯავშნულა. აირჩიეთ სხვა დრო.
409closed_day
არა, უცვლელად
არაფერი დაჯავშნულა. სხვა დღე აირჩიეთ — იმავე თარიღზე ცდა ყოველთვის იმავეს დააბრუნებს. რომელი დღეებია დაკეტილი, GET /bookings/busy-ის hours ამბობს.
409outside_hours
არა, უცვლელად
არაფერი დაჯავშნულა. სამუშაო საათებში აირჩიეთ დრო — იმავე საათზე ცდა ყოველთვის იმავეს დააბრუნებს. საათები GET /bookings/busy-ის hours-შია.
409ambiguous_booking
არა
არაფერი გაუქმებულა. გააუქმეთ კაბინეტში ან მოგვწერეთ.
400, 401, 403, 404, 413 და დანარჩენი 409
არა, უცვლელად
შესასწორებელია მოთხოვნა, გასაღები, გეგმა ან უფლება; message, field, index ამბობს, რა. 401 — გასაღები. 403 plan_required, addon_required, scope_required — გეგმა, დანამატი ან ახალი გასაღები საჭირო უფლებით. 413 window_too_large — უფრო ვიწრო ფანჯარა.
ფუნქცია, რომელიც ზემოთ მოცემულ წესს ასრულებს: მეორდება მხოლოდ ცხრილში
„დიახ“-ად მონიშნული კოდები, დანარჩენზე შეცდომა მაშინვე ბრუნდება.
გამეორების ფუნქცია
JavaScript
// Repeated: 429 (after retryAfterMs), 500 internal_error, 502 write_failed
// and order_not_saved. Everything else is thrown at once, attach_failed included.
const RETRY = new Set(["rate_limited", "too_many_attempts", "internal_error", "write_failed", "order_not_saved"]);
async function callApi(url, init = {}, maxAttempts = 5) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, init);
const body = await res.json().catch(() => ({}));
if (res.ok) return body;
if (attempt >= maxAttempts || !RETRY.has(body.error)) {
throw Object.assign(new Error(`${res.status} ${body.error}: ${body.message}`), { status: res.status, body });
}
const pause = body.retryAfterMs ?? Math.min(30_000, 500 * 2 ** (attempt - 1));
await new Promise((resolve) => setTimeout(resolve, pause));
}
}
// const usage = await callApi("https://www.botai.ge/api/v1/usage", {
// headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
// });
Python
import os
import time
import requests
# Repeated: 429 (after retryAfterMs), 500 internal_error, 502 write_failed
# and order_not_saved. Everything else raises at once, attach_failed included.
RETRY = {"rate_limited", "too_many_attempts", "internal_error", "write_failed", "order_not_saved"}
def call_api(method, url, max_attempts=5, **kwargs):
for attempt in range(1, max_attempts + 1):
res = requests.request(method, url, timeout=30, **kwargs)
try:
body = res.json()
except ValueError:
body = {}
if res.ok:
return body
if attempt == max_attempts or body.get("error") not in RETRY:
raise RuntimeError(f"{res.status_code} {body.get('error')}: {body.get('message')}")
pause_ms = body.get("retryAfterMs")
time.sleep(pause_ms / 1000 if pause_ms is not None else min(30, 0.5 * 2 ** (attempt - 1)))
# usage = call_api(
# "GET",
# "https://www.botai.ge/api/v1/usage",
# headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
# )
ვერსიები და ცვლილებები
ვერსია ბაზის გზაშია: /api/v1.
v1-ში ახალ endpoint-ებსა და არასავალდებულო ველებს ვამატებთ. არსებულის
შეცვლას თავს ვარიდებთ. თუ გარღვევის მომტანი ცვლილება დაგვჭირდა,
წინასწარ შეგატყობინებთ.
თქვენს მხარეს: პასუხში უცნობი ველი უგულებელყავით; უცნობი
error კოდი HTTP სტატუსის მიხედვით დაამუშავეთ.
თარიღი
ცვლილება
2026-09-19
v1 — ამჟამინდელი კონტრაქტი
Google Calendar — ასისტენტის ჩანაწერი
ჩვენი ჩანაწერის ამოცნობა და დაკავებული დრო მფლობელის Google Calendar-ში · გეგმა: ბიზნესი და ზემოთ · API გასაღები არ გამოიყენება.
ჩანაწერის ველები
ველი
ტიპი
შენიშვნა
summary
string
<სერვისი> — <კლიენტის სახელი>. სახელის გარეშე: უცნობი მომხმარებელი.
description
string
სტრიქონები: ტელეფონი: … (ტელეფონის გარეშე -), არხი: …, მოთხოვნილი დრო: YYYY-MM-DD HH:MM; სპეციალისტის თხოვნისას ასევე მოთხოვნილი სპეციალისტი: ….
start, end
object
dateTime UTC-შია (Z), timeZone — Asia/Tbilisi. end = start + სერვისის ხანგრძლივობა; ხანგრძლივობის გარეშე — 60 წუთი.
transparency
string
ყოველთვის opaque. transparent ჩანაწერი freebusy-ის პასუხში საერთოდ არ ჩანს — არც დაკავებულად, არც თავისუფლად.
extendedProperties.private
object
ოთხი გასაღები, იხ. ქვემოთ.
extendedProperties.private
გასაღები
მნიშვნელობა
დანიშნულება
botaiSource
botai.ge/booking
მუდმივი ნიშანი. ფილტრი ამაზე დაწერეთ.
botaiClientId
ასისტენტის იდენტიფიკატორი, მაგ. demo-salon
ერთ კალენდარში რამდენიმე ასისტენტი წერს — ასე გაარჩევთ.
botaiChannel
telegram, website, whatsapp, …
იგივე, რაც description-ის სტრიქონში „არხი:".
botaiRequested
კლიენტის მოთხოვნილი დრო ტექსტად, მაგ. 2026-09-14 14:30
იგივე, რაც description-ის სტრიქონში „მოთხოვნილი დრო:", მანქანით წასაკითხად.
მოთხოვნა · Google Calendar API
GET /calendar/v3/calendars/{calendarId}/events
?privateExtendedProperty=botaiSource%3Dbotai.ge%2Fbooking
ფილტრი summary-ზე ან description-ზე არ დააფუძნოთ: ხელით აკრეფილი „სერვისი — სახელი" იდენტურად გამოიყურება.
private ნიშნავს „ამ კალენდრის ასლისთვის" და არა „დამალულს": კალენდრის ყველა მკითხველი ამ ველებსაც ხედავს. მათში არაფერია ისეთი, რაც summary-სა და description-ში უკვე არ წერია.
ჯავშნის ჩანაწერის ნომერი ჩანაწერში არ არის: ჩანაწერი კალენდარში ჩვენს ბაზაში ჯავშნის ჩაწერამდე იქმნება. ბმა პირიქითაა — ჩვენთან ინახება Google-ის ჩანაწერის id, ვებჰუკში ის booking.calendarEventId-ია.
ოთხი გასაღები ერთად უნიკალურ გასაღებს არ ქმნის.
სხვა სარტყელზე დაყენებული კალენდარი იმავე მომენტს თავისი სარტყელის საათით აჩვენებს. ეს კალენდრის პარამეტრია და არა ამ API-ის.
მთელი დღის [დასადასტურებელი დრო] ჩანაწერზე ფილტრს ნუ დაწერთ. ახალი ჯავშანი ამ ფორმით აღარ იქმნება; ძველი მოთხოვნის დადასტურებისას ის მაინც შეიძლება გაჩნდეს.
ჯავშნის ვებჰუკი — ჩვენ გიგზავნით
ჯავშნის შექმნაზე, დადასტურებაზე, უარსა და გაუქმებაზე ვგზავნით POST-ს მფლობელის მითითებულ მისამართზე · გეგმა: ბიზნესი და ზემოთ · API გასაღების ნაცვლად — HMAC ხელმოწერა.
გაგზავნის წესები
საკითხი
წესი
სად ირთვება
კაბინეტი: ასისტენტის პარამეტრები → სხვა → ჯავშნის ვებჰუკი.
გეგმა
მოწმდება ყოველ გაგზავნაზე და არა მხოლოდ მისამართის შენახვისას: სტარტზე ჩამოსული ანგარიშისთვის გაგზავნა წყდება.
მოთხოვნა
POST, content-type: application/json; charset=utf-8.
მცდელობა
ერთი, 5 წამის ლოდინით. გამეორება და რიგი არ არსებობს.
თქვენი პასუხი
სხეულს არ ვკითხულობთ. ნებისმიერი 2xx წარმატებაა; 3xx წარუმატებელია, გადამისამართებას არ მივყვებით.
წარუმატებლობა
მოვლენა იკარგება. არც გაფრთხილება იგზავნება, არც ჟურნალი არსებობს.
ვებჰუკი სისწრაფეა და არა სრული სურათი. ხელით ჩაწერილი შეხვედრა და გამოტოვებული მოვლენა მასში არ ჩანს; სრულ სურათს Google Calendar იძლევა.
მოვლენები
event
როდის
booking.status
booking.created
ჯავშანი ჩაიწერა. დაკავებულ დროზე — არასოდეს.
confirmed; pending, თუ ასისტენტი ჯავშანს დასადასტურებლად აჩერებს
booking.approved
მაღაზიამ მოთხოვნა დაადასტურა. ამ წუთიდან დრო ვალდებულებაა.
confirmed
booking.rejected
მაღაზიამ მოთხოვნაზე უარი თქვა. დრო გაათავისუფლეთ.
rejected
booking.cancelled
ჯავშანი გაუქმდა — ასისტენტში კლიენტმა, ან ჩვენი API-ით.
დასადასტურებელი ჯავშანი ორ მოვლენას იძლევა: booking.created (pending), შემდეგ booking.approved ან booking.rejected. დადასტურების გარეშე ჯავშანი — ერთს: booking.created (confirmed).
უცნობი event გამოტოვეთ და მაინც უპასუხეთ 2xx: სია გაიზრდება.
ასისტენტის იდენტიფიკატორი — ის, რაც კაბინეტის მისამართშია. არასოდეს ანგარიშის.
booking.service
string
ზუსტად ისე, როგორც მაღაზიამ სერვისების სიაში დაწერა.
booking.date
string
YYYY-MM-DD, booking.timezone-ში და არა UTC-ში.
booking.time
string
HH:MM, 24-საათიანი, booking.timezone-ში.
booking.timezone
string
დღეს ყოველთვის Asia/Tbilisi. წაიკითხეთ და ნუ ივარაუდებთ.
booking.durationMinutes
number
სერვისის ხანგრძლივობა; 60, თუ არ არის მითითებული. იგივე, რაც Google Calendar-ის ჩანაწერს აქვს.
booking.staff
string | null
სპეციალისტი, ვისთანაც ჯავშანია. null — ბიზნესს სპეციალისტების სია არ აქვს.
booking.staffRequested
string | null
სპეციალისტი, რომელიც კლიენტმა ითხოვა სიის არმქონე ბიზნესში. ბაზაში ცალკე არ ინახება, ამიტომ მხოლოდ booking.created-ზეა; შემდეგ მოვლენებზე null კლიენტის უარს არ ნიშნავს.
booking.channel
string
website, telegram, whatsapp, instagram, messenger. ჩვენი არხი და არა კლიენტი.
booking.status
string
confirmed | pending | cancelled | rejected. სტატუსი ამ მოვლენის შემდეგ.
booking.calendarEventId
string | null
Google Calendar-ის ჩანაწერის id (არა კალენდრის). null — ჩანაწერი არ არსებობს, მაგ. დასადასტურებელ ჯავშანზე.
booking.rejectedReason
string | null
მაღაზიის საკუთარი სიტყვები, booking.rejected-ზე.
booking.createdAt
string
ISO 8601, ჯავშნის პირველი შექმნის დრო. ოთხივე მოვლენაზე ერთი და იგივეა და თანმიმდევრობის გასაღებია.
კლიენტის სახელი და ტელეფონი არ იგზავნება: ვებჰუკის მისამართს მაღაზია თავად წერს და შეცდომით აკრეფილ მისამართზე პერსონალური მონაცემები ჩუმად გაიგზავნებოდა.
მოვლენები კლიენტის ჩანაწერს ვერ მიუსადაგებთ. ერთმანეთს მიუსადაგებთ ბუნებრივი გასაღებით assistantId + service + date + time; createdAt განასხვავებს ჯავშანს, რომელიც დაიჯავშნა, გაუქმდა და იმავე დროზე თავიდან დაიჯავშნა.
სხეული იგზავნება კომპაქტურად, JSON.stringify-ის ფორმით (ინტერვალების გარეშე).
ხელმოწერა
საკითხი
წესი
ხელმოსაწერი მასალა
v1:<timestamp>:<ნედლი სხეული> — სიტყვასიტყვით, ორწერტილებით. <timestamp> — x-botai-timestamp-ის მნიშვნელობა.
ალგორითმი
HMAC-SHA256, გასაღები — whsec_….
სათაური
x-botai-signature: v1=<თექვსმეტობითი>.
დროის ფანჯარა
x-botai-timestamp და თქვენი საათი არაუმეტეს 300 წამით განსხვავდებოდეს, ორივე მიმართულებით.
შედარება
მუდმივ დროში (timingSafeEqual, compare_digest).
შეამოწმეთ ნედლი სხეული.JSON.parse და შემდეგ JSON.stringify ბაიტებს უცვლელად არ აბრუნებს: ინტერვალები და რიცხვების ჩაწერის ფორმა იცვლება. ბაიტები ვებ-ფრეიმვორკის მიერ სხეულის დამუშავებამდე აიღეთ: Express-ში express.raw({ type: "application/json" }), Flask-ში request.get_data(), FastAPI-ში await request.body().
დროის ნიშნული ხელმოწერის შიგნითაა: ამით ჩაჭერილი მოთხოვნის გამეორება ფანჯრის შემდეგ ვეღარ გაივლის.
უარი თქვით ორივე მიმართულებით: გამოგზავნის დრო, რომელიც თქვენს საათს უსწრებს, იმავე რისკს ქმნის, რასაც ძველი.
შემოწმების ფუნქცია
JavaScript
const crypto = require("node:crypto");
// rawBody — Buffer ან string, ზუსტად ისე, როგორც მოვიდა.
// headers — სათაურები პატარა ასოებით (Node http, Express).
function verify(rawBody, headers, secret) {
const ts = headers["x-botai-timestamp"];
const got = headers["x-botai-signature"];
if (typeof ts !== "string" || typeof got !== "string") return false;
const seconds = Number(ts);
if (!Number.isFinite(seconds)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - seconds) > 300) return false;
const expected =
"v1=" +
crypto
.createHmac("sha256", secret)
.update(`v1:${ts}:`, "utf8")
.update(rawBody)
.digest("hex");
const a = Buffer.from(got, "utf8");
const b = Buffer.from(expected, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hashlib
import hmac
import time
# raw_body — bytes, ზუსტად ისე, როგორც მოვიდა.
# headers — სათაურების ლექსიკონი პატარა ასოებით (Flask-ისა და FastAPI-ის headers სწორია).
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers.get("x-botai-timestamp")
got = headers.get("x-botai-signature")
if not ts or not got:
return False
try:
seconds = int(ts)
except ValueError:
return False
if abs(time.time() - seconds) > 300:
return False
material = b"v1:" + ts.encode() + b":" + raw_body
expected = "v1=" + hmac.new(secret.encode(), material, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), got.encode())
body — სხეულის ზემოთ მოცემული მაგალითი, კომპაქტურად, ერთ სტრიქონად. სათაურების მაგალითში სწორედ ამ სხეულის ხელმოწერაა.
ვექტორი დროის ფანჯარას ვერ გაივლის: timestamp ძველია. შესამოწმებლად საათი timestamp-ის მახლობლად დააყენეთ ან ფანჯრის შემოწმება გამორთეთ.
body-ის ან timestamp-ის ერთი სიმბოლოს შეცვლისას შედარებამ false უნდა დააბრუნოს.
გასაღები
საკითხი
წესი
ფორმა
whsec_…; იქმნება მისამართის პირველ შენახვაზე.
ჩვენება
ერთხელ. ჩვენთან დაშიფრული ინახება და ხელახლა აღარ გამოჩნდება; კაბინეტი ბოლო ოთხ სიმბოლოს აჩვენებს.
დაკარგვა
ახალი გასაღები კაბინეტში. ძველი მაშინვე წყვეტს შემოწმებას — ახალი იმავე მომენტში ჩასვით.
მისამართის შეცვლა
გასაღებს არ ცვლის.
მისამართი
მოთხოვნა
წესი
სქემა
მხოლოდ https://. http-ზე ხელმოწერა გზაში იკითხება და ფანჯრის განმავლობაში შეიძლება გამეორდეს.
ქსელი
მხოლოდ საჯაროდ ამოსახსნელი. loopback, link-local (მათ შორის 169.254.169.254) და კერძო დიაპაზონები უარყოფილია. მოწმდება შენახვისას და ყოველ გაგზავნამდე.
მონაცემები მისამართში
https://user:pass@… უარყოფილია. ტოკენი ბილიკში ან შეკითხვის პარამეტრში ჩადეთ, ან ხელმოწერას დაეყრდენით.
გადამისამართება
არ მივყვებით: 3xx წარუმატებელი გაგზავნაა. საბოლოო მისამართი მიუთითეთ.
სიგრძე
მაქსიმუმ 500 სიმბოლო.
მიმღების მხარე
2xx სწრაფად დააბრუნეთ. სამუშაო რიგში ჩააგდეთ და მოთხოვნის შიგნით ნუ შეასრულებთ.
იყავით იდემპოტენტური x-botai-delivery-ზე. მოსვლის თანმიმდევრობა გარანტირებული არაა — მოვლენა შეიძლება უფრო ადრინდელის შემდეგ მოვიდეს.
უცნობი event გამოტოვეთ და მაინც დააბრუნეთ 2xx.
კალენდრის პერიოდულ კითხვას ნუ შეაწყვეტთ.
კითხვა გაჩნდა, ან რაღაც ამ გვერდზე კოდს არ ემთხვევა?
დაგვიკავშირდით — და დაგვიწერეთ, რომელი მისამართი
იყო.