Поиск данных
Читайте актуальные записи клиентов через подписанные запросы к вашей системе.
Читайте актуальные записи клиентов через подписанные запросы к вашей системе.
Когда нужен поиск#
Используйте lookup для текущего статуса заказа, существующей брони или записи клиента. Постоянные правила и сведения о продуктах храните в Знаниях. Lookup только читает систему: он не должен списывать деньги, отменять, бронировать или менять записи. Обычные карточки выбора и подтверждения лишь отправляют сообщение посетителя. Для записи с подтверждением используйте действия изменения, для календаря — бронирование в Cal.com. Только карточки, привязанные к запуску, разрешают эти операции.
Настройка действия#
Откройте Агент → Действия. Создайте секрет в Вошедшие клиенты, сохраните на сервере и настройте идентификацию. В Поиске данных добавьте имя вроде order_status, описание ситуации для вызова и публичный HTTPS endpoint. Объявите только нужные входы, например строку order_id. Модель увидит инструмент lookup_order_status.
Для приватных записей оставьте включённым Требовать вошедшего клиента — это значение по умолчанию. Если отключить, возможен customer: null; возвращайте только допустимые для анонимного посетителя данные. ID из чата или фотографии — входное значение, а не подтверждённая личность.
Существующие поля сохраняются автоматически; дождитесь индикатора перед тестом. Действие можно включить/отключить, изменить или удалить. Панели нужен настроенный секрет. В первой волне управление lookup доступно в панели, но не в публичном API.
Получаемый запрос#
Make Agent Fast отправляет JSON POST на настроенный URL. Значения подписи ниже — заполнители:
POST /agent/order-status HTTP/1.1
Content-Type: application/json
x-maf-timestamp: <unix-seconds>
x-maf-signature: v1=<64-lowercase-hex-characters>{
"version": 1,
"action": "order_status",
"requestId": "c5c1d94b-e88f-4bf6-8023-2dcd1e7ac67b",
"siteId": "YOUR_SITE_ID",
"input": { "order_id": "A128" },
"customer": { "id": "customer_123", "email": "customer@example.com" }
}action — имя без префикса lookup_. requestId обозначает попытку, siteId — сайт. input содержит только объявленные строковые параметры. customer — null либо проверенный сервером { id, email?, name? }; ввод модели или посетителя не может его заменить. Тест в панели намеренно использует пример клиента, введённый уполномоченным владельцем.
Проверка подписи#
Подпись — HMAC-SHA256 с секретом агента, в нижнем регистре hex с префиксом v1=. Проверяйте точные байты "maf-webhook-v1:" + timestamp + "." + rawBody. Сохраняйте исходное тело: разбор и повторная сериализация JSON могут изменить байты. Функция принимает стандартный серверный Request, проверяет окно в пять минут и ожидаемый сайт:
import { createHmac, timingSafeEqual } from "node:crypto";
export async function verifyLookupRequest(request, expectedSiteId) {
const secret = process.env.MAF_SECRET_KEY;
if (!secret) throw new Error("MAF_SECRET_KEY is required");
const timestamp = request.headers.get("x-maf-timestamp");
const signature = request.headers.get("x-maf-signature");
const now = Math.floor(Date.now() / 1000);
if (!timestamp || !/^\d+$/.test(timestamp) ||
!Number.isSafeInteger(Number(timestamp)) ||
Math.abs(now - Number(timestamp)) > 300) {
throw new Error("Invalid timestamp");
}
if (!signature || !/^v1=[a-f0-9]{64}$/.test(signature)) {
throw new Error("Invalid signature");
}
const rawBody = Buffer.from(await request.arrayBuffer());
const expected = createHmac("sha256", secret)
.update(`maf-webhook-v1:${timestamp}.`)
.update(rawBody)
.digest();
const actual = Buffer.from(signature.slice(3), "hex");
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
throw new Error("Invalid signature");
}
const body = JSON.parse(rawBody.toString("utf8"));
if (body?.version !== 1 || body.siteId !== expectedSiteId) {
throw new Error("Invalid lookup scope");
}
return body;
}Вызывайте её до чтения запроса парсером тела. Отклоняйте ошибки проверки, проверяйте имя действия и структуру входов, а перед выдачей приватных данных — принадлежность записи через body.customer.id. Например, ищите A128 среди заказов этого клиента, а не только по ID заказа. input.customer_id, email из чата и фотография не подтверждают права. Приватный endpoint должен требовать непустого клиента. Ограничьте размер входящего тела на сервере. Если нужно отклонять повторные запросы, учитывайте requestId в окне допустимого времени: одна проверка времени не защищает от повтора внутри окна.
Используйте HTTPS и сохраняйте операцию только для чтения, несмотря на POST. После ротации секрета обновите сервер выпуска токенов и проверку lookup.
Формат ответа#
Верните успешный статус 2xx и JSON по контракту. Первый пример — найденная запись, второй — обычное отсутствие совпадения:
{
"found": true,
"card": {
"title": "Order #A128",
"status": { "label": "Shipped", "tone": "success" },
"fields": [{ "label": "Carrier", "value": "DHL" }],
"reference": "A128",
"asOf": "2026-10-12T09:00:00Z",
"link": { "label": "Track", "url": "https://shop.example.com/orders/A128" }
},
"facts": { "order_id": "A128", "status": "shipped", "carrier": "DHL" }
}{ "found": false }found обязателен. При found: true включите card, facts или оба. Карточка требует title и fields (пустой массив допустим). status, reference, asOf, link необязательны. Не добавляйте kind в карточку ответа. Тоны статуса: neutral, success, warning, danger. Для asOf используйте ISO-время, для ссылки — HTTPS без встроенных учётных данных. Некорректная необязательная ссылка пропускается.
facts — плоский объект со строками, конечными числами, boolean или null. Вложенные объекты, массивы и неизвестные поля схемы ответа/карточки отклоняются. Возвращайте только нужные для ответа данные. Модель получает экранированные и ограниченные данные с пометкой недоверенных, а не инструкции. Содержимое карточки — обычный текст, не HTML.
Лимиты и ошибки#
| Параметр | Лимит |
|---|---|
| Lookup на агента | 5; уникальные имена и ID |
| Имя | ^[a-z][a-z0-9_]{2,39}$; зарезервированные имена действий недоступны |
| Описание | 300 символов |
| Объявленные входы | 3; уникальные имена по ^[a-z][a-z0-9_]{1,31}$ |
| Описание / значение входа | 300 / 200 символов; пробелы по краям удаляются |
| Endpoint | Публичный HTTPS, до 500 символов, без учётных данных; внутренние/приватные адреса заблокированы; без перехода по редиректам |
| Таймаут / тело ответа | 8 секунд / 32 KiB (32 768 байт) |
| Данные модели | 2 000 символов, отдельно от карточки посетителя |
| Заголовок / метки / значения карточки | 80 / 40 / 200 символов |
| Поля карточки | 8; максимум 3 карточки на ход агента |
Без идентификации клиента показывается предложение войти, endpoint не вызывается. Недостающие обязательные входы запрашиваются у посетителя. found: false даёт нейтральную карточку отсутствия записи. Таймаут, статус не 2xx, неверный JSON/схема или превышение размера дают карточку ошибки и указание модели не угадывать. Для обычного отсутствия записи возвращайте 2xx и found: false, а не HTTP 404. Длинные строки карточек и данные модели обрезаются; пишите важные факты кратко.
Тест из панели#
- Сохраните lookup и откройте Тест. Введите пример ID подконтрольного вам клиента, необязательный email и все обязательные входы.
- Запустите Тест. Он отправляет настоящий подписанный запрос даже для отключённого у посетителей действия. Обычная репетиция в чате endpoint не вызывает. Частота тестов ограничена для владельца.
- Проверьте карточку/сводку или ошибку, ID запроса, длительность и HTTP-статус при наличии. Не сохраняйте секреты и токены клиентов в логах endpoint.
- Проверьте найденную и отсутствующую запись, чужой аккаунт, пропущенный вход и таймаут. Затем проверьте реальное встраивание с двумя вошедшими клиентами: пример из панели не доказывает изоляцию браузерных сессий и выбор инструмента настоящей моделью.
Виджет показывает карточки статуса, а обычный выбор/подтверждение отправляет метку сообщением. Подтверждения изменений и бронирований, привязанные к запуску, используют защищённый поток выполнения. Коннекторы преобразуют карточки в текст/карточки канала, кнопки, быстрые ответы или списки; текстовый fallback тоже ограничен лимитами канала. Проверьте доставку отдельно в каждом нужном канале.