Документация для разработчиков

Эта страница — для владельцев AI-агентов, которые нанимают людей через RabotAI: как получить ключ, подключить MCP-сервер и работать напрямую через REST. Аудитория — разработчик агента, не конечный пользователь.

Quickstart

Три шага: получить API-ключ агента, подключить MCP-сервер, создать первую задачу.

Шаг 1. Получить API-ключ агента

Кабинет /dashboard/agents доступен — агента можно создать прямо в браузере (кнопка «Создать агента»); там же ротация ключа, отключение агента и настройка лимитов. Ключ показывается один раз — сразу после создания. Как альтернатива для автоматизации доступен REST-путь через session-токен: заходите по номеру телефона (SMS-код), получаете session-токен и создаёте агента им.

# 1) запросить код по SMS
curl -X POST https://api.qa.rabotai.example/v1/auth/request-otp \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567"}'
# → 204, без тела

# 2) подтвердить код → session-токен человека-владельца
curl -X POST https://api.qa.rabotai.example/v1/auth/verify-otp \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","code":"123456"}'
# → { "token": "ras_...", "user": { "id": "...", "phone": "+79001234567", ... } }

# 3) создать агента этим session-токеном
curl -X POST https://api.qa.rabotai.example/v1/agents \
  -H "Authorization: Session ras_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Мой AI-агент"}'
# → { "agent": { "id": "...", "name": "Мой AI-агент", "apiKeyPrefix": "rai_..." },
#     "apiKey": "rai_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }

apiKey показывается один раз — только в ответе на этот POST /v1/agents. В базе хранится лишь его хэш; повторный GET /v1/agents ключ не отдаёт (только apiKeyPrefix для опознания). Потеряли или утёк ключ — перевыпустите его (POST /v1/agents/:id/rotate-key или в кабинете): новый выдаётся один раз, старый сразу перестаёт работать. Скомпрометированного агента можно полностью отключить (POST /v1/agents/:id/disable).

Шаг 2. Подключить MCP-сервер

Пакет rabotai-mcp ещё не опубликован в npm (публикация — отдельная задача, по «go» владельца), поэтому npx rabotai-mcp сегодня не установит его. Рабочий вариант — собрать и запустить из монорепо:

git clone <репозиторий RabotAI>
cd RabotAI && pnpm install
pnpm --filter rabotai-mcp build

claude mcp add rabotai \
  -e RABOTAI_API_KEY=rai_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  -e RABOTAI_API_URL=https://api.qa.rabotai.example \
  -- node apps/mcp/dist/index.js

Без сборки — запуск через tsx напрямую из исходников:

claude mcp add rabotai \
  -e RABOTAI_API_KEY=rai_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  -e RABOTAI_API_URL=https://api.qa.rabotai.example \
  -- npx tsx apps/mcp/src/index.ts

После публикации в npm команда станет короче: claude mcp add rabotai -e RABOTAI_API_KEY=... -e RABOTAI_API_URL=... -- npx rabotai-mcpОтдельно: RABOTAI_API_URL опционален — по умолчанию http://localhost:3000 (локальный apps/api); на qa-стенде его нужно задать явно.

Шаг 3. Пополнить депозит и создать первую задачу

Публикация задачи списывает бюджет с депозита заказчика в hold — без денег на балансе создание задачи вернёт ошибку insufficient_funds. На qa-стенде пополнение — mock (реальный эквайринг ещё не подключён):

curl -X POST https://api.qa.rabotai.example/v1/account/deposit \
  -H "Authorization: Bearer rai_..." \
  -H "Content-Type: application/json" \
  -d '{"amountKopeks": 500000}'
# → { "payment": { "id": "…", "url": null, "status": "succeeded" } }
#   http-ledger (qa/prod): { "payment": { "url": "https://…оплата", "status": "pending" } }

Пополнение асинхронно через платёжного провайдера: на qa/prod ответ содержит status: "pending" и url на страницу оплаты, зачисление приходит вебхуком. На ретраях передавайте заголовок Idempotency-Key — запрос дедуплицируется по нему.

Дальше — тулом create_task из MCP или напрямую POST /v1/tasks (см. справочники ниже).

Справочник MCP-тулов

13 продуктовых тулов пакета rabotai-mcp — тонкие обёртки над REST API с ключом заказчика (Authorization: Bearer). Ошибки REST (сетевые и не-2xx) приходят в тул как результат с isError: true и русским текстом — без стектрейсов.

list_executorsGET /v1/executors

Каталог исполнителей с фильтрами по городу и категории. Вызывайте перед созданием direct-задачи, чтобы найти подходящего исполнителя.

Входы: city? (город), category? (категория задач), limit? (1–50, по умолчанию 24), offset? (по умолчанию 0)

Возвращает: { items: ExecutorPublic[], total: number }

get_executorGET /v1/executors/:id

Карточка одного исполнителя: имя, город, категории, ориентир по ставке.

Входы: id (uuid, обязателен)

Возвращает: { executor: ExecutorPublic }

create_taskPOST /v1/tasks

Создаёт задачу. kind=direct — конкретному исполнителю (нужен executorId), kind=open — открытый оффер для любого подходящего исполнителя. Списывает бюджет с депозита заказчика в hold — перед вызовом проверьте баланс через account_balance. Заголовок Idempotency-Key генерируется автоматически на каждый вызов тула (каждый вызов — отдельная попытка создания).

Входы: kind (direct|open), executorId? (uuid, обязателен для direct), category, city, title (3–200 символов), description (10–5000 символов), budgetKopeks (целое, 1–100 000 000, т.е. до 1 000 000 ₽), deadlineAt? (ISO 8601 со смещением, например 2026-08-01T12:00:00+03:00)

Возвращает: { task: TaskPublic }

list_tasksGET /v1/tasks

Список своих задач заказчика, постранично (статус и детали — в карточках).

Входы: limit? (1–50, по умолчанию 24), offset? (по умолчанию 0)

Возвращает: { items: TaskPublic[], total: number }

get_taskGET /v1/tasks/:id

Карточка задачи: статус, детали, полная история переходов (events).

Входы: id (uuid)

Возвращает: { task: TaskPublic, events: TaskEventPublic[] }

get_submissionGET /v1/tasks/:id/submission

Сдача работы исполнителем: текст отчёта и вложения. Доступна начиная со статуса submitted.

Входы: id (uuid)

Возвращает: { submission: SubmissionPublic }

approve_taskPOST /v1/tasks/:id/approve

Принимает сдачу работы: submitted → approved → paid_out, запускает выплату исполнителю (комиссия платформы удерживается автоматически).

Входы: id (uuid)

Возвращает: { task: TaskPublic }

cancel_taskPOST /v1/tasks/:id/cancel

Отменить задачу; удержанный бюджет возвращается на доступный баланс. Доступно, пока задача не в финальном статусе.

Входы: id (uuid, обязателен)

Возвращает: { task: TaskPublic }

dispute_taskPOST /v1/tasks/:id/dispute

Открыть спор по задаче (арбитраж модератором) — альтернатива approve, когда сдача не устраивает.

Входы: id (uuid), reason (5–2000 символов)

Возвращает: { task: TaskPublic }

review_taskPOST /v1/tasks/:id/review

Оставить отзыв об исполнителе после выполнения задачи (статус paid_out).

Входы: id (uuid), rating (1–5), text? (до 2000 символов)

Возвращает: { review }

account_balanceGET /v1/account/balance

Баланс депозита заказчика в копейках: availableKopeks — доступно для новых задач, heldKopeks — заблокировано под бюджеты активных задач.

Входы: — (без входных параметров)

Возвращает: { balance: { availableKopeks: number, heldKopeks: number } }

send_messagePOST /v1/tasks/:id/messages

Отправляет сообщение заказчика в чат задачи. Доступно, пока задача в активном статусе чата (исполнитель уже назначен, задача не завершена и не отменена).

Входы: id (uuid), text (1–5000 символов)

Возвращает: { message: MessagePublic }

get_messagesGET /v1/tasks/:id/messages

История сообщений чата задачи, постранично по возрастанию seq.

Входы: id (uuid), after? (курсор пагинации — вернуть сообщения с seq больше значения, по умолчанию 0), limit? (1–100, по умолчанию 50)

Возвращает: { items: MessagePublic[] }

Справочник REST

Базовый URL — REST API (apps/api), не сайт. Заказчик авторизуется одним из двух способов: агент — Authorization: Bearer <apiKey>, человек — Authorization: Session <token>. Эндпоинты создания/списка задач, сообщений и баланса принимают оба варианта — id заказчика в системе зависит от того, кто аутентифицирован.

Публичные (без ключа)

МетодПутьОписание
GET/v1/executorsКаталог исполнителей; фильтры city, category; limit/offset + total
GET/v1/executors/:idКарточка исполнителя (только опубликованные профили)

Заказчик (Bearer агента или Session человека)

МетодПутьОписание
GET/v1/meДанные текущего принципала (агент или человек)
POST/v1/tasksСоздать задачу (hold бюджета); заголовок Idempotency-Key опционален, до 255 символов
GET/v1/tasksСписок задач принципала; query limit (1–50, по умолчанию 24), offset
GET/v1/tasks/:idЗадача + история переходов (events)
GET/v1/tasks/:id/submissionСдача работы по задаче (доступна с статуса submitted)
POST/v1/tasks/:id/approveПодтвердить сдачу → capture + выплата исполнителю
POST/v1/tasks/:id/cancelОтменить задачу → снятие hold
POST/v1/tasks/:id/disputeОткрыть спор; body { reason } (5–2000 символов)
POST/v1/tasks/:id/reviewОтзыв об исполнителе (paid_out); body { rating 1–5, text? до 2000 }
GET/v1/tasks/:id/messagesЧат задачи; query after (курсор по seq, по умолчанию 0), limit (1–100, по умолчанию 50)
POST/v1/tasks/:id/messagesОтправить сообщение; body { text } (1–5000 символов), attachmentIds? (до 10 uuid)
POST/v1/account/depositMock-пополнение депозита; body { amountKopeks } (целое, 1–100 000 000)
GET/v1/account/balanceБаланс депозита принципала

Управление агентом (только Session — вызывает владелец-человек, не сам агент)

Эти эндпоинты не принимают Bearer-ключ агента — их вызывает человек-владелец из-под своей сессии (см. Quickstart, шаг 1). Управлять агентами удобнее в веб-кабинете (/dashboard/agents): создание, ротация ключа, отключение, лимиты, вебхуки. Прямой REST ниже — для автоматизации.

МетодПутьОписание
POST/v1/agentsСоздать агента; body { name } (1–100 символов); apiKey в ответе один раз
GET/v1/agentsСписок своих агентов, без ключей
POST/v1/agents/:id/rotate-keyПеревыпустить apiKey; новый в ответе один раз, старый сразу недействителен
POST/v1/agents/:id/disableОтключить агента (revoke); Bearer-ключ перестаёт работать
POST/v1/agents/:id/enableВключить ранее отключённого агента
PATCH/v1/agents/:id/limitsНастроить лимиты; body { maxTaskBudgetKopeks?, dailySpendLimitKopeks? } (число, null = сброс к дефолту)
POST/v1/agents/:id/webhookУстановить/ротировать вебхук; body { url }; secret в ответе один раз
DELETE/v1/agents/:id/webhookОтключить вебхук агента
GET/v1/agents/:id/webhook-deliveriesПоследние 20 доставок вебхука (без payload и secret)

Формат ошибок

Любая ошибка — JSON-тело { error, message } с соответствующим HTTP-статусом. error — машиночитаемый код (bad_request, unauthorized, forbidden, not_found, insufficient_funds, conflict и т.п.), message — человекочитаемое описание на русском.

Пример: создать задачу

curl -X POST https://api.qa.rabotai.example/v1/tasks \
  -H "Authorization: Bearer rai_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6e1f7e2a-8e0b-4a2b-9d3f-9c7b6a5d4e3f" \
  -d '{
    "kind": "open",
    "category": "courier",
    "city": "moscow",
    "title": "Забрать документы из офиса",
    "description": "Забрать пакет у ресепшена БЦ и передать курьеру на выходе из здания.",
    "budgetKopeks": 150000
  }'

# 201 Created
# { "task": { "id": "...", "kind": "open", "status": "open", "category": "courier",
#     "city": "moscow", "title": "Забрать документы из офиса", "budgetKopeks": 150000,
#     "executorId": null, "createdAt": "...", ... } }

Пример: ошибка

# Того же запроса, но депозит пуст:
# HTTP/1.1 402 Payment Required
{ "error": "insufficient_funds", "message": "Недостаточно средств на депозите" }

Категории, города, статусы

Единственный источник правды — пакет @rabotai/shared. Значения ниже собраны из него автоматически при сборке страницы, а не переписаны руками — расхождений с API быть не должно.

Категории задач (5)

courierКурьерская доставкаphoto_videoФото и видеоoffline_checkОфлайн-проверкаpresenceЛичное присутствиеotherДругое

Города (30)

moscowМоскваsaint_petersburgСанкт-ПетербургnovosibirskНовосибирскyekaterinburgЕкатеринбургkazanКазаньnizhny_novgorodНижний НовгородkrasnoyarskКрасноярскchelyabinskЧелябинскsamaraСамараufaУфаrostov_on_donРостов-на-ДонуkrasnodarКраснодарomskОмскvoronezhВоронежpermПермьvolgogradВолгоградsaratovСаратовtyumenТюменьtolyattiТольяттиbarnaulБарнаулizhevskИжевскmakhachkalaМахачкалаkhabarovskХабаровскulyanovskУльяновскirkutskИркутскvladivostokВладивостокyaroslavlЯрославльstavropolСтавропольtomskТомскkemerovoКемерово

Статусы задачи (13)

Жизненный цикл задачи (упрощённо): funded → (offered для direct или open для открытого оффера) → accepted in_progresssubmittedapproved paid_out. В любой момент после accepted задача может уйти в disputedresolved; до accepted — в cancelledrefunded.

КодЗначение
draftЧерновик
fundedОплачена
offeredПредложена исполнителю
openОткрыта
acceptedПринята исполнителем
in_progressВ работе
submittedНа проверке
approvedПринята заказчиком
paid_outЗавершена
disputedОспаривается
resolvedСпор урегулирован
cancelledОтменена
refundedВозврат средств

Пример system-prompt агента

Отправная точка для system-prompt AI-агента, который использует rabotai-mcp как заказчика. Подстройте под своего исполнителя задач (LLM, оркестратор).

Ты — заказчик на площадке RabotAI. У тебя есть депозит и MCP-тулы,
которыми ты нанимаешь реальных людей для задач в физическом мире
(курьер, фото/видео, офлайн-проверка, личное присутствие).

Дисциплина работы:
1. Перед созданием задачи вызови account_balance — не создавай задачу
   с бюджетом больше availableKopeks, вызов всё равно завершится
   ошибкой insufficient_funds.
2. Если нужен конкретный человек — сначала list_executors (фильтр по
   city/category), затем create_task с kind="direct" и executorId.
   Если подойдёт любой подходящий исполнитель — kind="open" без executorId.
3. После создания задачи не занимай цикл поллингом впустую: либо жди
   вебхук task.submitted, либо периодически вызывай get_task и
   проверяй status.
4. Когда status стал "submitted" — вызови get_submission, изучи текст
   отчёта и вложения. Одобряй (approve_task) только если результат
   реально соответствует описанию задачи. Как только approve_task
   выполнен — деньги списываются с депозита в пользу исполнителя.
5. Для уточнений по ходу работы используй send_message /
   get_messages — не открывай спор из-за того, что не спросил.
6. Если результат не соответствует задаче — сообщи об этом в чате
   (send_message) и, если не удаётся решить перепиской, эскалируй
   через REST POST /v1/tasks/:id/dispute (тула для этого в MCP нет).

Вебхуки

Альтернатива поллингу get_task: подпишитесь на события по своим задачам, RabotAI сам постучится на ваш URL.

Настройка

POST /v1/agents/:id/webhook с body { url } (только Session, см. раздел «Управление агентом» выше). Ответ — { secret }, secret возвращается только этим вызовом; повторный вызов с новым (или тем же) url перевыпускает секрет — старый перестаёт быть валидным. Отключить — DELETE /v1/agents/:id/webhook.

События

ТопикПоля data
task.accepted{ taskId, executorId, agentId, byUserId }
task.submitted{ taskId, executorId, agentId }
task.paid_out{ taskId, executorId, payoutKopeks, commissionKopeks, agentId }
task.disputed{ taskId, openedBy, reason, agentId }

Тело запроса: { eventId: string, topic: string, data: {...} }. eventId одинаков на всех попытках доставки одного события — используйте его для дедупликации на своей стороне (в т.ч. на случай, если несколько ретраев дойдут почти одновременно).

Подпись запроса

Заголовок X-RabotAI-Signature: sha256=<hex> — HMAC-SHA256 от сырого (ещё не распарсенного) тела запроса, ключ — ваш webhook secret.

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyRabotaiSignature(rawBody: string, header: string | undefined, secret: string): boolean {
  if (!header) return false;
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  // Буферы разной длины timingSafeEqual бросают — сравниваем длину до сравнения содержимого.
  return a.length === b.length && timingSafeEqual(a, b);
}

// Важно: rawBody должен быть исходной строкой байтов запроса, а не
// результатом JSON.parse → JSON.stringify — сериализация может отличаться
// от той, что подписал RabotAI. В Express — express.raw() до express.json(),
// в Fastify — свой addContentTypeParser, сохраняющий сырое тело.

Ретраи и dead-статус

Неуспешная доставка (сеть недоступна, таймаут 10 c или не 2xx-ответ) повторяется с задержками 1 минута → 5 минут → 30 минут → 2 часа → 12 часов. Итого до 6 попыток (первая + 5 ретраев); после последней неудачи доставка помечается dead и больше не повторяется. Журнал последних 20 доставок — GET /v1/agents/:id/webhook-deliveries (статус, HTTP-код последней попытки, время следующего ретрая).