Интеграция Zendesk AI с Laravel
Опубликовано: 27 июня 2026 г.
- #dev
- #laravel
- #zendesk
- #ai
- #api
- #sanctum
Ситуация: клиент пишет в поддержку и спрашивает, какого черта заказ все еще не отправлен. Чел из саппорта смотрит тикет в Zendesk, потом идет в админку, ищет заказ по эмейлу или как-то еще, смотрит статус, потом снова идет в Zendesk и пишет клиенту ответ в духе “ваш заказ в статусе пу-пу-пу… поэтому еще недельку надо подождать”. Клиент пишет что ждать не хочет, а хочет отменить заказ. Чел из саппорта снова идет в админку, отменяет заказ и снова идет писать клиенту письмо что заказ отменён. Долго муторно и все такое.
Тут на помощь приходит ЭйАй: делегируем эту рутину агенту, который сам сходит в бэкэнд, посмотрит статус, напишет ответ и отменит заказ. А чел из саппорта больше не нужен - его увольняем может взять более сложные тикеты.
Как это сделать? Сейчас в каждом втором большом SaaS есть свой ИИ-агент с поддержкой MCP или сторонних инструментов. Zendesk теперь тоже даёт создавать кастомных агентов и строить workflow наподобие n8n. Можно натаскать агента отвечать по базе знаний и обращаться к внешним сервисам, например к интернет-магазину, через custom actions. Но для этого на стороне приложения нужна ответная часть - API.
И у меня как раз была недавно подобная задача: сделать интеграцию существующего Laravel-приложения и Zendesk AI агента.
Общая схема
Схема такая:
- В приложении, например, в интернет магазине админ создает токены с нужными правами.
- Zendesk connection будет хранить эти токены, чтобы использовать их для запросов в API
- Zendesk custom actions описывают доступные действия в виде запросов к API и используют connections для авторизации.
- Zendesk custom agents описывают агентов и их custom actions.
- Zendesk workflows описывают сценарии для агентов. Например, запускать агента при создании нового тикета.
- При выполнении сценария агент вызывает нужные custom actions, отправляются запросы в API.
- Приложение проверяет токен и его права.
- API возвращает JSON агенту, тот получает данные и действует дальше по сценарию. Например, пишет ответ пользователю.
Почему API, а не MCP? Для подобных задач подойдёт и то и другое, но на момент реализации Zendesk AI работал только с API. И это даже хорошо. MCP молодой и не очень продуманный протокол, а API уже сто лет в обед. Его проще переиспользовать для чего угодно ещё, в отличие от MCP, с которым работают только ЭйАй-агенты.
Ещё плюс обычного API в том, что его можно тестировать без Zendesk. Взял токен, дёрнул endpoint через curl или Postman, посмотрел JSON.
Минимальная backend-часть
Дальше начинается техническая часть, но это не полноценный туториал, здесь я только хочу показать основные слои такой интеграции: токены, abilities, маршруты, контроллеры и JSON-ответы.
Для аутентификации в Laravel будем использовать Sanctum:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Модель, от имени которой выпускаются токены, должна использовать HasApiTokens:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
}
Да, технически токены привязаны к пользователям или какой-либо другой модели, это может немного смущать, т.к. для простого e-commerce приложения кажется можно было бы и просто выпускать токены никуда не привязанные, но у этого подхода есть много преимуществ на будущее - например можно логировать пользователя от чьего токена выполнялись те или иные запросы.
Следующий слой - права доступа. Делаем их с прицелом на расширение: для каждой отдельной операции будет своя ability:
enum ApiAbility: string
{
case ORDERS_READ = 'orders.read';
case ORDERS_CANCEL = 'orders.cancel';
}
Так токен можно ограничить только теми действиями, которые нужны конкретному агенту. Можно разделить доступ так, чтобы один агент мог только читать данные через безопасный токен, а другой, более ответственные, запускал действия с эффектам - запись, отправка эмейлов и т.д.
Сам токен создается примерно так:
$token = $user->createToken(
name: 'Zendesk AI',
abilities: [
ApiAbility::ORDERS_READ->value,
ApiAbility::ORDERS_CANCEL->value,
],
);
$plainTextToken = $token->plainTextToken;
Sanctum показывает открытое значение токена только один раз. В базе хранится хеш, поэтому в админке токен нужно показать сразу после создания и дать возможность скопировать. Интерфейс управления токенами может быть любым, у нас это Nova resource. Описывать его тут смысла нет. Важно, чтобы в интерфейсе были чекбоксы или что-то похожее для ограничения токена по abilities. Для простой интеграции можно сгенерить токен один раз программно и вообще не добавлять UI.
Полезно сразу добавить имя токена, дату создания, дату последнего использования и кнопку revoke. Через месяц никто не вспомнит, какой именно токен лежит в Zendesk connection и можно ли его удалить.
После этого можно добавить API эндпоинты. Для примера возьмем два типа инструментов: получение данных заказа и отмену заказа:
Route::prefix('v1')
->middleware('auth:sanctum')
->group(function () {
Route::get('orders/{number}', [OrderController::class, 'show'])
->middleware('abilities:orders.read');
Route::post(
'orders/{number}/cancel',
[OrderCancelController::class, 'store'],
)->middleware('abilities:orders.cancel');
});
Resource endpoint должен возвращать только те данные, которые нужны агенту. Отдавать всю модель целиком - плохая идея. Если в будущем в модели появится что-то, что клиент не должен видеть, например коммент к заказу, что клиент душнила, это может утечь через агента. В Laravel удобно использовать Resources, но здесь обойдёмся без лишних слоёв:
public function show(string $number): JsonResponse
{
$order = Order::query()
->where('number', $number)
->first();
if (! $order) {
return response()->json([
'success' => false,
'message' => "Order with number {$number} not found",
], 404);
}
return response()->json([
'data' => [
'number' => $order->number,
'status' => $order->status,
],
]);
}
Пример ответа:
{
"data": {
"number": "AAA111",
"status": "active"
}
}
Action endpoint описывает результат выполненной операции. В простом варианте API сразу запускает действие, а агент получает success и человекочитаемое сообщение для ответа пользователю.
public function store(string $number): JsonResponse
{
$order = Order::query()
->where('number', $number)
->firstOrFail();
try {
CancelOrder::dispatch($order);
return response()->json([
'success' => true,
'message' => 'The order cancellation has been queued.',
]);
} catch (Throwable $exception) {
report($exception);
return response()->json([
'success' => false,
'message' => 'Order cancellation failed.',
], 500);
}
}
Тут стоит сделать оговорку, что безопасность вызываемых действий - прямая ответственность того, кто пишет код, а не агента, которые его вызывает. В примере выше мы принимаем только номер заказа, но не валидируем пользователя, который его отменяет. В реальном приложении нужно проверять, что заказ действительно принадлежит пользователю, от имени которого приходит запрос к агенту. А вот как можно это проверить зависит от вашего конкретного случая и вашего настроенного workflow.
Отдельно стоит привести ошибки к предсказуемому JSON-формату
{
"success": false,
"message": "Order with number AAA111 not found"
}
Чтобы АПИ всегда отдавало JSON, используем ForceJsonResponse middleware в app/Http/Kernel.php:
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\ForceJsonResponse::class,
...
],
];
Минимальный набор ожидаемых статусов обычно такой:
401- токен отсутствует или невалиден;403- у токена нет нужной ability;404- заказ или другая сущность не найдены;200- запрос или действие успешно обработаны;500- неожиданная серверная ошибка.
Для AI-агента особенно важны 403 и 404. Если вернуть одинаковое “что-то пошло не так”, агент может начать повторять запрос или придумывать пользователю странный ответ. Лучше сразу разделять: токен не имеет права выполнить действие, заказ не найден, действие невозможно по бизнес-правилам.
Если смотреть на это как на список изменений в приложении, набор такой: установить и настроить Sanctum, добавить abilities для внешнего API, реализовать выпуск токенов, добавить защищенные API endpoint’ы и подготовить контроллеры/resources под конкретные сценарии. Возможно, потребуется небольшой тюнинг Sanctum-конфига под кастомные guards, если используются:
// config/sanctum.php
return [
'guard' => ['office'],
...
];
Настройка на стороне Zendesk
Когда API готов, на стороне Zendesk нужно связать два слоя: connection и custom actions. Connection отвечает за аутентификацию, а custom actions описывают конкретные инструменты, которыми сможет пользоваться AI-агент.
Сначала создаем персональный токен в Laravel и добавляем его в Zendesk Admin Center: Admin center -> Apps and integrations -> Connections. Для нашего кейса используем authentication type bearer token. Форма ещё требует Allowed Domain с обязательным https-протоколом, поэтому для тестирования может пригодиться ngrok или cloudflare tunnel. Дальше при создании custom actions мы просто будем выбирать готовый connection с токеном. Токен не придётся вписывать несколько раз для каждого endpoint’а, и это удобно.
Теперь можно описать экшены для агента, для этого идем в Admin center -> Apps and integrations -> Custom actions.
В форме custom action нужно заполнить URL и описать входные параметры. Тут важно, что эти описания, name и description, видны агенту. По ним он понимает, какое значение нужно достать из тикета или сообщения пользователя и подставить в запрос. Это похоже на описание параметров tool use, только через UI.
Описание параметров лучше писать как для стажёра, который видит тикет впервые. Не id, а order number, usually looks like AAA111. Чем конкретнее описание, тем меньше шансов, что агент подставит неверное значение.
Для получения данных заказа настройки могут выглядеть так:
Input:
- name:
number; - description:
order number, e.g. AAA111.
Request:
- method:
GET; - URL:
https://api.example.com/api/v1/orders/{{number}}; - authentication: созданная bearer token connection;
- body, query parameters и headers: пустые.
Output:
- name:
data; - description:
order details.
Второе действие - отмена заказа.
Input:
- name:
number; - description:
order number, e.g. AAA111.
Request:
- method:
POST; - URL:
https://api.example.com/api/v1/orders/{{number}}/cancel; - authentication: та же bearer token connection;
- body, query parameters и headers: пустые.
Для такого действия output можно не описывать как отдельную структуру. Zendesk всё равно получит HTTP-статус и JSON с success и message, а агент сможет использовать это сообщение в своём сценарии.
Перед подключением действий к агенту проверяем API вручную:
curl -sS "https://api.example.com/api/v1/orders/AAA111" \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"
curl -sS -X POST \
"https://api.example.com/api/v1/orders/AAA111/cancel" \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"
После этого можно набросать тестового кастомного агента и workflow, чтобы потестировать на реальных тикетах, но в тестовом режиме. Для этого вдем в Admin Center -> AI -> Custom agents, создаем агента и описываем его основную инструкцию.
Что получилось
По факту вся интеграция свелась к обычному API на Sanctum. Никаких специальных zendesk SDK не понадобилось. Zendesk вызывает HTTP endpoint, Laravel проверяет токен и возвращает JSON. Разница только в том, что клиентом здесь выступает не фронтенд и не мобильное приложение, а AI-агент.