API для разработчиков
API только для чтения к помощнику вашего магазина: ответы, подсказки по мере набора, похожие товары и каталог — для вашего сервера, приложения или страницы.
Версия API 1.0.0
Обзор#
API даёт вашему коду то, что помощник на сайте даёт покупателям: полный ответ на вопрос обычными словами, подсказки товаров по мере набора, списки похожих товаров и чтение каталога. Базовый адрес: https://assistant.onesite.agency/api/v1.
Оно только для чтения и ограничено одним магазином: ключ открывает магазин, который его создал, и ничего больше. Названия, цены, наличие и ссылки всегда берутся из каталога; ИИ пишет лишь короткие тексты say и why, и ответ их помечает (см. «Текст, созданный ИИ» ниже).
Запросы и ответы — JSON поверх HTTPS. Версия указана в пути (/v1). API доступно на тарифах, которые включают API для разработчиков.
Быстрый старт#
От нуля до первого ответа за три шага.
1. Создайте ключ#
В кабинете откройте вкладку Developers, назовите ключ и выберите его права (scopes), при желании — список IP и источники (origins) для браузера. Ключ показывается один раз, у нас хранится только его хэш: сразу скопируйте его в секреты сервера, но не в репозиторий. Потерянный ключ не восстановить: отзовите его и создайте другой.
2. Проверьте#
Вызовите GET /me. Ответ 200 покажет, какой магазин открывает ключ, его права и лимиты. Подставьте свой ключ вместо YOUR_API_KEY.
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())3. Задайте вопрос#
Отправьте слова покупателя в POST /answers (нужно право answer). В ответе — лучшее совпадение, альтернативы и причина в одну строку для каждого.
Пример запроса, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}),
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": True,
"session_id": "visitor-8841",
},
)
print(response.status_code, response.json())Аутентификация#
Передавайте ключ в заголовке Authorization как bearer-токен. Никогда не кладите его в адрес: адреса попадают в журналы и историю браузера.
Аутентификация
Authorization: Bearer YOUR_API_KEYОтсутствующий, испорченный, неизвестный и отозванный ключ дают один и тот же 401 invalid_api_key: ответ никогда не подскажет подбирающему, что ключ «почти подошёл». Повторные неверные ключи с одного адреса замедляются ответом 429 too_many_failed_attempts.
Ключ принадлежит ровно одному магазину и не читает чужой: номер товара из другого магазина отвечает 404, как несуществующий.
Ключ можно сузить дальше списком разрешённых IP (403 ip_not_allowed) и списком источников для браузера (403 origin_not_allowed). Права задаются при создании ключа; чтобы расширить их, создайте другой ключ.
Права и лимиты#
У каждого ключа свой набор прав (scopes). Лимиты считаются отдельно для ключа и права.
| Право (scope) | Ручки | Вызовов за 60 с |
|---|---|---|
answer | POST /answers | 20 |
suggest | GET /suggestions | 120 |
catalogue:read | GET /products, GET /products/{product_id} | 120 |
similar | GET /products/{product_id}/similar | 60 |
analytics:read | GET /analytics/summary, GET /analytics/series, GET /analytics/queries, GET /analytics/sales | 30 |
У POST /answers есть ещё суточный потолок — 500 вызовов на ключ (сутки по UTC), потому что каждый ответ — это обращение к модели. Повтор с тем же Idempotency-Key не считается.
В каждом ответе есть X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset; ответ 429 добавляет Retry-After (секунды). Ждите столько, а не повторяйте в цикле. GET /me требует действующий ключ, но не какое-то определённое право.
Ручки API#
Каждая операция ниже собрана из документа OpenAPI, а каждый пример наши тесты выполняют против настоящего API, поэтому образцы не могут разойтись с API. Описания операций и полей — на английском, как в самом документе.
GET /me#
Проверить, какой магазин, права и лимиты у ключа.
Describes the key you are calling with: its scopes, shop, allow-lists and limits. Costs no quota; use it to check a key.
- Нужное право
- любой действующий ключ
Параметров нет.
| Статус | Схема ответа | Описание |
|---|---|---|
200 | KeyInfo | Successful Response |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Check a key (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/me' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"id": 3,
"name": "Storefront search",
"hint": "osk_…PLEE",
"scopes": [
"analytics:read",
"answer",
"catalogue:read",
"similar",
"suggest"
],
"shop": {
"domain": "alpha.example",
"currency": "EUR",
"language": "en"
},
"origins": [],
"networks": [],
"created_at": "2026-10-13T09:00:00+00:00",
"last_used_at": null,
"limits": {
"answer": {
"per_minute": 20
},
"suggest": {
"per_minute": 120
},
"similar": {
"per_minute": 60
},
"catalogue:read": {
"per_minute": 120
},
"analytics:read": {
"per_minute": 30
}
},
"answers_per_day": 500
}Missing or invalid key (HTTP 401)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/me'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/me", {
method: "GET",
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/me",
)
print(response.status_code, response.json())Пример ответа · HTTP 401
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "Missing or invalid API key. Send it as 'Authorization: Bearer osk_…'.",
"param": null,
"request_id": "req_9f2b000000000001"
}
}POST /answers#
Задать вопрос словами покупателя и получить лучшее совпадение, альтернативы и причину для каждого.
Answers a shopper's question the way the shop's own assistant does: the best match, alternatives and a one-line reason for each. Product facts (title, price, stock, link) always come from the shop's catalogue; only say and why are written by an AI model — see the ai object, and pass that on to your users.
Counted against the key's per-minute limit and its daily cap; a replay with the same Idempotency-Key is free.
- Нужное право
answer- Лимит
- 20 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
Idempotency-Key | header | string | null | нет | длина 1–64 | Makes a retry safe: the same key and body return the first answer without a second model call. |
Тело запроса: AnswerRequest#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
query | string | да | The shopper's question, in their own words. |
language | Language | null | нет | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | нет | Opaque id of one conversation. Send the same id to let follow-ups (“cheaper”, “something lighter”) build on earlier turns. Sessions are private to your API key. |
in_stock_only | boolean | нет | Only products that can be bought now (excludes pre-orders). |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | Answer | Successful Response |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
409 | ErrorBody | Same Idempotency-Key is still being processed. |
422 | ErrorBody | Invalid request body, or Idempotency-Key reused with a different body. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
502 | ErrorBody | The answer could not be produced right now. Safe to retry. |
A shopper asks in their own words (HTTP 200)#
Пример запроса, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": true,
"session_id": "visitor-8841"
}),
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"Idempotency-Key": "3f9d1c52-7b0e-4a53-9a6f-0c2f8e1d77aa",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
"language": "en",
"in_stock_only": True,
"session_id": "visitor-8841",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"id": "6f1b4a52-3c0d-4d5e-9a7e-2b8c1d0f4e11",
"query": "a cold, quiet science fiction novel for a long winter",
"intent": "search",
"say": "Here is the best match for a cold, quiet read.",
"main": {
"product": {
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
},
"why": "A quiet, snowbound classic - the cold-world mood you described, in one of the genre's best-loved novels.",
"why_source": "model"
},
"alternatives": [
{
"product": {
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
},
"why": "Set in the same universe: the politics of a desert moon instead of snow.",
"why_source": "model"
},
{
"product": {
"id": 104,
"title": "Solaris",
"maker": "Stanislaw Lem",
"line": null,
"url": "https://alpha.example/products/104",
"handle": "solaris",
"external_id": "gid://shop/104",
"price": {
"amount": "11.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/104.jpg",
"thumb_url": "https://cdn.alpha.example/104-s.jpg"
},
"why": "A different kind of stillness: an ocean planet that studies the people studying it.",
"why_source": "model"
}
],
"shelves": [],
"miss": false,
"miss_reason": null,
"clarify": null,
"degraded": false,
"currency": "EUR",
"ai": {
"generated": true,
"fields": [
"say",
"main.why",
"alternatives[].why"
],
"notice": "The `say` and `why` texts are generated by an AI model from this shop's catalogue and may contain mistakes. Product names, prices, stock and links come from the shop's catalogue, not from the model."
}
}Rate limit reached (HTTP 429)#
Пример запроса, curl
curl -X POST 'https://assistant.onesite.agency/api/v1/answers' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"query": "a cold, quiet science fiction novel for a long winter"
}'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/answers", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
"query": "a cold, quiet science fiction novel for a long winter"
}),
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.post(
"https://assistant.onesite.agency/api/v1/answers",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"query": "a cold, quiet science fiction novel for a long winter",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 429
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit reached: 20 calls per minute for this scope.",
"param": null,
"request_id": "req_9f2b000000000005"
}
}GET /suggestions#
Быстрые подсказки товаров под строкой поиска по мере набора. Модель ИИ не участвует.
Instant products for a half-typed query. No model is called and nothing is journalled, so it is cheap — but it is not a substitute for POST /answers when the shopper asked a full question.
- Нужное право
suggest- Лимит
- 120 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
q | query | string | да | длина 1–500 | What has been typed so far. |
limit | query | integer | нет | 1–20; по умолчанию 6 | |
in_stock_only | query | boolean | нет | по умолчанию false | Only products that can be bought now. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | SuggestionList | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Suggestions while the shopper types (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/suggestions?q=left+hand&limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "list",
"query": "left hand",
"data": [
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
}
]
}GET /products#
Прочитать весь каталог постранично.
The shop's visible catalogue in a stable order (by product id), a page at a time. Products the owner has hidden or excluded are not listed. A page may hold fewer than limit items while has_more is still true; keep following next_cursor until it is null.
- Нужное право
catalogue:read- Лимит
- 120 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
limit | query | integer | нет | 1–100; по умолчанию 20 | |
cursor | query | string | null | нет | длина ≤ 64 | next_cursor of the previous page. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | ProductPage | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
First page of the catalogue (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=2", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=2",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "list",
"data": [
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg"
},
{
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
}
],
"has_more": true,
"next_cursor": "c1.MTAy"
}Following the cursor (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=2&cursor=c1.MTAy",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "list",
"data": [
{
"id": 103,
"title": "A Wizard of Earthsea",
"maker": "Ursula K. Le Guin",
"line": "Earthsea",
"url": "https://alpha.example/products/103",
"handle": "a-wizard-of-earthsea",
"external_id": "gid://shop/103",
"price": {
"amount": "9.99",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/103.jpg",
"thumb_url": "https://cdn.alpha.example/103-s.jpg"
},
{
"id": 104,
"title": "Solaris",
"maker": "Stanislaw Lem",
"line": null,
"url": "https://alpha.example/products/104",
"handle": "solaris",
"external_id": "gid://shop/104",
"price": {
"amount": "11.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/104.jpg",
"thumb_url": "https://cdn.alpha.example/104-s.jpg"
}
],
"has_more": true,
"next_cursor": "c1.MTA0"
}A parameter out of range (HTTP 422)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products?limit=500' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products?limit=500", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products?limit=500",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 422
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Invalid request. limit: Input should be less than or equal to 100",
"param": "limit",
"request_id": "req_9f2b000000000004"
}
}GET /products/{product_id}#
Прочитать один товар по его номеру.
One product of the shop with its description and gallery. A product that belongs to another shop, does not exist or is hidden by the owner is the same 404.
- Нужное право
catalogue:read- Лимит
- 120 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
product_id | path | integer | да | 1–9223372036854776000 | Product number (id in any product object). |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | ProductDetail | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
404 | ErrorBody | No such visible product in this shop. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
One product with its description and gallery (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"id": 101,
"title": "The Left Hand of Darkness",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/101",
"handle": "the-left-hand-of-darkness",
"external_id": "gid://shop/101",
"price": {
"amount": "12.90",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/101.jpg",
"thumb_url": "https://cdn.alpha.example/101-s.jpg",
"description": "The Left Hand of Darkness by Ursula K. Le Guin.",
"images": [
"https://cdn.alpha.example/101.jpg"
]
}A key without the needed scope (HTTP 403)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products/101' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 403
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This key lacks the 'catalogue:read' scope. Create a key that has it.",
"param": "catalogue:read",
"request_id": "req_9f2b000000000002"
}
}A product that is not in this shop (HTTP 404)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products/987654' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/987654", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/987654",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 404
{
"error": {
"type": "not_found_error",
"code": "product_not_found",
"message": "No such product in this shop.",
"param": null,
"request_id": "req_9f2b000000000003"
}
}GET /products/{product_id}/similar#
Товары, похожие на заданный. Модель ИИ не участвует.
Neighbours of the product's own embedding vector, ranked the way the shop's assistant ranks them. No model is called. Needs only the similar scope — a key without catalogue:read cannot fetch the product itself.
- Нужное право
similar- Лимит
- 60 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
product_id | path | integer | да | 1–9223372036854776000 | Product number (id in any product object). |
limit | query | integer | нет | 1–24; по умолчанию 8 |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | SimilarList | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
404 | ErrorBody | No such visible product in this shop. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Products similar to one product (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/products/101/similar?limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/products/101/similar?limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/products/101/similar?limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "list",
"data": [
{
"id": 102,
"title": "The Dispossessed",
"maker": "Ursula K. Le Guin",
"line": "Hainish Cycle",
"url": "https://alpha.example/products/102",
"handle": "the-dispossessed",
"external_id": "gid://shop/102",
"price": {
"amount": "13.50",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/102.jpg",
"thumb_url": "https://cdn.alpha.example/102-s.jpg"
},
{
"id": 103,
"title": "A Wizard of Earthsea",
"maker": "Ursula K. Le Guin",
"line": "Earthsea",
"url": "https://alpha.example/products/103",
"handle": "a-wizard-of-earthsea",
"external_id": "gid://shop/103",
"price": {
"amount": "9.99",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/103.jpg",
"thumb_url": "https://cdn.alpha.example/103-s.jpg"
},
{
"id": 109,
"title": "Gift Cookbook",
"maker": "Chef Example",
"line": null,
"url": "https://alpha.example/products/109",
"handle": "gift-cookbook",
"external_id": "gid://shop/109",
"price": {
"amount": "24.00",
"currency": "EUR"
},
"availability": "in_stock",
"ready_at": null,
"image_url": "https://cdn.alpha.example/109.jpg",
"thumb_url": "https://cdn.alpha.example/109-s.jpg"
}
],
"product_id": 101
}GET /analytics/summary#
Итоги магазина за окно — те же числа, что в «Статистике» кабинета, — с необязательным сравнением.
The cabinet's Statistics figures for the window: questions, people who asked, sessions, found and not found, clicks, and the shopper's path by step. Counted for people only. With compare, the same totals for the comparison window — only when the shop's history covers it; otherwise previous is null rather than a growth from nothing.
- Нужное право
analytics:read- Лимит
- 30 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
compare | query | "prev" | "year" | "none" | нет | по умолчанию "prev" | Compare with the window just before (prev), the same window a year back (year), or nothing. |
since | query | string (date-time) | null | нет | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | нет | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | нет | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | нет | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | нет | длина ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | AnalyticsSummary | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Last week in figures, compared with the week before (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/summary?first_day=2026-10-06&last_day=2026-10-12",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "analytics.summary",
"window": {
"since": "2026-10-06T00:00:00+00:00",
"until": "2026-10-13T00:00:00+00:00",
"timezone": "UTC"
},
"totals": {
"queries": 280,
"people": 154,
"sessions": 174,
"answered": 238,
"missed": 42,
"asked_again": 31,
"impressions": 840,
"clicks": 82,
"reached": 63,
"browsed": 7
},
"compare": "prev",
"previous_window": {
"since": "2026-09-29T00:00:00+00:00",
"until": "2026-10-06T00:00:00+00:00"
},
"previous": {
"queries": 273,
"people": 150,
"sessions": 174,
"answered": 238,
"missed": 42,
"asked_again": 31,
"impressions": 840,
"clicks": 82,
"reached": 63,
"browsed": 7
},
"funnel": [
{
"stage": "visit",
"sessions": 900,
"share_of_previous": null
},
{
"stage": "offered",
"sessions": 860,
"share_of_previous": 0.9555555555555556
},
{
"stage": "opened",
"sessions": 140,
"share_of_previous": 0.16279069767441862
},
{
"stage": "asked",
"sessions": 120,
"share_of_previous": 0.8571428571428571
},
{
"stage": "reached",
"sessions": 30,
"share_of_previous": 0.25
},
{
"stage": "carted",
"sessions": 70,
"share_of_previous": null
},
{
"stage": "bought",
"sessions": 12,
"share_of_previous": 0.17142857142857143
}
],
"funnel_since": null
}GET /analytics/series#
Вопросы, промахи и вопросы, дошедшие до товара, — за каждый день окна.
Questions, misses and questions that reached a product, for every day of the window in timezone. Days without questions are zeros. Only the window is counted, so the first and last day may be partial, and the days add up to the summary's totals.
- Нужное право
analytics:read- Лимит
- 30 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
since | query | string (date-time) | null | нет | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | нет | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | нет | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | нет | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | нет | длина ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | AnalyticsSeries | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
Questions per day (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/series?first_day=2026-10-06&last_day=2026-10-12&timezone=Europe/Berlin",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "analytics.series",
"window": {
"since": "2026-10-05T22:00:00+00:00",
"until": "2026-10-12T22:00:00+00:00",
"timezone": "Europe/Berlin"
},
"data": [
{
"date": "2026-10-06",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-07",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-08",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-09",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-10",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-11",
"queries": 40,
"missed": 6,
"reached": 9
},
{
"date": "2026-10-12",
"queries": 40,
"missed": 6,
"reached": 9
}
]
}GET /analytics/queries#
Самые частые вопросы и вопросы, которые чаще всего остаются без товара.
The questions people ask most, and the questions most often left without a product — what to stock and what to describe better. Questions are grouped without regard to case; e-mail addresses and phone or card numbers in them are masked.
- Нужное право
analytics:read- Лимит
- 30 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
limit | query | integer | нет | 1–100; по умолчанию 20 | Rows in each list. |
since | query | string (date-time) | null | нет | | Start of the window (ISO 8601; without an offset — UTC). Default: until minus 30 days. |
until | query | string (date-time) | null | нет | | End of the window, exclusive (ISO 8601). Default: now. |
first_day | query | string (date) | null | нет | | First day of the window (YYYY-MM-DD) in timezone, inclusive. Use with last_day instead of since/until. |
last_day | query | string (date) | null | нет | | Last day of the window (YYYY-MM-DD) in timezone, inclusive. |
timezone | query | string | null | нет | длина ≤ 64 | IANA time zone for days, e.g. Europe/Berlin. Default: the shop's time zone, or UTC when the shop has none. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | AnalyticsQueries | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
What people ask most, and what they do not find (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/queries?first_day=2026-10-06&last_day=2026-10-12&limit=3",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "analytics.queries",
"window": {
"since": "2026-10-06T00:00:00+00:00",
"until": "2026-10-13T00:00:00+00:00",
"timezone": "UTC"
},
"top": [
{
"text": "science fiction for winter",
"count": 42,
"missed": 0,
"clicks": 28
},
{
"text": "gift for a ten-year-old",
"count": 28,
"missed": 7,
"clicks": 14
},
{
"text": "write to me at [email]",
"count": 1,
"missed": 1,
"clicks": 0
}
],
"missed": [
{
"text": "signed first editions",
"count": 21,
"missed": 21,
"clicks": 0
},
{
"text": "gift for a ten-year-old",
"count": 28,
"missed": 7,
"clicks": 14
}
]
}GET /analytics/sales#
Что продал помощник, по вашим заказам, — те же числа, что в сводке раздела «Покупки».
Orders and revenue the assistant helped with, by the shop's own orders: products its button put into the cart and products bought after it showed them. Orders after a conversation without either are counted apart and not as its revenue. The shop's total turnover is not part of this API.
- Нужное право
analytics:read- Лимит
- 30 вызовов за 60 с
| Имя | Где | Тип | Обязательно | Ограничения | Описание |
|---|---|---|---|---|---|
days | query | integer | нет | по умолчанию 30 | Period: today and the days before it. One of 7, 30, 90. |
timezone | query | string | null | нет | длина ≤ 64 | IANA time zone for days. Default: the shop's time zone, or UTC. |
| Статус | Схема ответа | Описание |
|---|---|---|
200 | AnalyticsSales | Successful Response |
304 | – | Not modified: your If-None-Match matched the current ETag. |
401 | ErrorBody | Missing, malformed, unknown or revoked API key (all look the same). |
402 | ErrorBody | The shop's subscription is not active. |
403 | ErrorBody | Key not allowed here: missing scope, IP or origin not on the key's list, plan without API. |
422 | ErrorBody | Invalid request. |
429 | ErrorBody | Rate limit or daily quota reached. See Retry-After. |
What the assistant sold in the last 7 days (HTTP 200)#
Пример запроса, curl
curl 'https://assistant.onesite.agency/api/v1/analytics/sales?days=7' \
-H 'Authorization: Bearer YOUR_API_KEY'Пример запроса, JavaScript (fetch)
const response = await fetch("https://assistant.onesite.agency/api/v1/analytics/sales?days=7", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
},
});
const data = await response.json();
console.log(response.status, data);Пример запроса, Python (requests)
import requests
response = requests.get(
"https://assistant.onesite.agency/api/v1/analytics/sales?days=7",
headers={
"Authorization": "Bearer YOUR_API_KEY",
},
)
print(response.status_code, response.json())Пример ответа · HTTP 200
{
"object": "analytics.sales",
"days": 7,
"since": "2026-10-07T00:00:00+00:00",
"timezone": "UTC",
"currency": "EUR",
"assistant_orders": 2,
"assistant_revenue": {
"amount": "23.90",
"currency": "EUR"
},
"button_revenue": {
"amount": "12.90",
"currency": "EUR"
},
"shown_revenue": {
"amount": "11.00",
"currency": "EUR"
},
"carted": 3,
"talked_orders": 1,
"average_order": {
"amount": "18.70",
"currency": "EUR"
},
"days_data": [
{
"date": "2026-10-07",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-08",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-09",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-10",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-11",
"orders": 1,
"revenue": {
"amount": "11.00",
"currency": "EUR"
}
},
{
"date": "2026-10-12",
"orders": 0,
"revenue": {
"amount": "0.00",
"currency": "EUR"
}
},
{
"date": "2026-10-13",
"orders": 1,
"revenue": {
"amount": "12.90",
"currency": "EUR"
}
}
],
"products": [
{
"product_id": 101,
"title": "The Left Hand of Darkness",
"quantity": 1,
"revenue": {
"amount": "12.90",
"currency": "EUR"
}
},
{
"product_id": 104,
"title": "Solaris",
"quantity": 1,
"revenue": {
"amount": "11.00",
"currency": "EUR"
}
}
]
}Типы данных#
Объекты, из которых состоят запросы и ответы. | null значит, что поле всегда есть, но может быть null; массив записан как Тип[].
AiDisclosure#
Shown to your users? Then say so. The wording below is the minimum we ask integrations to pass on.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
generated | boolean | нет | Some text in this response was written by an AI model. |
fields | string[] | да | JSON paths of the AI-written strings. |
notice | string | да |
AnalyticsFunnelStep#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
stage | "visit" | "offered" | "opened" | "asked" | "reached" | "carted" | "bought" | да | |
sessions | integer | да | Sessions that reached this step. Sessions, not people: events on the site carry no visitor mark. |
share_of_previous | number | null | нет | sessions divided by the previous step. null on the first step and where a step is larger than the one before it (for example, carts added with the shop's own button without a click in an answer). |
AnalyticsPeriod#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
since | string | да | |
until | string | да |
AnalyticsPoint#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
date | string | да | The day (YYYY-MM-DD) in window.timezone. The first and last day may be partial: only the window is counted. |
queries | integer | да | |
missed | integer | да | |
reached | integer | да | Questions of the day after which the shopper opened a product page from the answer. |
AnalyticsQueries#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "analytics.queries" | нет | |
window | AnalyticsWindow | да | |
top | AnalyticsQuery[] | да | Most frequent questions, most frequent first. |
missed | AnalyticsQuery[] | да | Questions most often left without a product, by number of misses. Its own list, not a filter of top. |
AnalyticsQuery#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
text | string | да | The question as asked (case-insensitive group). E-mail addresses, phone and card numbers are masked. |
count | integer | да | How many times it was asked. |
missed | integer | да | How many of those had nothing to show. |
clicks | integer | да | Clicks from its answers to product pages. |
AnalyticsSales#
What the assistant sold, by the shop's own orders — the same figures as the summary in the cabinet's Purchases section.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "analytics.sales" | нет | |
days | integer | да | Today and the days before it, from midnight in timezone. |
since | string | да | Midnight of the first day (ISO 8601). |
timezone | string | да | |
currency | string | да | Orders in another currency are not added up. |
assistant_orders | integer | да | Orders with a product added to the cart by the assistant's button or bought after the assistant showed it. |
assistant_revenue | Money | да | The value of those products (not the whole orders). |
button_revenue | Money | да | Part of assistant_revenue added to the cart by the assistant's button. |
shown_revenue | Money | да | Part of assistant_revenue bought after the assistant showed the product. |
carted | integer | да | Products the assistant helped to put into a cart in the period (by its button or after showing them). |
talked_orders | integer | да | Orders placed after a conversation with the assistant without any of the above. Not counted as its revenue. |
average_order | Money | null | нет | Average total of assistant_orders; null when none has a total. |
days_data | AnalyticsSalesDay[] | да | Every day of the period, empty days included. |
products | AnalyticsSoldProduct[] | да | Top products sold through the assistant, by revenue. |
AnalyticsSalesDay#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
date | string | да | The day (YYYY-MM-DD) in timezone. |
orders | integer | да | |
revenue | Money | да |
AnalyticsSeries#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "analytics.series" | нет | |
window | AnalyticsWindow | да | |
data | AnalyticsPoint[] | да | Every day of the window, days without questions included as zeros. The sum of queries equals totals.queries of the summary. |
AnalyticsSoldProduct#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
product_id | integer | null | нет | null when the product is no longer in the catalogue. |
title | string | null | нет | |
quantity | integer | да | |
revenue | Money | да |
AnalyticsSummary#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "analytics.summary" | нет | |
window | AnalyticsWindow | да | |
totals | AnalyticsTotals | да | |
compare | "prev" | "year" | "none" | да | |
previous_window | AnalyticsPeriod | null | нет | The window compared with. null when compare is none or the shop's history covers less than 90 % of it. |
previous | AnalyticsTotals | null | нет | Totals of previous_window, counted the same way. null — no comparison. |
funnel | AnalyticsFunnelStep[] | да | Sessions by step of the shopper's path, in order. |
funnel_since | string | null | нет | Set when the funnel is counted from a later moment than window.since: before it one of the steps was not yet recorded. |
AnalyticsTotals#
Counted for people only: bots, our own checks, calls made with API keys and conversations removed by the operator are not counted.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
queries | integer | да | Questions people asked the assistant, in their own words. |
people | integer | да | Different people who asked at least once, by the visitor mark in their browser: someone back three days later is one person, the same person on phone and laptop is two. A question without the mark (no analytics consent) counts its session. |
sessions | integer | да | Different sessions with at least one question. A session lasts about a day, so this is not people. |
answered | integer | да | Questions answered with at least one product. |
missed | integer | да | Questions with nothing to show. |
asked_again | integer | да | Sessions that asked another question after an answer. |
impressions | integer | да | Product cards shown in answers. |
clicks | integer | да | Clicks from answer cards to product pages. |
reached | integer | да | Questions after which the shopper opened a product page from the answer. Counted per question, not per click. |
browsed | integer | да | Clicks to product pages from shelves and collections, with no question before them. |
AnalyticsWindow#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
since | string | да | Start of the window, inclusive (ISO 8601, UTC). |
until | string | да | End of the window, exclusive (ISO 8601, UTC). |
timezone | string | да | IANA time zone in which days are cut (daily series, first_day/last_day). |
Answer#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
id | string | да | Id of this answer in the shop's journal. Quote it when you report a problem. |
query | string | да | |
intent | string | да | What the question was understood to be: search, browse, basket, question, greeting, off_topic, code, about_item, order…. New values may appear; treat unknown ones as search. |
say | string | null | нет | A short spoken-style reply, written by an AI model. May be null. |
main | Recommendation | null | нет | The single best match, when there is one. |
alternatives | Recommendation[] | нет | |
shelves | Shelf[] | нет | For “everything by …” questions: whole shelves instead of picks. |
miss | boolean | да | true when the shop has nothing that fits. Do not invent a product then. |
miss_reason | "absent" | "unstocked" | "unmatched" | null | нет | Why nothing is shown: not in the catalogue (absent), sold out (unstocked), or nothing matched the question's hard limits such as price (unmatched). |
clarify | string | null | нет | A clarifying question to ask the user, when the answer needs one. |
degraded | boolean | нет | true when the answer was built without the model (it was unavailable). |
currency | string | да | |
ai | AiDisclosure | да |
AnswerRequest#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
query | string | да | The shopper's question, in their own words. |
language | Language | null | нет | Language of the reply (ru en el es de it fr). Defaults to the shop's language. |
session_id | string | null | нет | Opaque id of one conversation. Send the same id to let follow-ups (“cheaper”, “something lighter”) build on earlier turns. Sessions are private to your API key. |
in_stock_only | boolean | нет | Only products that can be bought now (excludes pre-orders). |
ErrorBody#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
error | ErrorDetail | да |
ErrorDetail#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
type | "authentication_error" | "permission_error" | "invalid_request_error" | "not_found_error" | "conflict_error" | "rate_limit_error" | "billing_error" | "api_error" | да | |
code | string | да | Stable machine-readable reason. Branch on this, not on message. |
message | string | да | For humans. The wording may change between releases. |
param | string | null | нет | The offending field or parameter, for invalid_request. |
request_id | string | да | Also in the X-Request-Id response header. |
KeyInfo#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
id | integer | да | |
name | string | да | |
hint | string | да | Which key this is, e.g. osk_…x9Zq. The secret itself is never shown again. |
scopes | string[] | да | |
shop | KeyShop | да | |
origins | string[] | да | Browser origins allowed to use this key. Empty = any (server-side use). |
networks | string[] | да | IP networks the key is accepted from. Empty = any. |
created_at | string | да | |
last_used_at | string | null | нет | |
limits | map<string, ScopeLimit> | да | Calls per minute, per scope. |
answers_per_day | integer | да | Daily cap on POST /answers calls for this key (UTC day). |
KeyShop#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
domain | string | да | |
currency | string | да | |
language | string | да |
Language#
Language code: ru, en, el, es, de, it, fr.
"ru" | "en" | "el" | "es" | "de" | "it" | "fr"
Money#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
amount | string | да | Decimal amount as a string, e.g. "12.90". Never a float: no rounding surprises. |
currency | string | да | ISO 4217 code of the shop currency, e.g. EUR. |
Product#
A catalogue card. Everything here comes from the shop's own catalogue, never from the model.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
id | integer | да | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | да | |
maker | string | null | нет | Whoever the shop credits as the maker of the product. |
line | string | null | нет | Series, collection or product line, when the shop names one. |
url | string | null | нет | Product page on the shop's own site. |
handle | string | null | нет | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | нет | The shop platform's own product id, for mapping back to your system. |
price | Money | null | нет | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | да | |
ready_at | string | null | нет | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | нет | |
thumb_url | string | null | нет | A smaller image, when the shop has one. |
ProductDetail#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
id | integer | да | Product number in this shop's catalogue. Stable until the product is removed from the shop. |
title | string | да | |
maker | string | null | нет | Whoever the shop credits as the maker of the product. |
line | string | null | нет | Series, collection or product line, when the shop names one. |
url | string | null | нет | Product page on the shop's own site. |
handle | string | null | нет | The platform's product handle (Shopify handle, WooCommerce slug), when known. |
external_id | string | null | нет | The shop platform's own product id, for mapping back to your system. |
price | Money | null | нет | Current price; null when the shop does not publish one. |
availability | "in_stock" | "preorder" | "out_of_stock" | да | |
ready_at | string | null | нет | For preorder: the shop's promised ship date (ISO 8601). |
image_url | string | null | нет | |
thumb_url | string | null | нет | A smaller image, when the shop has one. |
description | string | null | нет | |
images | string[] | нет | The shop's gallery, in the shop's order. |
ProductPage#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "list" | нет | |
data | Product[] | да | |
has_more | boolean | да | |
next_cursor | string | null | нет | Pass as cursor to get the next page. null on the last page. |
Recommendation#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
product | Product | да | |
why | string | null | нет | One sentence on why this fits the question. Written by an AI model when why_source is model; a fixed catalogue template when it is catalogue. |
why_source | "model" | "catalogue" | null | нет | Who wrote why. null when there is no why. |
ScopeLimit#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
per_minute | integer | да |
Shelf#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
title | string | да | |
total | integer | да | How many products the shelf holds in the shop — not how many are shown in items. |
items | Product[] | да |
SimilarList#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "list" | нет | |
data | Product[] | да | |
product_id | integer | да |
SuggestionList#
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
object | "list" | нет | |
query | string | да | |
data | Product[] | да |
Ошибки#
У всех ошибок один формат и есть request_id (он же в заголовке X-Request-Id): называйте его, когда пишете нам. Ветвитесь по code, а не по message: его формулировка может меняться.
Ошибки · HTTP 422
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Invalid request. limit: Input should be less than or equal to 100",
"param": "limit",
"request_id": "req_9f2b000000000004"
}
}| Код | HTTP | Что значит и что делать | Повтор |
|---|---|---|---|
invalid_api_key | 401 | Ключ не передан, испорчен, неизвестен или отозван. Все четыре случая выглядят одинаково намеренно. Передайте Authorization: Bearer <ключ> или создайте новый ключ. | исправьте запрос |
ip_not_allowed | 403 | У ключа есть список разрешённых IP, и этого адреса в нём нет. Вызывайте из сети из списка или измените список в кабинете. | исправьте запрос |
origin_not_allowed | 403 | Вызов идёт со страницы в браузере, чьего источника (origin) нет в списке ключа. Добавьте точный origin (схема, хост, порт) в кабинете. | исправьте запрос |
insufficient_scope | 403 | У ключа нет права (scope), которое требует эта ручка. Права нельзя расширить: создайте новый ключ с нужным правом. | исправьте запрос |
api_disabled | 403 | API для разработчиков для этого магазина не включено. Откройте вкладку Developers в кабинете или напишите нам, если её нет. | исправьте запрос |
subscription_inactive | 402 | Подписка магазина не активна, поэтому API приостановлено. Оно возобновится само, когда подписка снова станет активной; ключи остаются действующими. | исправьте запрос |
plan_not_entitled | 403 | Тариф магазина не включает API для разработчиков. Оно доступно на тарифах, которые его включают. | исправьте запрос |
rate_limited | 429 | Минутный лимит этого ключа и права исчерпан. Подождите секунды из Retry-After и повторите; распределите вызовы или кэшируйте результаты. | позже |
daily_quota_exceeded | 429 | Суточный потолок ответов для этого ключа достигнут (сутки по UTC). Повторите через секунды из Retry-After, то есть после смены суток. | позже |
too_many_failed_attempts | 429 | С этого адреса пришло слишком много неверных ключей. Подождите секунды из Retry-After и проверьте, какой ключ отправляет ваш код. | позже |
invalid_request | 422 | Параметр или поле тела не передано либо вне допустимых значений. param называет первое такое; исправьте запрос, повтор не поможет. | исправьте запрос |
invalid_cursor | 422 | Курсор изменён или не относится к этому списку. Передавайте next_cursor ровно так, как получили, или начните заново без курсора. | исправьте запрос |
product_not_found | 404 | Такого товара в этом магазине нет. Номер товара из чужого магазина отвечает так же: ключ читает только свой магазин. | исправьте запрос |
idempotency_key_reused | 422 | Этот Idempotency-Key уже использован с другим телом запроса. Для другого вопроса возьмите новый ключ. | исправьте запрос |
idempotency_in_progress | 409 | Запрос с тем же Idempotency-Key ещё обрабатывается. Подождите немного и отправьте тот же запрос снова, чтобы получить результат. | позже |
request_too_large | 413 | Тело запроса больше допустимого. Отправьте тело поменьше. | исправьте запрос |
answer_unavailable | 502 | Ответ сейчас получить не удалось. Повторите вскоре с тем же Idempotency-Key. | позже |
internal_error | 500 | Что-то пошло не так на нашей стороне. Повторите с паузой; если не проходит, пришлите нам request_id. | позже |
not_found | 404 | Такой ручки нет. Сверьте путь со справочником; пути начинаются с версии, /api/v1. | исправьте запрос |
method_not_allowed | 405 | Путь существует, но не для этого метода HTTP. Заголовок Allow перечисляет допустимые. | исправьте запрос |
type группирует коды: authentication_error, permission_error, invalid_request_error, not_found_error, conflict_error, rate_limit_error, billing_error, api_error. В пределах v1 могут появиться новые коды; незнакомый code обрабатывайте по статусу HTTP.
Постраничный вывод#
Списки идут по курсору. Передавайте next_cursor из ответа как cursor в следующем запросе, пока next_cursor не станет null. limit по умолчанию 20, максимум 100. Страница может содержать меньше элементов, чем limit, пока has_more равно true, поэтому идите по next_cursor, а не считайте. Курсор непрозрачен: изменённый даст 422 invalid_cursor.
Кэш и ETag#
Ответы GET содержат ETag. Верните его в If-None-Match: если ничего не изменилось, придёт 304 Not Modified без тела — быстрее и легче, чем скачивать список заново. Кэшируйте чтение каталога у себя и перепроверяйте так.
Идемпотентность и повторы#
POST /answers принимает заголовок Idempotency-Key (от 1 до 64 символов: буквы, цифры и . _ : -). Повтор того же тела в течение 24 часа вернёт первый ответ без второго обращения к модели и без расхода квоты; у повтора есть заголовок Idempotent-Replayed: true. Тот же ключ с другим телом даст 422 idempotency_key_reused; пока первый запрос выполняется — 409 idempotency_in_progress.
Заводите новый UUID на каждый вопрос пользователя и используйте его повторно только для повторов этого же вопроса.
Тело запроса ограничено 16384 байтами (413 request_too_large).
Браузер и CORS#
Веб-страница может обращаться к API только с источника (origin), указанного у ключа: схема, хост и порт, например https://shop.example; заголовки CORS API отдаёт только перечисленным источникам. Всё, что есть в коде страницы, публично, поэтому ключ оттуда может скопировать любой, кто откроет страницу.
Поэтому для браузера давайте ключу только права suggest и similar и список источников, а ключи с answer держите на своём сервере.
Аналитика#
Право analytics:read открывает числа вашего магазина: те же итоги, ряд по дням, частые вопросы и продажи помощника, что в кабинете, посчитанные тем же кодом. Давайте его только ключам, которые остаются на вашем сервере.
Считаются только люди: роботы, наши проверки, вызовы с ключами API и разговоры, убранные оператором, не входят. people — разные посетители по метке их браузера: вернувшийся через три дня — один человек, тот же человек с телефона и с ноутбука — двое; sessions живут около суток; ступени воронки — сессии. Оборота магазина в API нет.
Окно — это since/until (ISO 8601; без смещения — UTC) или first_day/last_day, оба дня включительно, по суткам в поясе timezone (по умолчанию — пояс магазина, иначе UTC). Сравнение (compare) возвращается, только если история магазина покрывает окно сравнения; иначе previous — null.
Текст, созданный ИИ#
В ответе say и каждое why пишет модель ИИ, и ответ сообщает об этом в объекте ai: generated, пути JSON в fields и текст notice. Если вы показываете эти тексты своим пользователям, скажите им, что они созданы ИИ; notice — минимальная формулировка, которую мы просим передавать интеграциям.
Названия, цены, наличие и ссылки никогда не приходят от модели. Если miss равно true, в магазине нет подходящего: покажите это и не выдумывайте товар.
Версии#
Версия — часть пути: /v1. Внутри v1 мы вносим только обратно совместимые изменения: новые ручки, новые необязательные параметры, новые поля ответа и новые значения перечислений (например, новые значения intent). Пишите клиенты так, чтобы они игнорировали незнакомые поля и разумно обрабатывали незнакомые значения перечислений. Несовместимое изменение выйдет только под новым путём версии.
Для машин и ИИ-ассистентов#
Тот же справочник в машиночитаемом виде. Импортируйте документ OpenAPI в Postman, Insomnia или генератор кода.