Shopping Assistant API

На этой странице описаны контракты Shopping Assistant API: отправка сообщений, обработка структурированного ответа, восстановление истории и tracking товарных подборок.

Перед работой с endpoint-ами выполните шаги из гайда по API-интеграции. Настройка авторизации, пользователей, контекста и API-кампании выполняется через Gravity Field API V2.

POST /shopping/generate

POST https://shopping-assistant-api.gravityfield.ai/shopping/generate
Content-Type: application/json

Endpoint принимает новое сообщение пользователя и возвращает текстовые блоки, товарные карточки, кнопки действий и быстрые ответы. Готовый UI чата API не возвращает.

Для всех сообщений одного диалога используйте одинаковые threadId и resourceId. В resourceId передавайте user.uid, полученный через Gravity Field API V2. Историю диалога повторно отправлять не нужно: Shopping Assistant сохраняет контекст по этим идентификаторам.

Для обычной интеграции используйте роль user.

Контракт запроса

type ShoppingTextContentPart = {
  type: "text";
  text: string;
};

type ShoppingImageContentPart = {
  type: "image";
  source:
    | {
        type: "url";
        url: string;
      }
    | {
        type: "base64";
        mediaType: "image/jpeg" | "image/png" | "image/webp";
        data: string;
      };
};

type ShoppingGenerateRequest = {
  threadId: string;
  resourceId: string;
  trafficType?: "real" | "test";
  messages: Array<{
    role: "user";
    content:
      | string
      | Array<ShoppingTextContentPart | ShoppingImageContentPart>;
  }>;
  runtimeContext: {
    tenant_id: string;
    ctx?: {
      type?: string;
      data?: unknown;
      lng?: string | null;
      location?: string | null;
    };
  };
};

Поля запроса

Поле Обязательное Назначение
threadId Да ID диалога. Должен оставаться одинаковым для всех сообщений одной чат-сессии.
resourceId Да uid пользователя Gravity Field, сохраненный на этапе API-интеграции.
trafficType Нет real для боевого трафика или test для тестовых сценариев.
messages Да Текущее сообщение пользователя. Обычно передается массив с одним элементом. content может быть строкой или массивом блоков text и image.
runtimeContext.tenant_id Да ID секции Gravity Field.
runtimeContext.ctx Нет Контекст текущего экрана или страницы.

Как передать изображение

Чтобы отправить изображение, передайте в content массив блоков. В одном пользовательском сообщении поддерживается только одно изображение. Вместе с ним можно передать текстовый блок с пояснением:

{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "trafficType": "test",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Найдите похожий диван в более светлом цвете"
        },
        {
          "type": "image",
          "source": {
            "type": "base64",
            "mediaType": "image/jpeg",
            "data": "/9j/4AAQSkZJRgABAQ..."
          }
        }
      ]
    }
  ],
  "runtimeContext": {
    "tenant_id": "670ccaae56afcafaf808c146"
  }
}

В поле data передавайте только Base64-содержимое файла, без префикса data:image/jpeg;base64,. Значение mediaType должно соответствовать фактическому формату файла.

Изображение также можно передать по URL. Замените блок image в примере выше:

{
  "type": "image",
  "source": {
    "type": "url",
    "url": "https://cdn.example.com/user-photo.webp"
  }
}

Текст необязателен: для запроса только по изображению передайте в content массив с одним блоком image.

Ограничения для изображения

Ограничение Требование
Количество Не более одного изображения в одном пользовательском сообщении.
Форматы JPEG, PNG или WebP: image/jpeg, image/png, image/webp.
Размер файла Не более 5 МиБ до Base64-кодирования.
Разрешение Не более 16 000 000 пикселей: ширина × высота.
Base64 Только корректный raw Base64 без data:-префикса. MIME-тип должен совпадать с содержимым файла.
URL Публичный URL по HTTPS без логина и пароля. URL должен возвращать поддерживаемое изображение с корректным Content-Type; допускается не более трех перенаправлений, таймаут загрузки - 10 секунд.

После проверки Shopping Assistant учитывает EXIF-ориентацию, уменьшает изображение до размера не более 2048 × 2048 пикселей без увеличения и преобразует его в JPEG для обработки.

Как заполнять ctx

runtimeContext.ctx использует модель контекста API V2. Передавайте актуальный контекст, согласованный с /visit и /choose. Поддерживаемые типы и формат data описаны в разделе Контекст API V2.

Если чат открыт на карточке товара, передайте SKU товара в контексте:

{
  "runtimeContext": {
    "tenant_id": "670ccaae56afcafaf808c146",
    "ctx": {
      "type": "PRODUCT",
      "data": ["1066109"]
    }
  }
}

Минимальный пример

Запрос

curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/generate' \
  --header 'Content-Type: application/json' \
  --data '{
    "threadId": "thread-8f2b1c",
    "resourceId": "665f0a000000000000000001",
    "trafficType": "test",
    "messages": [
      {
        "role": "user",
        "content": "Помогите выбрать корм для взрослой кошки"
      }
    ],
    "runtimeContext": {
      "tenant_id": "670ccaae56afcafaf808c146"
    }
  }'

Ответ

{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Подберу корм, но сначала уточню пару деталей. Кошка стерилизована и есть ли особенности по здоровью?"
      }
    ],
    "ui": {
      "suggests": [
        "Стерилизована, без особенностей",
        "Не стерилизована, чувствительное пищеварение",
        "Пожилая кошка, нужен мягкий корм"
      ]
    }
  }
}

Пример ответа с товарами

{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Вот несколько подходящих вариантов. Смотрите на возраст, стерилизацию и чувствительность пищеварения."
      },
      {
        "type": "products",
        "title": "Подходящие корма для взрослых кошек",
        "value": [
          {
            "sku": "1066109",
            "name": "Сухой корм для взрослых кошек",
            "price": 1299,
            "features": "Для взрослых кошек; повседневный рацион",
            "is_stm": false,
            "brand": "Demo Brand",
            "url": "https://example.com/product/1066109",
            "image_url": "https://example.com/product/1066109.jpg",
            "events": [
              {
                "type": "visible_impression",
                "urls": [
                  "https://evs-01.gravityfield.ai/v2/engagement?...",
                  "https://evs-02.gravityfield.ai/v2/engagement?..."
                ]
              },
              {
                "type": "click",
                "urls": [
                  "https://evs-01.gravityfield.ai/v2/engagement?..."
                ]
              }
            ]
          },
          {
            "sku": "1023814",
            "name": "Корм для стерилизованных кошек",
            "price": 1599,
            "features": "Для стерилизованных кошек; контроль веса",
            "is_stm": true
          }
        ]
      },
      {
        "type": "message",
        "value": "Если кошка стерилизована, лучше начать со второго варианта. Если нет, подойдет первый."
      }
    ],
    "ui": {
      "suggests": [
        "Показать варианты дешевле",
        "Нужен влажный корм",
        "У кошки чувствительное пищеварение"
      ]
    }
  },
  "events": [
    {
      "type": "WRIMP",
      "urls": [
        "https://evs-01.gravityfield.ai/v2/engagement?...",
        "https://evs-02.gravityfield.ai/v2/engagement?..."
      ]
    }
  ]
}

Контракт ответа

type ShoppingGenerateResponse = {
  threadId: string;
  resourceId: string;
  response: {
    contents: Array<
      | { type: "message"; value: string }
      | { type: "products"; value: Product[]; title?: string }
      | { type: "button"; value: string; action: string }
    >;
    ui?: {
      suggests: string[];
    };
  };
  events?: EngagementEvent[];
};

type Product = {
  sku?: string;
  name: string;
  price?: number | null;
  features?: string;
  is_stm?: boolean;
  brand?: string;
  url?: string;
  image_url?: string;
  events?: EngagementEvent[];
};

type EngagementEvent = {
  type: string;
  urls: string[];
};

Для кнопок клиентская сторона должна обработать action. Например, open_support_chat можно связать с открытием клиентского чата поддержки.

Поле title в блоке products содержит готовый заголовок для этой группы рекомендаций. Если поле пришло, отобразите его над товарными карточками. Если title отсутствует, блок можно отобразить без заголовка.

Для товарных карточек считайте sku основным идентификатором товара. Поля price, brand, url, image_url и другие данные могут использоваться для быстрого отображения карточки, но актуализация цены, изображения, наличия, открытие PDP и добавление в корзину остаются на стороне клиентского канала или клиентского каталога.


POST /shopping/history

Чтобы восстановить сообщения после перезагрузки страницы или повторного открытия чата, вызовите:

POST https://shopping-assistant-api.gravityfield.ai/shopping/history
Content-Type: application/json

Endpoint возвращает только публичные пользовательские сообщения и ответы Shopping Assistant. Внутренние system-сообщения, вызовы инструментов и служебные данные в ответ не попадают.

Запрос истории

curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/history' \
  --header 'Content-Type: application/json' \
  --data '{
    "tenant_id": "670ccaae56afcafaf808c146",
    "resourceId": "665f0a000000000000000001",
    "threadId": "thread-8f2b1c",
    "page": 1,
    "limit": 50
  }'
Поле Обязательное Назначение
tenant_id Да ID секции Gravity Field. Должен совпадать с runtimeContext.tenant_id, который использовался при создании диалога.
resourceId Да uid пользователя Gravity Field, которому принадлежит диалог.
threadId Да ID диалога, историю которого нужно получить.
page Нет Номер страницы, начиная с 1. По умолчанию 1.
limit Нет Количество сообщений на странице: от 1 до 100. По умолчанию 50.

История доступна только при точном совпадении tenant_id, resourceId и threadId. Если диалог не найден или не принадлежит указанной секции или пользователю, API возвращает одинаковый ответ 404.

Ответ

{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "items": [
    {
      "id": "shopping:user:4f8c...",
      "role": "user",
      "content": "Помогите выбрать корм для взрослой кошки",
      "createdAt": "2026-07-28T10:00:00.000Z"
    },
    {
      "id": "shopping:assistant:8a21...",
      "role": "assistant",
      "createdAt": "2026-07-28T10:00:01.250Z",
      "threadId": "thread-8f2b1c",
      "resourceId": "665f0a000000000000000001",
      "response": {
        "contents": [
          {
            "type": "message",
            "value": "Кошка стерилизована и есть ли особенности по здоровью?"
          }
        ],
        "ui": {
          "suggests": [
            "Стерилизована, без особенностей",
            "Не стерилизована",
            "Есть особенности по здоровью"
          ]
        }
      },
      "responseLatencyMs": 1250
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "hasMore": false
  }
}

Элементы в items отсортированы по времени создания от ранних к поздним в пределах страницы:

  • для role: "user" поле content имеет тот же формат, что в запросе /shopping/generate: строка для текста или массив блоков text и image;
  • для role: "assistant" поле response имеет тот же контракт, что ответ /shopping/generate; дополнительно могут присутствовать responseLatencyMs и events;
  • если pagination.hasMore равно true, запросите следующую страницу, увеличив page на 1.

Для сообщения с изображением history endpoint возвращает блок image с Base64. Для отображения превью соберите data URL из source.mediaType и source.data, но не передавайте это сообщение повторно в /shopping/generate.

const previewUrl =
  `data:${image.source.mediaType};base64,${image.source.data}`;

Ответ содержит заголовок Cache-Control: no-store. Не сохраняйте историю в общем или публичном кеше на клиентской стороне.

Ошибки получения истории

HTTP-статус Причина Что делать клиенту
400 Некорректный JSON, отсутствует обязательное поле или page/limit выходит за допустимый диапазон. Проверить payload перед повторной отправкой.
404 Диалог не найден или связка tenant_id, resourceId и threadId не совпадает. Код ответа: SHOPPING_HISTORY_NOT_FOUND. Начать новый диалог или проверить сохраненные идентификаторы.
500 Историю не удалось загрузить. Показать ошибку и предложить повторить запрос.

Tracking из ответа Shopping Assistant

В ответе Shopping Assistant tracking-события могут находиться на двух уровнях:

Уровень Где искать Когда использовать
Рекомендательный блок events[] в корне ответа Для событий всего блока, например видимого показа WRIMP.
Товарная карточка response.contents[type="products"].value[].events[] Для visible_impression и click конкретного товара.

При наступлении события обработайте все элементы events[] с соответствующим type. Для каждого найденного элемента выполните GET-запрос по каждому URL из urls[]. Не ограничивайтесь первым элементом events[] или первым URL: один факт взаимодействия может требовать нескольких tracking-запросов.

async function sendTrackingEvents(events, type) {
  const urls = events
    .filter((event) => event.type === type)
    .flatMap((event) => event.urls);

  await Promise.allSettled(
    urls.map((url) => fetch(url, { method: "GET", keepalive: true }))
  );
}

// Весь рекомендательный блок стал видимым.
sendTrackingEvents(result.events ?? [], "WRIMP");

// Стала видимой конкретная карточка товара.
sendTrackingEvents(product.events ?? [], "visible_impression");

Отправляйте событие видимого показа один раз для одного фактического показа блока или карточки. Ошибка tracking-запроса не должна блокировать отображение рекомендаций или действие пользователя. Общие правила работы с tracking URL описаны в разделе Передача взаимодействий пользователя с кампаниями.


Ошибки /shopping/generate и повтор запроса

HTTP-статус Причина Что делать клиенту
400 Некорректный JSON, тело запроса не соответствует контракту, передано больше одного изображения или некорректный Base64/URL. Проверить payload перед повторной отправкой.
413 Изображение превышает лимит 5 МиБ или 16 000 000 пикселей. Попросить пользователя выбрать или подготовить изображение меньшего размера.
415 Формат изображения не поддерживается или MIME-тип не соответствует содержимому файла. Разрешить только JPEG, PNG и WebP и проверить MIME-тип.
422 Файл изображения поврежден или его невозможно обработать. Попросить пользователя выбрать другое изображение.
500 Техническая ошибка генерации ответа. Показать ошибку и предложить повторить запрос.
502 Ответ ассистента не прошел структурную валидацию. Показать сообщение вроде "Не удалось получить ответ, попробуйте еще раз".
504 Shopping Assistant не успел загрузить изображение по URL. Проверить доступность URL и повторить запрос.

Рекомендуем показывать индикатор загрузки до получения ответа, не создавать новый threadId при повторе того же пользовательского сообщения и защищаться от двойной отправки одинакового сообщения при нестабильной сети.