BotAI.ge წესები შესვლა

დეველოპერებისთვის

HTTP API

ასისტენტის მართვა პროგრამიდან: ცოდნის ბაზა, მიმოწერა, ჯავშნები, ასისტენტები, ხარჯი, პროდუქციის კატალოგი, გადახდა.

სწრაფი დაწყება

პარამეტრიმნიშვნელობა
ბაზაhttps://www.botai.ge/api/v1
ავთენტიფიკაციაAuthorization: Bearer <გასაღები>. გასაღები კაბინეტში იქმნება: ავთენტიფიკაცია
ფორმატიJSON, UTF-8; სხეულიან მოთხოვნაზე Content-Type: application/json
გარემოJavaScript — Node 18+ (fetch); Python — pip install requests
ბაზა მხოლოდ www-ით. botai.ge (www-ს გარეშე) გადამისამართებას აბრუნებს (308), და HTTP-კლიენტების უმეტესობა გადამისამართებისას Authorization-ს არ გადააქვს.

1. გასაღები ცვლადებში

shell
export CORPORATE_API_KEY="cxk_Jt7q2sN9xVb0Lp4rWc8yZa3mEh6kFd1gTu5nQi2oXvA"
export PRODUCTS_API_KEY="cxk_Hn3v8mRb1zXd6Kt0Pw5jLy9cSe4uGa7fQo2iYr8sZbN"

გასაღებები მოგონილია — ჩასვით თქვენი. ყოველი მაგალითი კითხულობს ორ ცვლადს: CORPORATE_API_KEY — კორპორატიული API, PRODUCTS_API_KEY — პროდუქცია და გადახდა.

2. პირველი მოთხოვნა — რა იცის ასისტენტმა

მოთხოვნა
curl
curl https://www.botai.ge/api/v1/knowledge \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/knowledge", {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/knowledge",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "clientId": "salon-vake",
  "description": "სილამაზის სალონი ვაკეში, 2011 წლიდან.",
  "services": [
    { "name": "თმის შეჭრა", "price": "40 ₾", "durationMinutes": 45 }
  ],
  "faq": [
    { "question": "პარკინგი გაქვთ?", "answer": "დიახ, ეზოში." }
  ],
  "rules": ["ფასს ნუ იგონებ — მხოლოდ სიიდან"],
  "documents": []
}
  • 401 unauthorized — გასაღები არ მოვიდა ან უცნობია; ყველაზე ხშირად ცვლადი ცარიელია.
  • 403 plan_required — გასაღები წესრიგშია, ანგარიში კორპორატიულ გეგმაზე არაა: ორი API.
  • დანარჩენი უარები — შეცდომები და ლიმიტები.

მისამართები

ყველა მისამართი https://www.botai.ge-ზეა. ბმული გადაგიყვანთ მისამართის აღწერაზე.

მეთოდიმისამართიუფლებაAPIრას აკეთებსთავი
კორპორატიული API
GET /api/v1/knowledge knowledge:read კორპორატიული ცოდნის ბაზის წაკითხვა 1
POST /api/v1/knowledge knowledge:write კორპორატიული დასახელებული თავების ჩანაცვლება 1
PATCH /api/v1/knowledge knowledge:write კორპორატიული დასახელებული თავების შერწყმა; არაფერი იშლება 1
GET /api/v1/conversations conversations:read კორპორატიული მიმოწერების სია 2
GET /api/v1/conversations/{channel}/{userId} conversations:read კორპორატიული ერთი მიმოწერის სტენოგრამა 2
GET /api/v1/contacts conversations:read კორპორატიული საკონტაქტო ბაზა 2
GET /api/v1/handoffs conversations:read კორპორატიული ოპერატორის მოთხოვნები 2
GET /api/v1/bookings bookings:read კორპორატიული ჯავშნების სია 3
GET /api/v1/bookings/busy bookings:read კორპორატიული დაკავებული დრო და სამუშაო საათები, კლიენტების გარეშე 3
POST /api/v1/bookings bookings:write კორპორატიული ახალი ჯავშანი 3
POST /api/v1/bookings/{bookingId}/cancel bookings:write კორპორატიული ჯავშნის გაუქმება; გამეორება უსაფრთხოა 3
GET /api/v1/assistants assistants:read კორპორატიული ანგარიშის ასისტენტების სია 4
POST /api/v1/assistants assistants:write კორპორატიული ასისტენტის შექმნა; გამორთული იქმნება 4
PATCH /api/v1/assistants/{clientId} assistants:write კორპორატიული ასისტენტის პარამეტრების შეცვლა 4
GET /api/v1/usage usage:read კორპორატიული შეტყობინებები და ტოკენები ასისტენტების მიხედვით 5
პროდუქციის API
POST /api/v1/products products:write პროდუქციის კატალოგის სრული ჩანაცვლება; რაც არ გამოგიგზავნიათ, არქივდება 6
PATCH /api/v1/products products:write პროდუქციის კატალოგის ნაწილობრივი განახლება; არაფერი არქივდება 6
POST /api/v1/payments payments:write პროდუქციის შეკვეთა და გადახდის ბმული ან QR 7
POST /api/v1/payments/quickpay/webhook/{clientId} გასაღების გარეშე პროდუქციის ბანკის გამოძახება; თქვენ არ იძახებთ 7
ჩვენგან თქვენკენ — API არაა
POST თქვენი მისამართი გასაღების გარეშე ვებჰუკი ჯავშნის მოვლენები; ბიზნესი და ზემოთ ვებჰუკი

სარჩევი

ორი API

კორპორატიული APIპროდუქციის API
რას ხსნის ცოდნის ბაზა, მიმოწერა, ჯავშნები, ასისტენტები, ხარჯი კატალოგი (/products) და გადახდა (/payments)
პაკეტი კორპორატიული; სტარტი და ბიზნესი — უარი ნებისმიერი ფასიანი გეგმა, დანამატით
დანამატი არ სჭირდება პროდუქციის სინქრონიზაცია იმ ასისტენტზე, რომელშიც წერთ; კორპორატიულში შედის, სტარტსა და ბიზნესზე ცალკე იყიდება
გასაღები cxk_…, უფლებების სიით cxk_…
სად იქმნება ასისტენტი → ასისტენტის პარამეტრები → სხვა → API წვდომა ასისტენტი → პროდუქცია → API გასაღები
ბაზა https://www.botai.ge/api/v1 — ორივესთვის
უარი, როცა პირობა არ სრულდება 403 plan_required 403 addon_required
  • პროდუქციის ეკრანზე გაცემული გასაღები კორპორატიულ მისამართებს არ ხსნის; რას ხსნის გასაღები — უფლებებში.
  • ჯავშნის ვებჰუკი API არაა: ჩვენ გიგზავნით, გასაღები არ სჭირდება, გეგმა — ბიზნესი და ზემოთ. ვებჰუკი.

ავთენტიფიკაცია

სათაური — ყოველ მოთხოვნაზე
Authorization: Bearer cxk_Jt7q2sN9xVb0Lp4rWc8yZa3mEh6kFd1gTu5nQi2oXvA
გასაღებიწესი
ფორმატიcxk_…
ჩანსერთხელ, შექმნისას. ჩვენთან მხოლოდ მისი SHA-256 ინახება, ამიტომ ხელახლა ვერავინ გაჩვენებთ.
ასისტენტიერთი გასაღები — ერთი ასისტენტი. სამი ფილიალი — სამი გასაღები.
უფლებებიფიქსირდება გაცემისას: უფლებები.
დაკარგვა ან გაჟონვაახალი გასაღები, ძველის გაუქმება. გაუქმება მომდევნო მოთხოვნიდანვე მოქმედებს.
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დახურულია
bookings:readასისტენტის დონეGET /bookings, GET /bookings/busyდახურულია
bookings:writeასისტენტის დონეPOST /bookings, POST /bookings/{bookingId}/cancelდახურულია
products:writeასისტენტის დონეPOST / PATCH /products — პროდუქციის APIღიაა
payments:writeასისტენტის დონეPOST /payments — გადახდაღიაა
assistants:readანგარიშის დონეGET /assistantsდახურულია
assistants:writeანგარიშის დონეPOST /assistants, PATCH /assistants/{clientId}დახურულია
usage:readანგარიშის დონეGET /usageდახურულია
  • უფლებები გაცემისას ფიქსირდება. მეტი დაგჭირდათ — ახალი გასაღები ახალი უფლებებით, ძველი გააუქმეთ.
  • ანგარიშის დონის უფლებას კაბინეტი მხოლოდ იმ ანგარიშს აძლევს, რომელიც ამ ასისტენტს ფლობს.

ორი დონე

დონეგასაღები წვდებაასისტენტი მოთხოვნაშიუცხო ასისტენტი
ასისტენტის დონე მხოლოდ გასაღების ასისტენტს არ იწერება; გამოგზავნილი clientId იგნორირდება სხვა ასისტენტს ვერ ასახელებთ
ანგარიშის დონე ანგარიშის ყველა ასისტენტს clientId — გზაში, სხეულში ან ?clientId=-ში 404 assistant_not_found — იგივე პასუხი, რაც არარსებულს
  • ანგარიშის დონის უფლება ასისტენტის გასაღებს არ ეძლევა: მეორე ასისტენტის შექმნა და მეზობლის ხარჯის წაკითხვა მხოლოდ ამ უფლებით შეიძლება.
  • ასისტენტს მფლობელი ანგარიში თუ არ ჰყავს, ანგარიშის დონის მისამართები 403 no_account-ს პასუხობს.

გვერდები

ყველა სიის პასუხს ერთი და იგივე სამი ველი აქვს:

ველირას ამბობს
returnedრამდენი სტრიქონია ამ პასუხში.
truncatedfalse = სულ ეს არის. true = მეტი არსებობს, ვიდრე მოგივიდათ.
limitრეალურად გამოყენებული გვერდის ზომა, შემოკლების შემდეგ.

დანარჩენის მოსათხოვნად, სადაც ეს შეიძლება, პასუხს კიდევ ერთი ველი აქვს:

ველისადრა ქნათ
nextOffset/conversations, /contactsგაიმეორეთ ?offset= ამ რიცხვით.
nextFrom/bookings, /bookings/busyგაიმეორეთ ?from= ამ თარიღით. იმ თარიღის სტრიქონები ხელახლა მოვა — დუბლიკატები id-ით გაფილტრეთ.
nextBefore/handoffsგაიმეორეთ ?before= ამ დროით. ის წამიც ხელახლა მოვა — დუბლიკატები id-ით გაფილტრეთ.
maxLimit/conversations/{channel}/{userId}ყველაზე დიდი ?limit=, რასაც ეს მისამართი იღებს.
ზღვარიმნიშვნელობა
?limit=მაქსიმუმ 500, ნაგულისხმევი 100
ერთი მიმოწერის სტენოგრამა900-მდე
totalმხოლოდ იქ, სადაც ფაქტია: /conversations, /assistants. სხვაგან არ არის.
დაეყრდენით მხოლოდ truncated: false-ს. სისრულე returned < limit-იდან ნუ გამოიყვანთ.

1. ცოდნის ბაზა

ცოდნის ბაზის წაკითხვა და ჩაწერა · უფლებები knowledge:read, knowledge:write · ასისტენტის დონე · გეგმა: კორპორატიული.

ყველა მისამართზე ასევე: 401, 403, 429, 500 — შეცდომები და ლიმიტები.

თავები

თავიტიპიჩანაწერის ველები და ზღვრებიPOSTPATCH
descriptionstring—იცვლებაიცვლება
servicesarrayარაუმეტეს 200. name — სავალდებულო; description, price, priceUnit, durationMinutes — არა. price თავისუფალი ტექსტია („40 ₾“, „50 ₾-დან“); durationMinutes — დადებითი მთელი.მთლიანად იცვლებაname-ით ედრება, რეგისტრისა და დასაწყისსა თუ ბოლოში მდგარი ცარიელი სიმბოლოების გაუთვალისწინებლად: დამთხვევა ახლდება, დანარჩენი ემატება
faqarrayარაუმეტეს 300. question და answer — ორივე სავალდებულო.მთლიანად იცვლებაquestion-ით ედრება, იგივე წესით
rulesarray of stringარაუმეტეს 100. ცარიელი სტრიქონი არ შეიძლება.მთლიანად იცვლებაემატება, თუ უკვე არ დევს; არაფერი იშლება
documentsarrayარაუმეტეს 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-ით ვაბრუნებთ; ერთეულს თვითონ არ ვირჩევთ, რადგან ეს თქვენს ფასზე განცხადება იქნებოდა. თუ ფასი უბრალოდ ამ ერთი რამის ფასია, ნუ გამოგვიგზავნით — ასისტენტი მაშინ მხოლოდ ციფრს იტყვის.
GET /api/v1/knowledge knowledge:read

ცოდნის ბაზა მთლიანად.

მოთხოვნა
curl
curl https://www.botai.ge/api/v1/knowledge \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/knowledge", {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/knowledge",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "clientId": "demo-salon",
  "description": "სილამაზის სალონი ვაკეში, 2011 წლიდან.",
  "services": [{"name": "თმის შეჭრა", "price": "40 ₾", "durationMinutes": 45}],
  "faq": [{"question": "პარკინგი გაქვთ?", "answer": "დიახ, ეზოში."}],
  "rules": ["ფასს ნუ იგონებ — მხოლოდ სიიდან"],
  "documents": [
    {
      "id": "returns-policy",
      "label": "დაბრუნების წესები",
      "text": "დაბრუნება შესაძლებელია შეძენიდან 14 დღის განმავლობაში, ჩეკით.",
      "charCount": 61,
      "updatedAt": "2026-08-20T11:02:00.000Z"
    }
  ]
}

პასუხის ველები

ველიტიპიშენიშვნა
clientIdstringგასაღების ასისტენტი.
descriptionstringცარიელი სტრიქონი, თუ არ არის ჩაწერილი.
servicesarrayname, description, price, priceUnit, durationMinutes — რაც ჩაწერილია.
faqarrayquestion, answer.
rulesarray of string
documentsarrayმხოლოდ API-ით ჩაწერილი: id, label, text, charCount, updatedAt (ISO 8601). კაბინეტში ატვირთული არ ბრუნდება.

შეცდომები

სტატუსიerrorროდის
404assistant_not_foundგასაღების ასისტენტი აღარ არსებობს.
POST /api/v1/knowledge knowledge:write

დასახელებული თავი მთლიანად იცვლება; დაუსახელებელი რჩება, როგორც იყო.

მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/knowledge \
  -H "Authorization: Bearer $CORPORATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "description": "სილამაზის სალონი ვაკეში, 2011 წლიდან.",
      "services": [
        {"name": "თმის შეჭრა", "price": "40 ₾", "durationMinutes": 45},
        {"name": "მანიკური", "price": "30 ₾", "durationMinutes": 60}
      ],
      "documents": [
        {
          "id": "returns-policy",
          "label": "დაბრუნების წესები",
          "text": "დაბრუნება შესაძლებელია შეძენიდან 14 დღის განმავლობაში, ჩეკით."
        }
      ]
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/knowledge", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CORPORATE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    description: "სილამაზის სალონი ვაკეში, 2011 წლიდან.",
    services: [
      { name: "თმის შეჭრა", price: "40 ₾", durationMinutes: 45 },
      { name: "მანიკური", price: "30 ₾", durationMinutes: 60 },
    ],
    documents: [
      {
        id: "returns-policy",
        label: "დაბრუნების წესები",
        text: "დაბრუნება შესაძლებელია შეძენიდან 14 დღის განმავლობაში, ჩეკით.",
      },
    ],
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/knowledge",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    json={
        "description": "სილამაზის სალონი ვაკეში, 2011 წლიდან.",
        "services": [
            {"name": "თმის შეჭრა", "price": "40 ₾", "durationMinutes": 45},
            {"name": "მანიკური", "price": "30 ₾", "durationMinutes": 60},
        ],
        "documents": [
            {
                "id": "returns-policy",
                "label": "დაბრუნების წესები",
                "text": "დაბრუნება შესაძლებელია შეძენიდან 14 დღის განმავლობაში, ჩეკით.",
            },
        ],
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{
  "ok": true,
  "mode": "replace",
  "applied": ["description", "services", "documents"],
  "documents": {"written": 1, "removed": 2}
}

პასუხის ველები

ველიტიპიშენიშვნა
okbooleantrue — ყველაფერი ჩაიწერა. 502-ზე — false ან ველი არ არის.
modestringreplace.
appliedarray of stringრომელი თავები ჩაიწერა.
documents.writtenintegerჩაწერილი დოკუმენტები. ველი მხოლოდ მაშინაა, როცა სხეულში documents იყო.
documents.removedintegerწაშლილი API-დოკუმენტები: ისინი, რომლებიც სხეულში არ დაგისახელებიათ. კაბინეტში ატვირთულს არასოდეს ეხება.

შეცდომები

სტატუსიerrorროდის
400invalid_bodyსხეული ობიექტი არაა, ან თავი არასწორი ტიპისაა (field).
400nothing_to_writeსხეულმა არც ერთი თავი არ დაასახელა.
400invalid_service, invalid_extra_price, invalid_faq, invalid_rule, invalid_documentერთი ჩანაწერი გამრუდებულია; index და field ამბობს, რომელი.
400duplicate_document_idერთი id ერთ მოთხოვნაში ორჯერ.
404assistant_not_foundგასაღების ასისტენტი აღარ არსებობს.
409write_conflictწაკითხვასა და ჩაწერას შორის ასისტენტის კონფიგურაცია შეიცვალა. არაფერი ჩაწერილა; გაიმეორეთ.
413too_many_services, too_many_extra_prices, too_many_faq, too_many_rules, too_many_documentsთავის ზღვარს ზემოთ (200 / 20 / 300 / 100 / 10), ან ასისტენტის ცოდნის წყაროები სულ 10-ს აღემატება.
413document_too_longდოკუმენტის text 8 000 სიმბოლოზე გრძელია.
502write_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/knowledge knowledge:write

დასახელებული თავი არსებულში ერთვება; არაფერი იშლება. სხეული და შეცდომები — როგორც POST-ზე.

მოთხოვნა
curl
curl -X PATCH https://www.botai.ge/api/v1/knowledge \
  -H "Authorization: Bearer $CORPORATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "faq": [
        {
          "question": "ბარათით გადახდა შეიძლება?",
          "answer": "დიახ, ბარათითაც და ნაღდითაც."
        }
      ],
      "rules": ["სტუმარს ყოველთვის სახელით მიმართე"]
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/knowledge", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CORPORATE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    faq: [
      {
        question: "ბარათით გადახდა შეიძლება?",
        answer: "დიახ, ბარათითაც და ნაღდითაც.",
      },
    ],
    rules: ["სტუმარს ყოველთვის სახელით მიმართე"],
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.patch(
    "https://www.botai.ge/api/v1/knowledge",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    json={
        "faq": [
            {
                "question": "ბარათით გადახდა შეიძლება?",
                "answer": "დიახ, ბარათითაც და ნაღდითაც.",
            },
        ],
        "rules": ["სტუმარს ყოველთვის სახელით მიმართე"],
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{"ok": true, "mode": "merge", "applied": ["faq", "rules"]}

პასუხის ველები

ველიტიპიშენიშვნა
ok, appliedboolean, array of stringროგორც POST-ზე.
modestringmerge.
documents.writtenintegerმხოლოდ თუ სხეულში documents იყო. removed PATCH-ის პასუხში არ არის: PATCH არაფერს შლის.
  • თავების შერწყმის წესი — ცხრილში ზემოთ.
  • ცარიელი მასივი PATCH-ზე არაფერს ცვლის. description PATCH-ზეც მთლიანად იცვლება; "" მას ასუფთავებს.

2. მიმოწერა და ლიდები

მიმოწერა, კონტაქტები და ოპერატორის მოთხოვნები · უფლება conversations:read (ოთხივე მისამართზე) · ასისტენტის დონე · გეგმა: კორპორატიული.

ყველა მისამართზე ასევე: 401, 403, 429, 500 — შეცდომები და ლიმიტები.

GET /api/v1/conversations conversations:read

მიმოწერების სია: თითო სტრიქონი თითო წყვილზე (channel, userId), ბოლო აქტივობით წინ.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
limitintegerარა100 · მაქს. 500გამრუდებული მნიშვნელობა — ნაგულისხმევი, შეცდომა არა.
offsetintegerარა0გამრუდებული მნიშვნელობა — 0.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/conversations?limit=50&offset=0" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ limit: 50, offset: 0 });
const res = await fetch(`https://www.botai.ge/api/v1/conversations?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/conversations",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"limit": 50, "offset": 0},
    timeout=30,
)
print(res.json())
პასუხი
{
  "conversations": [
    {
      "channel": "website",
      "userId": "w_8fa2c41e",
      "lastMessage": "მადლობა!",
      "lastRole": "user",
      "lastTimestamp": "2026-08-22T09:14:02.000Z",
      "messageCount": 11
    }
  ],
  "returned": 1,
  "total": 1,
  "limit": 50,
  "offset": 0,
  "truncated": false,
  "countsComplete": true
}

პასუხის ველები

ველიტიპიშენიშვნა
conversations[]arraychannel, userId, lastMessage, lastRole (user ან assistant), lastTimestamp (ISO 8601), messageCount.
returned, limit, offsetintegerრამდენი მოვიდა; გამოყენებული გვერდის ზომა; საიდან.
totalintegerსიის სიგრძე. countsComplete: false-ზე — ქვედა ზღვარი.
truncatedbooleantrue — მეტი არსებობს, ან countsComplete: false.
nextOffsetintegerმხოლოდ თუ მომდევნო გვერდი არსებობს: გაიმეორეთ ?offset= ამ რიცხვით.
countsCompletebooleanfalse — სკანირება ზღვარზე გაჩერდა (იხ. ქვემოთ).
  • countsComplete: false ერთადერთი ზღვარია, რომელსაც offset ვერ გასცდება. სია ასისტენტის შენახული შეტყობინებების სკანირებით შენდება და სკანირება 20 000 სტრიქონზე ჩერდება. მის იქით total და ყოველი messageCount ქვედა ზღვარია, ყველაზე ძველი მიმოწერა სიაში არაა.
  • მთელი ისტორია გჭირდებათ — მოგვწერეთ.
GET /api/v1/conversations/{channel}/{userId} conversations:read

ერთი მიმოწერის სტენოგრამა, ძველიდან ახლისკენ.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
channelstringდიახ (გზაში)—conversations[].channel-ის მნიშვნელობა.
userIdstringდიახ (გზაში)—conversations[].userId-ის მნიშვნელობა; URL-ში კოდირდება.
limitintegerარა100 · მაქს. 900იმდენი ბოლო შეტყობინება ბრუნდება.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/conversations/website/w_8fa2c41e?limit=100" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ limit: 100 });
const res = await fetch(`https://www.botai.ge/api/v1/conversations/website/w_8fa2c41e?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/conversations/website/w_8fa2c41e",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"limit": 100},
    timeout=30,
)
print(res.json())
პასუხი
{
  "channel": "website",
  "userId": "w_8fa2c41e",
  "messages": [
    {"role": "user", "text": "როდის ხართ ღია?", "timestamp": "2026-08-22T09:12:41.000Z"},
    {
      "role": "assistant",
      "text": "ყოველდღე 10:00-20:00.",
      "timestamp": "2026-08-22T09:12:43.000Z"
    }
  ],
  "returned": 2,
  "limit": 100,
  "truncated": false
}

პასუხის ველები

ველიტიპიშენიშვნა
channel, userIdstringროგორც გზაში გაიგზავნა.
messages[]arrayrole (user ან assistant), text, timestamp (ISO 8601). ძველიდან ახლისკენ.
returned, limitinteger
truncatedbooleantrue — მიმოწერას დასაწყისი აკლია: ბრუნდება ბოლო limit შეტყობინება.
maxLimitintegerმხოლოდ truncated: true-ზე: 900.
  • 900 შემთხვევითი რიცხვი არაა: ერთი მიმოწერისთვის ზუსტად ამდენი შეტყობინება ინახება, ამიტომ ?limit=900 ყოველთვის მთელ მიმოწერას აბრუნებს.
  • total არ არსებობს; truncated: false ნიშნავს, რომ returned მთელი მიმოწერაა.
  • არარსებული წყვილი — 200 ცარიელი messages-ით, 404 არა.
GET /api/v1/contacts conversations:read

საკონტაქტო ბაზა: თითო სტრიქონი თითო ადამიანზე — იმით, რაც მასზე მიმოწერიდან, ჯავშნებიდან, ოპერატორის მოთხოვნებიდან და შეფასებებიდან ვიცით.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
limitintegerარა100 · მაქს. 500გამრუდებული მნიშვნელობა — ნაგულისხმევი, შეცდომა არა.
offsetintegerარა0გამრუდებული მნიშვნელობა — 0.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/contacts?limit=50&offset=0" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ limit: 50, offset: 0 });
const res = await fetch(`https://www.botai.ge/api/v1/contacts?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/contacts",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"limit": 50, "offset": 0},
    timeout=30,
)
print(res.json())
პასუხი
{
  "contacts": [
    {
      "channel": "website",
      "userId": "w_8fa2c41e",
      "name": "ნინო",
      "phone": "+995555123456",
      "email": null,
      "firstSeen": "2026-08-20T10:02:11.000Z",
      "lastSeen": "2026-08-22T09:14:02.000Z",
      "messageCount": 11,
      "inboundMessageCount": 6,
      "lastMessageAt": "2026-08-22T09:14:02.000Z",
      "bookingCount": 1,
      "cancelledCount": 0,
      "rejectedCount": 0,
      "pendingCount": 1,
      "attendedCount": 0,
      "noShowCount": 0,
      "unknownOutcomeCount": 0,
      "needsAttendanceCount": 0,
      "lastBookingAt": null,
      "nextBookingAt": "2026-09-01T08:00:00.000Z",
      "lastBookingMadeAt": "2026-08-22T09:00:00.000Z",
      "oldestPendingAt": "2026-08-22T09:00:00.000Z",
      "lastAttendedAt": null,
      "handoffCount": 0,
      "lastHandoffAt": null,
      "reviewCount": 0,
      "reviewRating": null,
      "reviewComment": null,
      "lastReviewAt": null,
      "manualStage": null,
      "manualStageSource": null,
      "manualNote": null,
      "manualUpdatedAt": null
    }
  ],
  "returned": 1,
  "offset": 0,
  "limit": 50,
  "truncated": false
}

პასუხის ველები

ველიტიპიშენიშვნა
contacts[]arrayერთი ადამიანი — ველები ქვემოთ.
returned, offset, limitinteger
truncatedbooleantrue — მეტი არსებობს.
nextOffsetintegerმხოლოდ truncated: true-ზე: გაიმეორეთ ?offset= ამ რიცხვით.

contacts[]-ის ველები

ველიტიპიშენიშვნა
channel, userIdstringადამიანის გასაღები. იგივე წყვილი, რაც /conversations-შია.
name, phone, emailstring | nullბოლო არაცარიელი მნიშვნელობა ჯავშნებიდან — სხვა წყარო არ არსებობს.
firstSeen, lastSeenISO 8601 | nullპირველი და ბოლო კონტაქტი: შეტყობინება, ჯავშანი, ოპერატორის მოთხოვნა, შეფასება.
messageCount, inboundMessageCountintegerყველა შეტყობინება; მხოლოდ მისი დაწერილი.
lastMessageAtISO 8601 | null
bookingCountintegerმოქმედი ჯავშნები: გაუუქმებელი და უარყოფილი არა.
pendingCountintegerbookingCount-ის ნაწილი, რომელიც დადასტურებას ელოდება.
cancelledCount, rejectedCountinteger
attendedCount, noShowCountintegerადამიანმა მონიშნა „მოვიდა“ / „არ მოვიდა“.
unknownOutcomeCountintegerწარსული მოქმედი ჯავშნები, რომლებზეც არავის არაფერი მიუთითებია. ეს „არ მოვიდა“ არ არის.
needsAttendanceCountintegerწინა ველის ნაწილი, რომელზეც კაბინეტი ჯერ კიდევ ითხოვს პასუხს.
lastBookingAt, nextBookingAtISO 8601 | nullბოლო წარსული და უახლოესი მომავალი ვიზიტი.
lastBookingMadeAt, oldestPendingAt, lastAttendedAtISO 8601 | nullროდის მოითხოვა ჯავშანი უკანასკნელად; უძველესი უპასუხო მოთხოვნა; ბოლო დასწრება.
handoffCount, lastHandoffAtinteger, ISO 8601 | nullოპერატორის მოთხოვნები.
reviewCount, reviewRating, reviewComment, lastReviewAtinteger, number | null, string | null, ISO 8601 | nullშეფასებები; რეიტინგი და კომენტარი — ბოლო შეფასებისაა, არა საუკეთესოსი.
manualStagestring | nullnew, qualified, booked, won, lost — CRM-ის ეტაპი, რომელიც ადამიანმა ან სისტემამ დააყენა.
manualStageSourcestring | nullmanual — ადამიანმა; system — ჯავშნის გამო ავტომატურად.
manualNote, manualUpdatedAtstring | null, ISO 8601 | nullშენიშვნა და მისი დრო.

შეცდომები

სტატუსიerrorროდის
500contacts_unreadableსაკონტაქტო ბაზა ვერ წავიკითხეთ. ეს განზრახ არ არის ცარიელი სია: „კონტაქტები არ არის“ და „ვერ წავიკითხეთ“ სხვადასხვა ფაქტია. თუ გაგრძელდა — მოგვწერეთ.
GET /api/v1/handoffs conversations:read

სად ითხოვა კლიენტმა ადამიანი. ახლიდან ძველისკენ.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
limitintegerარა100 · მაქს. 500გამრუდებული მნიშვნელობა — ნაგულისხმევი, შეცდომა არა.
openstringარა—true — მხოლოდ უპასუხოები (acknowledgedAt: null). სხვა მნიშვნელობა — ყველა.
sincestringარა—ISO 8601; ჩათვლით.
beforestringარა—ISO 8601; ჩათვლით. გამრუდებული — 400.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/handoffs?limit=100&open=true" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ limit: 100, open: "true" });
const res = await fetch(`https://www.botai.ge/api/v1/handoffs?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/handoffs",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"limit": 100, "open": "true"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "handoffs": [
    {
      "id": 4471,
      "channel": "whatsapp",
      "userId": "995555123456",
      "reason": "ოპერატორი",
      "createdAt": "2026-08-22T08:40:00.000Z",
      "acknowledgedAt": null,
      "acknowledgedBy": null
    }
  ],
  "returned": 1,
  "limit": 100,
  "truncated": false
}

პასუხის ველები

ველიტიპიშენიშვნა
handoffs[]arrayid, channel, userId, reason, createdAt, acknowledgedAt, acknowledgedBy.
acknowledgedAt, acknowledgedByISO 8601 | null, string | nullnull — კლიენტი ჯერ კიდევ ელოდება. „შეტყობინება გაიგზავნა“ და „ვიღაცამ აიღო“ ორი სხვადასხვა ფაქტია.
returned, limitinteger
truncatedbooleantrue — ყველაზე ძველები ვერ ჩაეტია.
nextBeforestringმხოლოდ truncated: true-ზე: გაიმეორეთ ?before= ამ დროით.

შეცდომები

სტატუსიerrorროდის
400invalid_requestbefore ISO 8601 არ არის.
  • before ჩათვლითია, ამიტომ სასაზღვრო სტრიქონი ხელახლა მოვა: დუბლიკატები id-ით გაფილტრეთ. ორ მოთხოვნას წამის მეათასედამდე ერთი და იგივე createdAt შეიძლება ჰქონდეს, ამიტომ ზღვარი ჩათვლითია.
  • truncated: true და nextBefore არ არის — გვერდი ერთმა დროის ნიშნულმა აავსო, კურსორი ვერ წაინაცვლებს.

3. ჯავშნები

ჯავშნების წაკითხვა, დაკავებული დრო, ახალი ჯავშანი და გაუქმება · უფლებები bookings:read, bookings:write · ასისტენტის დონე · გეგმა: კორპორატიული.

ყველა მისამართზე ასევე: 401, 403, 429, 500 — შეცდომები და ლიმიტები.

თარიღი და დრო თბილისის დროით იკითხება (Asia/Tbilisi).

GET /api/v1/bookings bookings:read

ჯავშნების სია თარიღისა და დროის მიხედვით, დღის მსვლელობის რიგით.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
fromstringარა—YYYY-MM-DD, ჩათვლით.
tostringარა—YYYY-MM-DD, ჩათვლით.
limitintegerარა100 · მაქს. 500გამრუდებული მნიშვნელობა — ნაგულისხმევი, შეცდომა არა.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/bookings?from=2026-09-01&to=2026-09-07" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ from: "2026-09-01", to: "2026-09-07" });
const res = await fetch(`https://www.botai.ge/api/v1/bookings?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/bookings",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"from": "2026-09-01", "to": "2026-09-07"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "bookings": [
    {
      "id": 91,
      "channel": "api",
      "userId": "crm-4471",
      "service": "თმის შეჭრა",
      "date": "2026-09-01",
      "time": "12:00",
      "customerName": "ნინო",
      "customerPhone": "5xx xx xx xx",
      "customerEmail": null,
      "staffName": null,
      "cancelled": false,
      "pendingApproval": true,
      "rejectedReason": null,
      "attended": null,
      "createdAt": "2026-08-22T09:00:00.000Z"
    }
  ],
  "returned": 1,
  "limit": 100,
  "truncated": false
}

პასუხის ველები

ველიტიპიშენიშვნა
bookings[]arrayველები ქვემოთ.
returned, limitinteger
truncatedbooleantrue — მეტი არსებობს.
nextFromstringმხოლოდ truncated: true-ზე: გაიმეორეთ ?from= ამ თარიღით.

bookings[]-ის ველები

ველიტიპიშენიშვნა
idintegerჯავშნის ერთადერთი ზუსტი სახელური; გაუქმებაში ის იწერება გზაში.
channelstringapi — თქვენი ჩანაწერი; სხვა — კლიენტის არხი.
userIdstring
service, date, timestringdate — YYYY-MM-DD, time — HH:MM.
customerName, customerPhone, customerEmail, staffNamestring | null
cancelledbooleantrue — გაუქმებული; სიაში მაინც რჩება.
pendingApprovalbooleantrue — მფლობელის დადასტურებას ელოდება.
rejectedReasonstring | nullმაღაზიის სიტყვებით; არა-null — ჯავშანი უარყოფილია და cancelled: true-ცაა.
attendedboolean | nulltrue — ადამიანმა მონიშნა „მოვიდა“; false — „არ მოვიდა“; null — არავის არაფერი დაუფიქსირებია.
createdAtstringISO 8601.

შეცდომები

სტატუსიerrorროდის
400invalid_requestfrom ან to YYYY-MM-DD არ არის.
  • attended: null „არ მოვიდა“ არ არის და ასე არ უნდა შემოიტანოთ.
  • nextFrom-ის თარიღის სტრიქონები ხელახლა მოვა: დუბლიკატები id-ით გაფილტრეთ. truncated: true და nextFrom არ არის — გვერდი ერთმა დღემ აავსო: ითხოვეთ ვიწრო from / to.
GET /api/v1/bookings/busy bookings:read

დაკავებული დრო — და არაფერი იმაზე, ვინ დაიკავა. განრიგისთვის, რომელიც კითხულობს „როდის მცალია“. hours ბლოკი კვირის სამუშაო საათებსაც აბრუნებს, რომ POST /bookings-ის ორი უარი — closed_day და outside_hours — წინასწარ შემოწმდეს.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
fromstringარადღეს (თბილისის დროით)YYYY-MM-DD, ჩათვლით.
tostringარაfromYYYY-MM-DD, ჩათვლით.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/bookings/busy?from=2026-09-01&to=2026-09-07" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ from: "2026-09-01", to: "2026-09-07" });
const res = await fetch(`https://www.botai.ge/api/v1/bookings/busy?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/bookings/busy",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"from": "2026-09-01", "to": "2026-09-07"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "from": "2026-09-01",
  "to": "2026-09-07",
  "busy": [
    {
      "date": "2026-09-01",
      "time": "12:00",
      "service": "თმის შეჭრა",
      "staffName": null,
      "pendingApproval": true
    }
  ],
  "returned": 1,
  "limit": 500,
  "truncated": false,
  "source": "bookings",
  "hours": {
    "timeZone": "Asia/Tbilisi",
    "mustFinishInside": true,
    "week": {
      "mon": { "state": "open", "opens": "10:00", "closes": "20:00" },
      "tue": { "state": "open", "opens": "10:00", "closes": "20:00" },
      "wed": { "state": "open", "opens": "10:00", "closes": "20:00" },
      "thu": { "state": "open", "opens": "10:00", "closes": "20:00" },
      "fri": { "state": "open", "opens": "10:00", "closes": "20:00" },
      "sat": { "state": "unknown" },
      "sun": { "state": "closed" }
    }
  }
}

პასუხის ველები

ველიტიპიშენიშვნა
from, tostringგამოყენებული ფანჯარა.
busy[]arraydate, time, service, staffName (string | null), pendingApproval (boolean). მოქმედი ჯავშნები: გაუუქმებელი და უარყოფილი არა; დადასტურების მომლოდინეც შედის.
returned, limitintegerlimit ყოველთვის 500.
truncatedbooleantrue — ფანჯარა 500 ჯავშანზე დიდია.
nextFromstringმხოლოდ truncated: true-ზე.
sourcestringbookings — მხოლოდ ჩვენი ცხრილი.
hoursobjectბიზნესის სამუშაო კვირა: timeZone (ყოველთვის Asia/Tbilisi), mustFinishInside და week. შეიძლება საერთოდ არ მოვიდეს — იხილეთ ქვემოთ.
hours.weekobjectშვიდი გასაღები: mon…sun. თითოეულს აქვს state: open (მაშინ opens და closes, HH:MM), closed, ან unknown.
hours.mustFinishInsidebooleanყოველთვის true: ჯავშანი სამუშაო საათებში უნდა დასრულდეს, და არა მხოლოდ დაიწყოს.

შეცდომები

სტატუსიerrorროდის
400invalid_requestfrom ან to YYYY-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/bookings bookings:write

ახალი ჯავშანი. ასისტენტის საკუთარი პარამეტრი „ჯავშანს ჩემი დადასტურება სჭირდება“ ამ მოთხოვნაზეც მოქმედებს.

სხეულის ველები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
userIdstringდიახ—კლიენტის იდენტიფიკატორი თქვენს სისტემაში; ამით უკავშირდება ჯავშანი კონტაქტსა და მიმოწერას.
servicestringდიახ—
datestringდიახ—YYYY-MM-DD.
timestringდიახ—24-საათიანი HH:MM.
customerName, customerPhone, customerEmail, staffNamestringარა—
channelstringარაapiდატოვეთ api: პროდუქტში ყოველი დათვლა და ექსპორტი არხით ჯგუფდება, website კი ჯავშანს ვიჯეტის ციფრებში ჩათვლიდა.
მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/bookings \
  -H "Authorization: Bearer $CORPORATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "userId": "crm-4471",
      "service": "თმის შეჭრა",
      "date": "2026-09-01",
      "time": "12:00",
      "customerName": "ნინო",
      "customerPhone": "5xx xx xx xx"
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/bookings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CORPORATE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    userId: "crm-4471",
    service: "თმის შეჭრა",
    date: "2026-09-01",
    time: "12:00",
    customerName: "ნინო",
    customerPhone: "5xx xx xx xx",
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/bookings",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    json={
        "userId": "crm-4471",
        "service": "თმის შეჭრა",
        "date": "2026-09-01",
        "time": "12:00",
        "customerName": "ნინო",
        "customerPhone": "5xx xx xx xx",
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი · 201 Created
{
  "ok": true,
  "date": "2026-09-01",
  "time": "12:00",
  "service": "თმის შეჭრა",
  "pendingApproval": true
}

პასუხის ველები

ველიტიპიშენიშვნა
okbooleantrue.
date, time, servicestringროგორც ჩაიწერა.
pendingApprovalbooleantrue — ჯავშანი მფლობელის დადასტურებას ელოდება და კალენდარში ჯერ არ ჩანს. მნიშვნელობა ასისტენტის პარამეტრიდან მოდის (ნაგულისხმევად ჩართულია) და მოთხოვნით არ იცვლება.

შეცდომები

სტატუსიerrorროდის
400invalid_bodyსხეული ობიექტი არაა.
400invalid_bookinguserId, service, date ან time აკლია ან გამრუდებულია; field ამბობს, რომელი.
404assistant_not_foundგასაღების ასისტენტი აღარ არსებობს.
409slot_takenდრო უკვე დაკავებულია — ჩვენს დღიურში ან კალენდარში. არაფერი დაჯავშნულა; აირჩიეთ სხვა.
409closed_dayბიზნესი იმ დღეს დაკეტილია. იმავე თარიღზე ხელახლა ცდა იმავე პასუხს დააბრუნებს. წინასწარ: hours.week-ში ამ დღის state არის closed.
409outside_hoursდრო სამუშაო საათებს სცდება, ან ჯავშანი მათ დასრულებამდე არ სრულდება. იმავე დროზე ხელახლა ცდა იმავე პასუხს დააბრუნებს. წინასწარ: hours.week-ის opens/closes.
  • დაკავებულობა ყოველ ჩაწერაზე მოწმდება — 2026-09-20-მდე მხოლოდ მაშინ მოწმდებოდა, როცა ასისტენტს Google Calendar ჰქონდა მიბმული და pendingApproval false იყო, ანუ ყველაზე დატვირთული შემთხვევა — დასადასტურებლად მდგარი მოთხოვნების რიგი — სწორედ ის იყო, რასაც არაფერი ამოწმებდა. ახლა ჩვენი საკუთარი ჯავშნები და 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).
POST /api/v1/bookings/{bookingId}/cancel bookings:write

ჯავშნის გაუქმება, როცა კლიენტმა ის თქვენს სისტემაში გააუქმა: ჯავშანი გაუქმებულად აღინიშნება, Google Calendar-ის ჩანაწერი იშლება, შეხსენება ჩერდება, GET /bookings/busy დროს აღარ ითვლის.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
bookingIdintegerდიახ (გზაში)—id, რომელსაც GET /bookings აბრუნებს. სხეული არ არის.
მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/bookings/91/cancel \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/bookings/91/cancel", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/bookings/91/cancel",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{
  "ok": true,
  "cancelled": true,
  "alreadyCancelled": false,
  "booking": {
    "id": 91,
    "channel": "api",
    "userId": "crm-4471",
    "service": "თმის შეჭრა",
    "date": "2026-09-01",
    "time": "12:00",
    "staffName": null
  }
}

პასუხის ველები

ველიტიპიშენიშვნა
ok, cancelledbooleanორივე true.
alreadyCancelledbooleantrue — ჯავშანი უკვე გაუქმებული იყო.
bookingobjectid, channel, userId, service, date, time, staffName.

შეცდომები

სტატუსიerrorროდის
400invalid_requestbookingId დადებითი მთელი არაა.
404booking_not_foundამ ასისტენტს ასეთი ჯავშანი არ ეკუთვნის. სხვისი და არარსებული id ერთნაირად პასუხობს.
409ambiguous_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/assistants assistants:read

ყველა ასისტენტი, რომელსაც ეს ანგარიში ფლობს. არ იფურცლება.

მოთხოვნა
curl
curl https://www.botai.ge/api/v1/assistants \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/assistants", {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/assistants",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "assistants": [
    {
      "clientId": "demo-salon",
      "name": "დემო სალონი",
      "active": true,
      "language": "ka",
      "tone": "friendly, concise",
      "requireBookingApproval": true
    }
  ],
  "returned": 1,
  "total": 1,
  "truncated": false
}

პასუხის ველები

ველიტიპიშენიშვნა
assistants[]arrayclientId, name, active (boolean), language, tone, requireBookingApproval (boolean).
returnedintegerრამდენი ასისტენტი დაბრუნდა.
totalintegerრამდენს ასახელებს ანგარიშის ჩანაწერი.
truncatedbooleantrue — returned total-ზე ნაკლებია.
  • სხვაობა returned-სა და total-ს შორის ნიშნავს, რომ ანგარიში ისეთ ასისტენტს ასახელებს, რომელიც აღარ არსებობს. ხელახალი მოთხოვნა ამას არ ცვლის — მოგვწერეთ.
POST /api/v1/assistants assistants:write

ასისტენტის შექმნა. გამორთული იქმნება: ჯერ გამართეთ (ცოდნა და არხები — კაბინეტში), მერე ჩართეთ PATCH-ით.

სხეულის ველები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
clientIdstringდიახ—პატარა ლათინური ასოები, ციფრები და ერთმაგი დეფისი; დიდი ასო პატარად გადაიქცევა. მუდმივია — მოგვიანებით არ იცვლება.
namestringდიახ—სახელი, რომლითაც ეს ფილიალი წარადგენს თავს.
activebooleanარაfalseჩართულად შესაქმნელად უნდა იყოს true; სხვა ყველა მნიშვნელობა — გამორთული.
templateNamestringარა—საწყისი შაბლონის სახელი; გამოტოვებულზე ნაგულისხმევი შაბლონი გამოიყენება. უცნობი სახელი — create_failed.
მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/assistants \
  -H "Authorization: Bearer $CORPORATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"clientId": "demo-salon-2", "name": "დემო სალონი 2"}'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/assistants", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CORPORATE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ clientId: "demo-salon-2", name: "დემო სალონი 2" }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/assistants",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    json={"clientId": "demo-salon-2", "name": "დემო სალონი 2"},
    timeout=30,
)
print(res.status_code, res.json())
პასუხი · 201 Created
{
  "assistant": {
    "clientId": "demo-salon-2",
    "name": "დემო სალონი 2",
    "active": false,
    "language": "ka"
  },
  "botLimit": 5,
  "botCount": 4
}

პასუხის ველები

ველიტიპიშენიშვნა
assistantobjectclientId, name, active, language.
botLimitinteger | nullანგარიშისთვის შეთანხმებული ასისტენტების რაოდენობა; შეთანხმებამდე — გეგმის მინიმუმი; null — შეუზღუდავი.
botCountintegerასისტენტები ანგარიშზე ახლის ჩათვლით.

შეცდომები

სტატუსიerrorროდის
400invalid_client_idclientId აკლია ან დასაშვები სახის არაა.
400invalid_requestname აკლია.
400create_failedასისტენტი ვერ შეიქმნა: clientId სხვა ანგარიშს უკავია, ან ასეთი templateName არ არსებობს. მიზეზს პასუხი განზრახ არ ასახელებს.
403bot_limit_reachedასისტენტების ლიმიტს ზემოთ. პასუხში: plan, botLimit, botCount.
409client_id_takenამ ანგარიშს ასეთი clientId-ის ასისტენტი უკვე ჰყავს.
502attach_failedასისტენტი შეიქმნა, მაგრამ ანგარიშზე ვერ მიემაგრა. არ გაიმეოროთ — გამეორება create_failed-ს დააბრუნებს; მოგვწერეთ.
  • ლიმიტი ანგარიშისაა და ისე მოქმედებს, როგორც კაბინეტში.
  • მოთხოვნები ერთმანეთის მიმართ ატომური არაა: ორი ერთდროული შექმნა ორივე გაივლის ლიმიტის შემოწმებას. შექმენით რიგრიგობით.
PATCH /api/v1/assistants/{clientId} assistants:write

ასისტენტის პარამეტრების შეცვლა. სხეულში მხოლოდ ქვემოთ ჩამოთვლილი ველები დაიშვება.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
clientIdstringდიახ (გზაში)—ასისტენტი, რომელიც ამ ანგარიშს ეკუთვნის.
namestringარა—არაცარიელი.
descriptionstringარა—ცარიელი სტრიქონი აცარიელებს.
activebooleanარა—true ჩართავს ასისტენტს.
tone, languagestringარა—არაცარიელი.
requireBookingApprovalbooleanარა—true — ჯავშანს მფლობელის დადასტურება სჭირდება.
მოთხოვნა
curl
curl -X PATCH https://www.botai.ge/api/v1/assistants/demo-salon-2 \
  -H "Authorization: Bearer $CORPORATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"active": true}'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/assistants/demo-salon-2", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.CORPORATE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ active: true }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.patch(
    "https://www.botai.ge/api/v1/assistants/demo-salon-2",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    json={"active": True},
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{
  "assistant": {
    "clientId": "demo-salon-2",
    "name": "დემო სალონი 2",
    "active": true,
    "language": "ka",
    "tone": "friendly, concise",
    "requireBookingApproval": true
  }
}

პასუხის ველები

ველიტიპიშენიშვნა
assistantobjectცვლილების შემდეგ: clientId, name, active, language, tone, requireBookingApproval.

შეცდომები

სტატუსიerrorროდის
400unsupported_fieldსხეულში ისეთი ველია, რომელსაც ეს მისამართი არ ცვლის; პასუხში fields ჩამოთვლის. არხის მონაცემები (Telegram-ის ბოტის ტოკენი, Meta-ს გვერდის ტოკენი) აქედან განზრახ არ იცვლება — ისინი კაბინეტში რჩება.
400nothing_to_writeსხეულმა არც ერთი ველი არ დაასახელა.
400invalid_requestველის მნიშვნელობა არასწორი ტიპისაა ან ცარიელია; field ამბობს, რომელი.
404assistant_not_foundამ ანგარიშს ასეთი ასისტენტი არ ეკუთვნის.
409write_conflictწაკითხვასა და ჩაწერას შორის კონფიგურაცია შეიცვალა. არაფერი ჩაწერილა; წაიკითხეთ და გაიმეორეთ.

5. ხარჯის მიკუთვნება — ანგარიშის დონე

შეტყობინებები და ტოკენები ასისტენტების მიხედვით · უფლება usage:read · ანგარიშის დონე · გეგმა: კორპორატიული.

ყველა მისამართზე ასევე: 401, 403 (მათ შორის no_account), 429, 500 — შეცდომები და ლიმიტები.

GET /api/v1/usage usage:read

შეტყობინებები და ტოკენები ასისტენტების მიხედვით.

პარამეტრები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
fromstringარამიმდინარე თვის პირველი დღეYYYY-MM-DD, ჩათვლით.
tostringარადღესYYYY-MM-DD, ჩათვლით. ერთი მოთხოვნა მაქსიმუმ 92 დღეს ფარავს.
clientIdstringარაანგარიშის ყველა ასისტენტიერთ ასისტენტამდე ავიწროებს; ვერასოდეს აფართოებს.
მოთხოვნა
curl
curl "https://www.botai.ge/api/v1/usage?from=2026-08-01&to=2026-08-22" \
  -H "Authorization: Bearer $CORPORATE_API_KEY"
JavaScript
const params = new URLSearchParams({ from: "2026-08-01", to: "2026-08-22" });
const res = await fetch(`https://www.botai.ge/api/v1/usage?${params}`, {
  headers: { Authorization: `Bearer ${process.env.CORPORATE_API_KEY}` },
});
console.log(await res.json());
Python
import os
import requests

res = requests.get(
    "https://www.botai.ge/api/v1/usage",
    headers={"Authorization": f"Bearer {os.environ['CORPORATE_API_KEY']}"},
    params={"from": "2026-08-01", "to": "2026-08-22"},
    timeout=30,
)
print(res.json())
პასუხი
{
  "from": "2026-08-01",
  "to": "2026-08-22",
  "assistants": [
    {
      "clientId": "demo-salon",
      "messages": 4120,
      "calls": 4408,
      "inputTokens": 5203991,
      "outputTokens": 412884,
      "cacheWriteTokens": 74110,
      "cacheReadTokens": 4711002,
      "unmeasuredCalls": 3,
      "byModel": [
        {
          "provider": "anthropic",
          "model": "claude-haiku-4-5",
          "calls": 4408,
          "inputTokens": 5203991,
          "outputTokens": 412884,
          "cacheWriteTokens": 74110,
          "cacheReadTokens": 4711002,
          "unmeasuredCalls": 3
        }
      ]
    }
  ]
}

პასუხის ველები

ველიტიპიშენიშვნა
from, tostringგამოყენებული ფანჯარა.
assistants[]arrayთითო სტრიქონი ანგარიშის (ან clientId-ით არჩეულ) თითო ასისტენტზე, ნულებითაც.
messagesintegerკლიენტის დაწერილი შეტყობინებები — ერთეული, რომლითაც გეგმა იყიდება და რომლის მიმართაც ლიმიტი მოქმედებს.
callsintegerმოდელის გამოძახებები სულ, გაუზომავის ჩათვლით.
inputTokens, outputTokens, cacheWriteTokens, cacheReadTokensintegerრაც პასუხებმა ჩვენ დაგვიჯდა. გაზომილი გამოძახებების ჯამია.
unmeasuredCallsintegercalls-ის ის ნაწილი, რომლის ტოკენებიც ვერ გავზომეთ. ეს უფასო გამოძახებები არაა.
byModel[]arrayიგივე ციფრები მოდელების მიხედვით: provider, model, calls, ოთხი ტოკენის ველი, unmeasuredCalls. ცარიელია, თუ გამოძახება არ ყოფილა.

შეცდომები

სტატუსიerrorროდის
400invalid_requestfrom ან to YYYY-MM-DD არ არის, ან from to-ზე გვიანაა.
400window_too_longფანჯარა 92 დღეზე გრძელია. პასუხში maxDays.
404assistant_not_foundclientId ამ ანგარიშს არ ეკუთვნის.
413window_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, კატალოგს არაფერი ეშლება. უფლებების სიის გარეშე გაცემული გასაღები (პროდუქციის ეკრანზე) ისევ წერს. დანამატი და უფლება ორი ცალკე შემოწმებაა: ორივეს დადებითი პასუხი სჭირდება.
სხეულიJSON, არაუმეტეს 1 000 000 ბაიტი (413 body_too_large).
ზღვრებიპროდუქტი მოთხოვნაში — 500; ვარიანტი პროდუქტზე — 100, მოთხოვნაში — 5 000.
სიხშირე60 მოთხოვნა წუთში ასისტენტზე; 120 — IP-დან.
ფულიმთელი თეთრი: 18999 = 189.99 ₾.

სხეულის ველები

პარამეტრიტიპისავალდებულონაგულისხმევი · ზღვარიშენიშვნა
productsarrayდიახ1–500პროდუქტები. ცარიელი მასივი — 400 empty_catalogue.
sourceIdintegerარა—ასისტენტის კატალოგის წყარო. გამოტოვებულზე გამოიყენება ამ ასისტენტის საკუთარი „კატალოგი API-დან“ წყარო (პირველ მოთხოვნაზე იქმნება). პასუხში ბრუნდება. სხვისი — 404 source_not_found.

products[]-ის ველები

ველიტიპისავალდებულოზღვარიშენიშვნა
idstring | integerდიახ—პროდუქტის id თქვენს სისტემაში: მისით ვცნობთ პროდუქტს შემდეგ ჩაწერაზე და ვახლებთ მას; მეორე ასლი არ იქმნება. სინონიმი — externalId.
namestringდიახ500 სიმბოლოPATCH-ზეც სავალდებულოა.
descriptionstringარა20 000 სიმბოლო
categorystringარა500 სიმბოლო
imageUrlstringარა2 000 სიმბოლოსინონიმი — image.
statusstringარა—active (ნაგულისხმევი), hidden, archived. პროდუქტის სტატუსი ვარიანტებიდან გამოითვლება: ერთი აქტიური ვარიანტი პროდუქტს აქტიურად ტოვებს.
variantsarrayარა100თუ პროდუქტს ერთი ფორმა აქვს, გამოტოვეთ: sku, ფასი, მარაგი და attributes თვით პროდუქტზე იწერება.

products[].variants[]-ის ველები

ველიტიპისავალდებულოზღვარიშენიშვნა
idstring | integerპირობით—ვარიანტის id; თუ არაა — sku. ერთვარიანტიან პროდუქტს შეუძლია პროდუქტის id-ის გამოყენება; რამდენიმე ვარიანტიანს — არა: id ან sku სავალდებულოა. სინონიმი — externalId.
skustringარა200 სიმბოლო
priceTetriinteger | string | nullარა2 147 483 647ფასი; წესები — ქვემოთ. სინონიმი — price.
salePriceTetriinteger | string | nullარა2 147 483 647ფასდაკლებული ფასი; ითვლება მხოლოდ ძირითად ფასთან ერთად. სინონიმი — salePrice.
stockinteger | string | nullარა2 147 483 647მარაგი; წესები — ქვემოთ.
statusstringარა—როგორც პროდუქტზე.
attributesobjectარა20 გასაღებიმნიშვნელობა — string ან რიცხვი: {"ზომა": "42", "ფერი": "შავი"}.

ფასი და მარაგი

მნიშვნელობაროგორ იკითხება
ფასი, JSON-რიცხვი18999 = 18999 თეთრი. წილადი (189.99) — 400 invalid_product, შეცდომაში 18999 წერია. უარყოფითი — 400.
ფასი, JSON-სტრიქონიიკითხება ისე, როგორც ფიდის ფასის სვეტი: "189.99", "189,99", "1 899,00 GEL", "1,899.00".
ფასი სხვა ვალუტაში"45 USD" — პროდუქტი რჩება, ფასი ვარდება, პასუხში warnings-ში ჩნდება foreign_currency. ვალუტას არაფერი გადაჰყავს.
stock: null ან ველი არ არის„ამას ცალობით არ ვთვლით“: ასისტენტი პროდუქტს ხელმისაწვდომად თვლის.
stock: 0„დავთვალეთ, აღარაა“: ასისტენტი ამბობს, რომ მარაგი ამოიწურა.
stock, სტრიქონი"in stock", "out of stock" ან რიცხვი სტრიქონად.
POST /api/v1/products products:write

სრული ჩანაცვლება: სხეული ამ წყაროს მთელი კატალოგია. რაც არ დაასახელეთ, არქივდება და არ იშლება. შესაფერისია ღამის ან საათობრივი სრული ექსპორტისთვის.

მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/products \
  -H "Authorization: Bearer $PRODUCTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "products": [
        {
          "id": "SKU-1041",
          "name": "ტყავის ჩექმა",
          "category": "ფეხსაცმელი",
          "variants": [
            {
              "id": "1041-42",
              "sku": "1041-42",
              "attributes": {"ზომა": "42", "ფერი": "შავი"},
              "priceTetri": 18999,
              "stock": 3
            },
            {
              "id": "1041-43",
              "sku": "1041-43",
              "attributes": {"ზომა": "43", "ფერი": "შავი"},
              "priceTetri": 18999,
              "stock": 0
            }
          ]
        },
        {
          "id": "SKU-2007",
          "name": "ტყავის ქამარი",
          "category": "აქსესუარი",
          "priceTetri": 4500,
          "stock": null
        }
      ]
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/products", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PRODUCTS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    products: [
      {
        id: "SKU-1041",
        name: "ტყავის ჩექმა",
        category: "ფეხსაცმელი",
        variants: [
          {
            id: "1041-42",
            sku: "1041-42",
            attributes: { "ზომა": "42", "ფერი": "შავი" },
            priceTetri: 18999,
            stock: 3,
          },
          {
            id: "1041-43",
            sku: "1041-43",
            attributes: { "ზომა": "43", "ფერი": "შავი" },
            priceTetri: 18999,
            stock: 0,
          },
        ],
      },
      {
        id: "SKU-2007",
        name: "ტყავის ქამარი",
        category: "აქსესუარი",
        priceTetri: 4500,
        stock: null,
      },
    ],
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/products",
    headers={"Authorization": f"Bearer {os.environ['PRODUCTS_API_KEY']}"},
    json={
        "products": [
            {
                "id": "SKU-1041",
                "name": "ტყავის ჩექმა",
                "category": "ფეხსაცმელი",
                "variants": [
                    {
                        "id": "1041-42",
                        "sku": "1041-42",
                        "attributes": {"ზომა": "42", "ფერი": "შავი"},
                        "priceTetri": 18999,
                        "stock": 3,
                    },
                    {
                        "id": "1041-43",
                        "sku": "1041-43",
                        "attributes": {"ზომა": "43", "ფერი": "შავი"},
                        "priceTetri": 18999,
                        "stock": 0,
                    },
                ],
            },
            {
                "id": "SKU-2007",
                "name": "ტყავის ქამარი",
                "category": "აქსესუარი",
                "priceTetri": 4500,
                "stock": None,
            },
        ],
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{
  "ok": true,
  "mode": "replace",
  "sourceId": 7,
  "created": 2,
  "updated": 0,
  "archived": 5,
  "variants": 3,
  "warnings": []
}

პასუხის ველები

ველიტიპიშენიშვნა
okbooleantrue. 502 write_failed-ზე — false.
modestringreplace.
sourceIdintegerწყარო, რომელშიც ჩაიწერა.
created, updatedintegerახალი და განახლებული პროდუქტები.
archivedintegerდაარქივებული პროდუქტები — ისინი, რომლებიც ამ წყაროში იყო და მოთხოვნაში არ დასახელდა.
variantsintegerჩაწერილი ვარიანტები.
warnings[]arraycode, message (ქართულად), index (პროდუქტის ნომერი, თუ ვრცელდება).

შეცდომები

სტატუსიerrorროდის
400invalid_bodyსხეული ობიექტი არაა; products მასივი არაა; sourceId დადებითი მთელი არაა (field).
400empty_catalogueproducts ცარიელია.
400invalid_productპროდუქტი ან ვარიანტი გამრუდებულია. index — პროდუქტის ნომერი მასივში (0-დან), field — ველის გზა, მაგ. variants[0].priceTetri.
400duplicate_product_id, duplicate_variant_idერთი id ერთსა და იმავე მოთხოვნაში ორჯერ (ერთი პროდუქტის შიგნით — ვარიანტისა).
400too_many_variantsერთ პროდუქტზე 100-ზე მეტი ვარიანტია.
404source_not_foundამ ასისტენტს ასეთი წყარო არ ეკუთვნის.
413too_many_productsერთ მოთხოვნაში 500-ზე მეტი პროდუქტი.
413too_many_variantsერთ მოთხოვნაში 5 000-ზე მეტი ვარიანტი.
413body_too_largeსხეული 1 000 000 ბაიტზე დიდია; პასუხში maxBytes.
502write_failedნაწილი ჩაიწერა, ნაწილი — არა. პასუხში ჩანს, რა ჩაიწერა (created, updated, variants); არაფერი დაარქივებულა.
  • ცარიელი POST კატალოგს არ ცლის (empty_catalogue): ეს თითქმის ყოველთვის თქვენი ექსპორტის შეცდომაა. გასაყიდიდან მოსახსნელი პროდუქტი გამოგზავნეთ "status": "archived"-ით.
  • 500-ზე დიდი კატალოგი: გაგზავნეთ გვერდები PATCH-ით, გასაყიდიდან მოსახსნელი პროდუქტები კი — ბოლოს, PATCH-ით "status": "archived"-ით. ბოლო გვერდზე POST ყველაფერს დაარქივებდა, გარდა ამ გვერდისა.
  • 502 write_failed: იგივე მოთხოვნა უსაფრთხოდ მეორდება — პროდუქტი id-ით ახლდება.
PATCH /api/v1/products products:write

ნაწილობრივი განახლება: მხოლოდ დასახელებული პროდუქტი ახლდება ან ემატება; არაფერი არქივდება. სხეული და შეცდომები — როგორც POST-ზე.

მოთხოვნა
curl
curl -X PATCH https://www.botai.ge/api/v1/products \
  -H "Authorization: Bearer $PRODUCTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "products": [
        {
          "id": "SKU-1041",
          "name": "ტყავის ჩექმა",
          "variants": [{"id": "1041-42", "priceTetri": 16999, "stock": 2}]
        }
      ]
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/products", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.PRODUCTS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    products: [
      {
        id: "SKU-1041",
        name: "ტყავის ჩექმა",
        variants: [{ id: "1041-42", priceTetri: 16999, stock: 2 }],
      },
    ],
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.patch(
    "https://www.botai.ge/api/v1/products",
    headers={"Authorization": f"Bearer {os.environ['PRODUCTS_API_KEY']}"},
    json={
        "products": [
            {
                "id": "SKU-1041",
                "name": "ტყავის ჩექმა",
                "variants": [{"id": "1041-42", "priceTetri": 16999, "stock": 2}],
            },
        ],
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი
{
  "ok": true,
  "mode": "upsert",
  "sourceId": 7,
  "created": 0,
  "updated": 1,
  "variants": 1,
  "warnings": []
}

პასუხის ველები

ველიტიპიშენიშვნა
modestringupsert.
archived—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-დან.

სხეულის ველები

ველიტიპისავალდებულოზღვარიშენიშვნა
variantIdstring | integerდიახ128 სიმბოლოვარიანტის id კატალოგში — პროდუქციის API-ის id (ან sku).
productIdstring | integerარა128 სიმბოლომხოლოდ ავიწროებს: საჭიროა, როცა ერთი variantId ამ მაღაზიაში ორ პროდუქტზეა.
quantityintegerარა1–1000 · ნაგულისხმევი 1
methodstringარაonline (ნაგულისხმევი), codcod — მიტანისას გადახდა.
orderRefstringარაA-Za-z0-9_-, 4–64 სიმბოლოშეკვეთის ნომერი და იდემპოტენტურობის გასაღები. გამოტოვეთ — ჩვენ დავარქმევთ.
customerobjectარაname 120, phone 40, email 160მყიდველი. გრძელი მნიშვნელობა იჭრება.
descriptionstringარა200 სიმბოლობარათით გადახდაზე Quickpay-ს ეგზავნება. გრძელი იჭრება.
notestringარა500 სიმბოლოინახება შეკვეთაზე. გრძელი იჭრება.
amountTetriintegerარა—მხოლოდ შესამოწმებლად: კატალოგის ჯამს უნდა ემთხვეოდეს.
currencystringარა—მხოლოდ შესამოწმებლად: კატალოგის ვალუტას უნდა ემთხვეოდეს.
POST /api/v1/payments payments:write

ხსნის შეკვეთას და ქმნის გადახდას მაღაზიის საკუთარ Quickpay-ანგარიშზე. online-ზე ბრუნდება გადახდის ბმული და მისი QR.

მოთხოვნა
curl
curl -X POST https://www.botai.ge/api/v1/payments \
  -H "Authorization: Bearer $PRODUCTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "variantId": "1041-42",
      "quantity": 1,
      "method": "online",
      "orderRef": "ORD-10482",
      "customer": {"name": "ნინო", "phone": "+995555100200"}
    }'
JavaScript
const res = await fetch("https://www.botai.ge/api/v1/payments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PRODUCTS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    variantId: "1041-42",
    quantity: 1,
    method: "online",
    orderRef: "ORD-10482",
    customer: { name: "ნინო", phone: "+995555100200" },
  }),
});
console.log(res.status, await res.json());
Python
import os
import requests

res = requests.post(
    "https://www.botai.ge/api/v1/payments",
    headers={"Authorization": f"Bearer {os.environ['PRODUCTS_API_KEY']}"},
    json={
        "variantId": "1041-42",
        "quantity": 1,
        "method": "online",
        "orderRef": "ORD-10482",
        "customer": {"name": "ნინო", "phone": "+995555100200"},
    },
    timeout=30,
)
print(res.status_code, res.json())
პასუხი · 201 Created
{
  "ok": true,
  "orderId": 812,
  "orderRef": "ORD-10482",
  "method": "online",
  "outcome": "pending",
  "amountTetri": 18999,
  "amountLabel": "189.99 ₾",
  "currency": "GEL",
  "message": "გადახდის ბმული მზადაა — გახსენით და გადაიხადეთ.",
  "checkoutUrl": "https://pay.example.com/checkout/3b1f9c1e",
  "qrSvg": "<svg …>",
  "paymentUuid": "3b1f9c1e-7a52-4d0e-9a41-5f6a2c8d0b17",
  "variantId": "1041-42",
  "quantity": 1,
  "unitAmountTetri": 18999,
  "replayed": false
}
პასუხი · method: cod · 201 Created
{
  "ok": true,
  "orderId": 813,
  "orderRef": "ORD-10483",
  "method": "cod",
  "outcome": "awaiting_offline",
  "amountTetri": 18999,
  "amountLabel": "189.99 ₾",
  "currency": "GEL",
  "message": "შეკვეთა მიღებულია — თანხას მიტანისას გადაიხდით.",
  "variantId": "1041-42",
  "quantity": 1,
  "unitAmountTetri": 18999
}

პასუხის ველები

ველიტიპიშენიშვნა
okbooleantrue.
orderId, orderRefinteger, stringშეკვეთა ჩვენთან და მისი ნომერი.
methodstringonline ან cod.
outcomestringpending — online-ის გახსნისას; awaiting_offline — cod-ისას. 200-ზე (გამეორება) — შეკვეთის ახლანდელი მდგომარეობა, მაგ. paid.
amountTetri, amountLabel, currencyinteger, string, stringჯამი კატალოგიდან: ფასი × რაოდენობა.
messagestringქართულად; მყიდველისთვის მზა ტექსტი.
checkoutUrl, qrSvg, paymentUuidstringმხოლოდ online-ზე. qrSvg — იგივე ბმულის QR; null, თუ ვერ დაიხაზა.
testKeyNoticestringმხოლოდ თუ მაღაზიას Quickpay-ის სატესტო გასაღები აქვს: ფული არ ჩამოიჭრება.
variantId, quantity, unitAmountTetristring, integer, integerრა და რა ფასად დაფასდა.
replayedbooleantrue — Quickpay-მ იგივე orderRef-ზე უკვე გახსნილი გადახდა დააბრუნა. cod-ის 201-ზე ველი არ არის.

შეცდომები

სტატუსიerrorროდის
400bad_requestსხეული ობიექტი არაა.
400variant_requiredvariantId არ მოვიდა ან წასაკითხი არაა.
400bad_method, bad_quantity, bad_amount, bad_currency, bad_order_ref, bad_product_idერთი ველი გამრუდებულია; field ამბობს, რომელი. amountTetri მთელი თეთრია — 149.99 აქ შეცდომაა.
403addon_requiredასისტენტს პროდუქციის დანამატი არ აქვს.
403scope_requiredგასაღებს payments:write არ აქვს; scope ასახელებს მას.
404variant_not_foundამ ასისტენტის კატალოგში ასეთი ვარიანტი არაა. სხვისი ვარიანტისთვისაც იგივე პასუხია.
409variant_ambiguousერთი id-ით ამ მაღაზიაში ორი ვარიანტი მოიძებნა. დააზუსტეთ productId-ით.
409variant_not_for_sale, variant_unpricedვარიანტი გაყიდვიდან მოხსნილია, ან ფასი მას არ უწერია.
409amount_mismatch, currency_mismatchსხეულმა თანხა ან ვალუტა დაასახელა და კატალოგს არ დაემთხვა. არაფერი შექმნილა.
409amount_not_chargeableჯამი (ფასი × რაოდენობა) ნულია ან 2 147 483 647 თეთრს აღემატება.
409currency_not_chargeableამ ვალუტით ბარათის გადახდა არ იქმნება. chargeableIn ჩამოთვლის, რითი იქმნება.
409cod_disabledმაღაზია მიტანისას გადახდას არ იღებს. methods — რა დარჩა: ["online"], ან [], როცა ბარათით გადახდაც მომართული არაა (მაშინ გადახდის გზას მაღაზია თავად ადასტურებს).
409payment_account_missing, payment_key_unreadableQuickpay ჯერ არ დაუკავშირებიათ, ან შენახული გასაღები ვეღარ იკითხება. მფლობელის მოსაგვარებელია.
409order_ref_takenამ ნომერზე უკვე სხვა გაყიდვაა გახსნილი. აირჩიეთ ახალი ნომერი.
400, 404, 409, 429, 502payment_providerQuickpay-მ უარი თქვა ან არ პასუხობს. failure ასახელებს მიზეზს, message ქართულად წერია; currency — თუ უარი ვალუტას ეხება.
502order_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. გადახდა, შეცდომების დამუშავება.

სტატუსიerrorრა მოხდა
400invalid_bodyსხეული ობიექტი არაა, ან თავი არასწორი ტიპისაა.
400nothing_to_writeსხეულმა არც ერთი თავი არ დაასახელა.
400invalid_service / invalid_extra_price / invalid_faq / invalid_rule / invalid_documentერთი ჩანაწერი გამრუდებულია. index და field ამბობს, რომელი. extraPrices-ში სტრიქონს label-იც სჭირდება და price-იც; ნახევრად შევსებულს არ ვაგდებთ — მთელ მოთხოვნას ვუარყოფთ.
400duplicate_document_idერთსა და იმავე მოთხოვნაში ერთი id ორჯერ მოვიდა.
400invalid_bookingჯავშნის სავალდებულო ველი აკლია ან გამრუდებულია.
400invalid_requestპარამეტრი, გზის ნაწილი ან სავალდებულო ველი გამრუდებულია: ჯავშნის id, რომელიც დადებითი მთელი არაა; name, რომელიც POST /assistants-ს აკლია.
400invalid_productპროდუქციის API: ერთი პროდუქტი ან ვარიანტი გამრუდებულია. index და field ამბობს, რომელი.
400empty_catalogueპროდუქციის API: products ცარიელია. ცარიელი POST კატალოგს არ ცლის — ეს თითქმის ყოველთვის თქვენი ექსპორტის შეცდომაა. გასაყიდიდან მოსახსნელს "status": "archived"-ით გამოგზავნით.
400duplicate_product_id / duplicate_variant_idპროდუქციის API: ერთი id ერთსა და იმავე მოთხოვნაში ორჯერ.
400invalid_client_idclientId დასაშვები სახის არაა.
400create_failedასისტენტი ვერ შეიქმნა: clientId სხვა ანგარიშს უკავია, ან ასეთი templateName არ არსებობს. მიზეზს პასუხი არ ასახელებს.
400unsupported_fieldPATCH /assistants-ს ისეთი ველი გაეგზავნა, რომელსაც ის არ ცვლის.
400window_too_longხარჯის ფანჯარა 92 დღეზე გრძელია.
401unauthorizedგასაღები აკლია, უცნობია, გამრუდებულია ან გაუქმებულია.
403plan_requiredანგარიში კორპორატიულ გეგმაზე არაა.
403scope_requiredგასაღებს არ აქვს ის უფლება, რომელსაც ეს მისამართი ითხოვს. scope ასახელებს მას.
403addon_requiredასისტენტს პროდუქციის დანამატი არ აქვს.
403no_accountგასაღების ასისტენტს მფლობელი ანგარიში არ ჰყავს, ამიტომ ანგარიშის დონის მისამართები მისთვის მიუწვდომელია.
403bot_limit_reachedანგარიშის ასისტენტების ლიმიტს ზემოთ.
404assistant_not_foundამ ანგარიშს ასეთი ასისტენტი არ ეკუთვნის.
404booking_not_foundამ ასისტენტს ასეთი ჯავშანი არ ეკუთვნის.
404source_not_foundამ ასისტენტს ასეთი კატალოგის წყარო არ ეკუთვნის.
409write_conflictPOST / PATCH /knowledge და PATCH /assistants/{clientId}: წაკითხვასა და ჩაწერას შორის ასისტენტის კონფიგურაცია შეიცვალა. არაფერი ჩაწერილა — არც კონფიგურაცია, არც დოკუმენტები. წაიკითხეთ ახლანდელი მდგომარეობა და გაგზავნეთ ხელახლა.
409client_id_takenPOST /assistants: ამ ანგარიშს ასეთი clientId-ის ასისტენტი უკვე ჰყავს.
409slot_takenდრო შემოწმებასა და ჩაწერას შორის სხვამ დაიკავა. არაფერი დაჯავშნულა.
409closed_dayბიზნესი იმ დღეს დაკეტილია — ეს რბოლა არაა და ხელახლა ცდა არ გამოასწორებს. წინასწარ ჩანს GET /bookings/busy-ის hours ბლოკში.
409outside_hoursდრო სამუშაო საათებს სცდება — ეს რბოლა არაა და ხელახლა ცდა არ გამოასწორებს. წინასწარ ჩანს GET /bookings/busy-ის hours ბლოკში.
409ambiguous_bookingგაუქმება ერთზე მეტ ჯავშანს შეეხებოდა. არაფერი გაუქმებულა.
413body_too_largeსხეული ზღვარზე დიდია.
413too_many_services / too_many_extra_prices / too_many_faq / too_many_rules / too_many_documentsთავის ზღვარს ზემოთ (200 / 20 / 300 / 100 / 10).
413document_too_longცოდნის დოკუმენტი 8 000 სიმბოლოზე გრძელია.
413too_many_productsპროდუქციის API: ერთ მოთხოვნაში 500-ზე მეტი პროდუქტი.
400 / 413too_many_variantsპროდუქციის API: ერთ პროდუქტზე ან ერთ მოთხოვნაში ვარიანტების ზღვარს ზემოთ.
413window_too_largeიმ ფანჯარაში იმაზე მეტი გამოძახებაა, ვიდრე ერთ მოთხოვნას წაკითხვა შეუძლია.
429rate_limitedწუთში ძალიან ბევრი მოთხოვნა ამ ასისტენტისთვის. retryAfterMs ამბობს, რამდენს დაელოდოთ.
429too_many_attemptsწუთში ძალიან ბევრი მოთხოვნა ამ IP-დან, დათვლილი მანამ, სანამ გასაღებს შევხედავთ.
500internal_errorჩვენია. გაიმეორეთ.
500contacts_unreadableGET /contacts: საკონტაქტო ბაზა ვერ წავიკითხეთ. ეს განზრახ არ არის ცარიელი სია — „კონტაქტები არ არის" და „ვერ წავიკითხეთ" სხვადასხვა ფაქტია. გაგრძელდა — მოგვწერეთ.
502write_failedჩაწერის ნაწილი შესრულდა, ნაწილი — არა; არაფერი წაშლილა. პასუხი ამბობს, რა ჩაიწერა: applied და failed (ცოდნის ბაზა), created და updated (კატალოგი). იგივე მოთხოვნა უსაფრთხოდ მეორდება.
502attach_failedასისტენტი შეიქმნა, მაგრამ ანგარიშზე ვერ მივამაგრეთ. ეს არ გაიმეოროთ — მოგვწერეთ; გამეორება მხოლოდ create_failed-ს დააბრუნებს.
უარყოფილი სხეული არაფერს წერს. ვალიდაცია პირველ ჩაწერამდე მუშაობს: 400-ის შემდეგ ყველაფერი ისეა, როგორც იყო — იმავე მოთხოვნის წესრიგში მყოფი თავებიც არ ჩაწერილა.
ლიმიტიკორპორატიული APIპროდუქციის API
სხეული512 000 ბაიტი1 000 000 ბაიტი
მოთხოვნა წუთში, ასისტენტზე12060
მოთხოვნა წუთში, IP-დან120120
სიის გვერდი500 სტრიქონი500 პროდუქტი

ორი 429 — ორი განცალკევებული მთვლელი: too_many_attempts — მისამართზე (გასაღების შემოწმებამდე), rate_limited — ასისტენტზე. ერთის გადავსება მეორეს არ ხარჯავს.

შეცდომების დამუშავება

გამეორების წესი კოდზეა და არა სტატუსზე: ერთი და იგივე 502 ხან მეორდება, ხან არა.

სტატუსი და errorგამეორება?მოქმედება
429 rate_limited, too_many_attempts დიახ დაელოდეთ პასუხის სხეულის retryAfterMs მილიწამს და გაიმეორეთ იგივე მოთხოვნა.
500 internal_error დიახ გაიმეორეთ ექსპონენციალური პაუზით (0,5 წმ, 1, 2, 4…), შეზღუდული რაოდენობით. POST /payments-ზე ყოველთვის გაგზავნეთ orderRef: იგივე orderRef იგივე გადახდას აბრუნებს და მეორეს არ ქმნის.
500 contacts_unreadable მოგვიანებით ჩვენი მხარის ხარვეზია. პასუხი ცარიელ სიად ნუ ჩაითვლება. თუ გაგრძელდა — მოგვწერეთ.
502 write_failed დიახ, უცვლელად ჩაწერა ნაწილობრივ შესრულდა, არაფერი წაშლილა. პასუხი ამბობს, რა ჩაიწერა; მოთხოვნა იდემპოტენტურია.
502 order_not_saved დიახ, იმავე orderRef-ით გადახდა შეიქმნა, ჩვენთან ვერ ჩაიწერა. იმავე orderRef-ით გამეორება იმავე გადახდას აბრუნებს. თუ orderRef არ გამოგიგზავნიათ, ის პასუხშია: გაიმეორეთ მისით.
502 attach_failed არა ასისტენტი შეიქმნა და ანგარიშზე ვერ მიემაგრა. გამეორება create_failed-ს დააბრუნებს. მოგვწერეთ.
409 write_conflict წაკითხვის შემდეგ არაფერი ჩაწერილა. GET /knowledge ან GET /assistants და გაგზავნეთ ხელახლა.
409 slot_taken არა, უცვლელად არაფერი დაჯავშნულა. აირჩიეთ სხვა დრო.
409 closed_day არა, უცვლელად არაფერი დაჯავშნულა. სხვა დღე აირჩიეთ — იმავე თარიღზე ცდა ყოველთვის იმავეს დააბრუნებს. რომელი დღეებია დაკეტილი, GET /bookings/busy-ის hours ამბობს.
409 outside_hours არა, უცვლელად არაფერი დაჯავშნულა. სამუშაო საათებში აირჩიეთ დრო — იმავე საათზე ცდა ყოველთვის იმავეს დააბრუნებს. საათები GET /bookings/busy-ის hours-შია.
409 ambiguous_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-19v1 — ამჟამინდელი კონტრაქტი

Google Calendar — ასისტენტის ჩანაწერი

ჩვენი ჩანაწერის ამოცნობა და დაკავებული დრო მფლობელის Google Calendar-ში · გეგმა: ბიზნესი და ზემოთ · API გასაღები არ გამოიყენება.

ჩანაწერის ველები

ველიტიპიშენიშვნა
summarystring<სერვისი> — <კლიენტის სახელი>. სახელის გარეშე: უცნობი მომხმარებელი.
descriptionstringსტრიქონები: ტელეფონი: … (ტელეფონის გარეშე -), არხი: …, მოთხოვნილი დრო: YYYY-MM-DD HH:MM; სპეციალისტის თხოვნისას ასევე მოთხოვნილი სპეციალისტი: ….
start, endobjectdateTime UTC-შია (Z), timeZone — Asia/Tbilisi. end = start + სერვისის ხანგრძლივობა; ხანგრძლივობის გარეშე — 60 წუთი.
transparencystringყოველთვის opaque. transparent ჩანაწერი freebusy-ის პასუხში საერთოდ არ ჩანს — არც დაკავებულად, არც თავისუფლად.
extendedProperties.privateobjectოთხი გასაღები, იხ. ქვემოთ.

extendedProperties.private

გასაღებიმნიშვნელობადანიშნულება
botaiSourcebotai.ge/bookingმუდმივი ნიშანი. ფილტრი ამაზე დაწერეთ.
botaiClientIdასისტენტის იდენტიფიკატორი, მაგ. demo-salonერთ კალენდარში რამდენიმე ასისტენტი წერს — ასე გაარჩევთ.
botaiChanneltelegram, 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": "ტელეფონი: +995555000000\nარხი: telegram\nმოთხოვნილი დრო: 2026-09-14 14:30",
  "transparency": "opaque",
  "start": {"dateTime": "2026-09-14T10:30:00.000Z", "timeZone": "Asia/Tbilisi"},
  "end": {"dateTime": "2026-09-14T11:15:00.000Z", "timeZone": "Asia/Tbilisi"},
  "extendedProperties": {
    "private": {
      "botaiSource": "botai.ge/booking",
      "botaiClientId": "demo-salon",
      "botaiChannel": "telegram",
      "botaiRequested": "2026-09-14 14:30"
    }
  }
}
  • ფილტრი 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-ით.cancelled
webhook.testკაბინეტში დააჭირეთ შემოწმებას. სანიმუშო მონაცემებია (სატესტო სერვისი, 2030-01-01, 12:00, website), ნამდვილი ჯავშანი არა.confirmed
  • დასადასტურებელი ჯავშანი ორ მოვლენას იძლევა: booking.created (pending), შემდეგ booking.approved ან booking.rejected. დადასტურების გარეშე ჯავშანი — ერთს: booking.created (confirmed).
  • უცნობი event გამოტოვეთ და მაინც უპასუხეთ 2xx: სია გაიზრდება.
მოთხოვნის სათაურები
POST /hooks/botai HTTP/1.1
Host: example.ge
content-type: application/json; charset=utf-8
user-agent: BotAI-Webhooks/1.0 (+https://botai.ge)
x-botai-event: booking.created
x-botai-delivery: 0f7b1d2c-9a44-4e13-8b56-2c9f0e5a7d31
x-botai-timestamp: 1789000000
x-botai-signature: v1=66d7ff203a2cb9f512441e6fbbce1f43d8724346f421ff786cacd925ce067d73
სათაურიმნიშვნელობა
x-botai-eventმოვლენის სახელი, იგივე რაც event სხეულში. სხეულის წაკითხვამდე გასანაწილებლად.
x-botai-deliveryUUID, უნიკალური მცდელობაზე; მცდელობა ერთია, ამიტომ უნიკალურია მოვლენაზეც. იდემპოტენტობისთვის შეინახეთ.
x-botai-timestampUnix-წამები ხელმოწერის მომენტში. ხელმოწერილი მასალის ნაწილია.
x-botai-signaturev1= და 64 პატარა თექვსმეტობითი სიმბოლო.
მოთხოვნის სხეული
{
  "event": "booking.created",
  "deliveryId": "0f7b1d2c-9a44-4e13-8b56-2c9f0e5a7d31",
  "sentAt": "2026-09-10T00:26:40.480Z",
  "assistantId": "demo-salon",
  "booking": {
    "service": "თმის შეჭრა",
    "date": "2026-09-14",
    "time": "14:30",
    "timezone": "Asia/Tbilisi",
    "durationMinutes": 45,
    "staff": "ნინო",
    "staffRequested": null,
    "channel": "telegram",
    "status": "confirmed",
    "calendarEventId": "6k3q1m0p9r7s2t4u",
    "rejectedReason": null,
    "createdAt": "2026-09-10T00:26:39.000Z"
  }
}

სხეულის ველები

ველიტიპიშენიშვნა
eventstringზემოთ ჩამოთვლილი ხუთიდან ერთი.
deliveryIdstringUUID v4; იგივე, რაც x-botai-delivery.
sentAtstringISO 8601, UTC. როდის ავაწყვეთ.
assistantIdstringასისტენტის იდენტიფიკატორი — ის, რაც კაბინეტის მისამართშია. არასოდეს ანგარიშის.
booking.servicestringზუსტად ისე, როგორც მაღაზიამ სერვისების სიაში დაწერა.
booking.datestringYYYY-MM-DD, booking.timezone-ში და არა UTC-ში.
booking.timestringHH:MM, 24-საათიანი, booking.timezone-ში.
booking.timezonestringდღეს ყოველთვის Asia/Tbilisi. წაიკითხეთ და ნუ ივარაუდებთ.
booking.durationMinutesnumberსერვისის ხანგრძლივობა; 60, თუ არ არის მითითებული. იგივე, რაც Google Calendar-ის ჩანაწერს აქვს.
booking.staffstring | nullსპეციალისტი, ვისთანაც ჯავშანია. null — ბიზნესს სპეციალისტების სია არ აქვს.
booking.staffRequestedstring | nullსპეციალისტი, რომელიც კლიენტმა ითხოვა სიის არმქონე ბიზნესში. ბაზაში ცალკე არ ინახება, ამიტომ მხოლოდ booking.created-ზეა; შემდეგ მოვლენებზე null კლიენტის უარს არ ნიშნავს.
booking.channelstringwebsite, telegram, whatsapp, instagram, messenger. ჩვენი არხი და არა კლიენტი.
booking.statusstringconfirmed | pending | cancelled | rejected. სტატუსი ამ მოვლენის შემდეგ.
booking.calendarEventIdstring | nullGoogle Calendar-ის ჩანაწერის id (არა კალენდრის). null — ჩანაწერი არ არსებობს, მაგ. დასადასტურებელ ჯავშანზე.
booking.rejectedReasonstring | nullმაღაზიის საკუთარი სიტყვები, booking.rejected-ზე.
booking.createdAtstringISO 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())
ტესტ-ვექტორი
secret:    whsec_demo_only_not_a_real_secret
timestamp: 1789000000
body:      {"event":"booking.created","deliveryId":"0f7b1d2c-9a44-4e13-8b56-2c9f0e5a7d31","sentAt":"2026-09-10T00:26:40.480Z","assistantId":"demo-salon","booking":{"service":"თმის შეჭრა","date":"2026-09-14","time":"14:30","timezone":"Asia/Tbilisi","durationMinutes":45,"staff":"ნინო","staffRequested":null,"channel":"telegram","status":"confirmed","calendarEventId":"6k3q1m0p9r7s2t4u","rejectedReason":null,"createdAt":"2026-09-10T00:26:39.000Z"}}
signature: v1=66d7ff203a2cb9f512441e6fbbce1f43d8724346f421ff786cacd925ce067d73
  • 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.
  • კალენდრის პერიოდულ კითხვას ნუ შეაწყვეტთ.

კითხვა გაჩნდა, ან რაღაც ამ გვერდზე კოდს არ ემთხვევა? დაგვიკავშირდით — და დაგვიწერეთ, რომელი მისამართი იყო.