demondehellis

Full-Stack Wizard, Tech Guru & Bash Evangelist.

← Назад к блогу

Интеграция Zendesk AI с Laravel

Опубликовано: 27 июня 2026 г.

  • #dev
  • #laravel
  • #zendesk
  • #ai
  • #api
  • #sanctum

Ситуация: клиент пишет в поддержку и спрашивает, какого черта заказ все еще не отправлен. Чел из саппорта смотрит тикет в Zendesk, потом идет в админку, ищет заказ по эмейлу или как-то еще, смотрит статус, потом снова идет в Zendesk и пишет клиенту ответ в духе “ваш заказ в статусе пу-пу-пу… поэтому еще недельку надо подождать”. Клиент пишет что ждать не хочет, а хочет отменить заказ. Чел из саппорта снова идет в админку, отменяет заказ и снова идет писать клиенту письмо что заказ отменён. Долго муторно и все такое.

Тут на помощь приходит ЭйАй: делегируем эту рутину агенту, который сам сходит в бэкэнд, посмотрит статус, напишет ответ и отменит заказ. А чел из саппорта больше не нужен - его увольняем может взять более сложные тикеты.

Как это сделать? Сейчас в каждом втором большом SaaS есть свой ИИ-агент с поддержкой MCP или сторонних инструментов. Zendesk теперь тоже даёт создавать кастомных агентов и строить workflow наподобие n8n. Можно натаскать агента отвечать по базе знаний и обращаться к внешним сервисам, например к интернет-магазину, через custom actions. Но для этого на стороне приложения нужна ответная часть - API.

И у меня как раз была недавно подобная задача: сделать интеграцию существующего Laravel-приложения и Zendesk AI агента.

Общая схема

Схема такая:

  1. В приложении, например, в интернет магазине админ создает токены с нужными правами.
  2. Zendesk connection будет хранить эти токены, чтобы использовать их для запросов в API
  3. Zendesk custom actions описывают доступные действия в виде запросов к API и используют connections для авторизации.
  4. Zendesk custom agents описывают агентов и их custom actions.
  5. Zendesk workflows описывают сценарии для агентов. Например, запускать агента при создании нового тикета.
  6. При выполнении сценария агент вызывает нужные custom actions, отправляются запросы в API.
  7. Приложение проверяет токен и его права.
  8. 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-агент.