/api/v1.application/json.Bearer JWT.planned_start_at (дата и время начала планового заказа).Idempotency-Key).Пример единого формата ошибки:
{
"error": {
"code": "ORDER_NO_LONGER_RELEVANT",
"message": "Заказ уже не актуален",
"details": {}
}
}
POST /auth/request-codeЗапрос кода подтверждения по номеру телефона.
Запрос:
{
"phone": "+79991234567"
}
Ответ 200:
{
"status": "code_sent"
}
POST /auth/verify-codeПодтверждение кода и выдача токенов.
Запрос:
{
"phone": "+79991234567",
"code": "1234"
}
Ответ 200 включает needs_registration: true, если нет full_name / current_mode.
POST /auth/complete-registrationИмя и режим: client |
executor. Пишется в current_mode. При executor создаётся executor_profile. |
GET /users/meПрофиль, режим, контакты. Для заказа нужны имя и хотя бы один канал: contact_phone |
telegram_url |
whatsapp_url |
max_url |
доп. мессенджер. |
{
"full_name": "Иван Петров",
"current_mode": "client",
"contact_phone": "+79991234567",
"telegram_url": "https://t.me/ivan",
"whatsapp_url": null,
"max_url": null,
"messengers": []
}
Поля Telegram / WhatsApp / MAX на форме всегда. Доп. мессенджеры — кнопка «добавить».
PATCH /users/meПрофиль, current_mode, контакты.
PATCH /users/me/modeПереключение current_mode. При первом переходе в executor создаётся executor_profile.
GET /users/{userId}/reviewsПубличные отзывы о пользователе. Параметр role=executor (по умолчанию) — отзывы о нём как об исполнителе: звёзды, текст, дата, роль автора. role=client — только сводка (avg, count), без текста; используется в карточке входящего запроса у исполнителя. Без контактов авторов. Доступно всем авторизованным. Параметры: page, limit.
Исполнитель сам добавляет транспорт. В поиске: лицензия категории approved (если у типа license_category не пустой), при включённом флаге — активная подписка, для срочных заказов — accepts_urgent_orders.
GET /categoriesКаждый элемент — строка типа транспорта: системный code, текст для UI, категория прав, разрешённые тарифы. JSON-конфига лицензий нет.
{
"id": "uuid",
"code": "tow_truck",
"name_ru": "Эвакуатор",
"license_category": "B",
"pricing_types": ["hour", "shift", "day", "month", "trip", "km"]
}
license_category: null — лицензия для типа не нужна.
PATCH /executor/profilemultipart/form-data: about, файл avatar (не URL с клиента; в ответе — avatar_url). Сам профиль создаётся автоматически при регистрации в режиме исполнителя или при первом переключении режима; отдельного метода создания нет.
GET /executor/settingsОтвет 200:
{
"accepts_urgent_orders": false
}
PATCH /executor/settingsЧекбокс «принимаю срочные заказы» на экране профиля. Меняет только исполнитель; сервер сам его не переключает. Флаг на уровне исполнителя, не машины.
Запрос:
{
"accepts_urgent_orders": true
}
При false исполнитель не попадает в выдачу по срочным заказам; по плановым виден. Если executor_subscription_enabled и подписка не active — не в поиске вообще (409 SUBSCRIPTION_REQUIRED на попытку включить флаг без оплаты).
POST /executor/subscription/payТолько при включённом флаге. Создаёт платёж подписки; webhook активирует профиль.
GET /executor/machineryСписок единиц транспорта текущего исполнителя (без удалённых), с фото и тарифами.
POST /executor/machineryContent-Type: multipart/form-data. Фото — файлы (photos[]), не URL с клиента, минимум одно.
Поля формы: category_id, address_text, location_lat, location_lng, pricing_rules (JSON-строка), файлы фото.
Ошибки:
400 VALIDATION_ERROR — нет типа, нет тарифов, нет ни одного фото, base_rate <= 0, неизвестный pricing_type;400 PRICING_TYPE_NOT_ALLOWED_FOR_CATEGORY — тариф не разрешён типу транспорта;403 EXECUTOR_PROFILE_REQUIRED — нет профиля исполнителя.Ответ 201 включает visible_in_search: false, если лицензия категории ещё не approved.
POST /executor/licensesДокументы лицензии на категорию (category_id + файлы). Статус pending, пока админ не подтвердит. Повторная подача по той же категории заменяет pending.
GET /executor/licensesСписок лицензий текущего исполнителя.
GET /executor/machinery/{machineryId}Карточка своей единицы с фото и тарифами.
PATCH /executor/machinery/{machineryId}Обновление типа / локации / тарифов. Новые фото — тоже файлами (multipart). Смена ставок не трогает уже посчитанные цены в заказах: order_pricing фиксируется на момент отправки запроса или правки заказчиком.
DELETE /executor/machinery/{machineryId}/photos/{photoId}Удаление фото. Последнее фото удалить нельзя — 409 LAST_PHOTO.
DELETE /executor/machinery/{machineryId}Мягкое удаление: deleted_at заполняется, единица исчезает из поиска и из выдачи списка.
Если единица связана с заказом в in_negotiation / awaiting_payment / in_progress / disputed — 409 MACHINERY_IN_ACTIVE_ORDER.
Элемент выдачи — единица транспорта, не карточка исполнителя как целого. Контакты в выдаче не отдаются. Занятость по времени не проверяется.
GET /search/machineryПараметры фильтра заказа — SEARCH_ALGORITHM.md:
category_id, pricing_type, quantity, search_mode (обязательные);planned_start_at (обязателен при search_mode=planned, запрещён при urgent; на выдачу не влияет, копируется в заказ);lat, lng (обязательные);max_amount, radius_km (опциональные фильтры);exclude_for_order_id (опционально; при поиске внутри существующего заказа — обязательно передавать).При search_mode=urgent отдаются только исполнители с accepts_urgent_orders = true. Сортировка: расстояние, при равенстве цена. Контактов нет. В карточке — ставка выбранного тарифа, computed_amount, сводка отзывов исполнителя.
Список: плоский массив карточек машин. Карта группирует элементы с одинаковыми координатами: на точке число +N, по клику — список машин этой точки (снизу).
Ответ 200:
{
"items": [
{
"machinery_unit_id": "uuid",
"category_id": "uuid",
"cover_photo_url": "https://...",
"address_text": "Москва, Каширское шоссе, 31",
"location_lat": 55.618,
"location_lng": 37.688,
"distance_km": 3.2,
"pricing_type": "hour",
"unit_rate": 3500,
"quantity": 8,
"computed_amount": 28000,
"currency": "RUB",
"executor": {
"user_id": "uuid",
"full_name": "Иван Петров",
"avatar_url": "https://...",
"reviews_avg": 4.7,
"reviews_count": 12
}
}
]
}
GET /search/machinery/{machineryId}Экран перед запросом: исполнитель + эта единица + последние отзывы. Контактов нет.
GET /categories/{categoryId}/pricing-typesТарифы, разрешённые категории (из category_pricing_types).
POST /orderssave_as_draft: true → статус draft, без кандидата. Иначе сразу published и запрос одному исполнителю.
В теле: executor_user_id и machinery_unit_id обязательны, если не черновик; category_id, pricing_type, quantity, search_mode, planned_start_at (если planned; только в будущем), точка (address_text, location_lat, location_lng), description (необязательно), settlement_mode (direct_payment по умолчанию; secure_deal — переключатель на экране отправки, виден только при включённом флаге). Даты окончания нет. Сервер пишет unit_rate и computed_amount по текущим ставкам машины и search_activity_at.
Ошибки:
400 VALIDATION_ERROR — в том числе planned_start_at в прошлом или отсутствует при planned, задан при urgent;409 PROFILE_INCOMPLETE — нет имени или ни одного канала связи;409 CANNOT_ORDER_OWN_MACHINERY;409 LICENSE_NOT_APPROVED — машина не должна была попасть в поиск;409 EXECUTOR_NOT_ACCEPTING_URGENT — срочный запрос исполнителю с выключенным флагом;409 SUBSCRIPTION_REQUIRED — при включённом флаге подписки исполнитель без активной подписки;409 SECURE_DEAL_DISABLED — secure_deal при выключенном secure_deal_enabled.PATCH /orders/{orderId}Правка полей фильтра (тариф, количество, дата начала — только в будущем, адрес, описание; на draft / published — ещё и settlement_mode). Цена пересчитывается по текущим ставкам машины и фиксируется в order_pricing.
draft — любые поля.published — только если нет кандидата notified (иначе 409 PENDING_CANDIDATE). Статус не меняется, search_activity_at обновляется.in_negotiation — details_version++, дедлайн подтверждения условий заново, уведомление исполнителю.awaiting_payment — отменить/вернуть платёж, статус in_negotiation, details_version++, уведомление исполнителю.Иначе 409 INVALID_STATE_TRANSITION.
POST /orders/{orderId}/publishЧерновик → published без кандидата. Дальше клиент выбирает машину в выдаче (GET /search/machinery?exclude_for_order_id=...) и вызывает request-executor.
Ответ 201:
{
"id": "uuid",
"status": "published",
"candidate": null
}
POST /orders/{orderId}/request-executorОтправка запроса исполнителю внутри существующего заказа. Только published, нет кандидата в notified. Обновляет search_activity_at.
Запрещено, если по заказу у этого исполнителя уже есть declined, auto_declined, expired, negotiation_rejected или terms_expired. После withdrawn — разрешено.
Ошибки: те же, что у POST /orders (PROFILE_INCOMPLETE, CANNOT_ORDER_OWN_MACHINERY, LICENSE_NOT_APPROVED, EXECUTOR_NOT_ACCEPTING_URGENT, SUBSCRIPTION_REQUIRED), плюс 409 PENDING_CANDIDATE, 409 EXECUTOR_EXCLUDED.
Запрос:
{
"executor_user_id": "uuid",
"machinery_unit_id": "uuid"
}
Ответ 201:
{
"candidate": {
"id": "uuid",
"executor_user_id": "uuid",
"candidate_status": "notified",
"response_deadline_at": "2026-09-07T11:25:00Z"
}
}
POST /orders/{orderId}/confirm-termsИсполнитель фиксирует текущие условия. Только in_negotiation, до terms_confirm_deadline_at. Тело: { "details_version": 2 }. Несовпадение версии — 409 STALE_ORDER_DETAILS.
direct_payment → in_progress + assignment. secure_deal → awaiting_payment + payment_deadline_at.
GET /orders/{orderId}Карточка заказа со статусом, текущим кандидатом, дедлайнами, ценой и назначением. Открытие из уведомления статус не меняет. Исполнителю до принятия — без контактов заказчика, но со сводкой отзывов о нём как о клиенте (client_reviews_avg, client_reviews_count).
Если исполнитель больше не актуальный кандидат, карточка всё равно отдаётся (можно прочитать, что отказал / не ответил / принял другой заказ). Действия accept / decline / confirm-terms на неактуальном кандидате — 409 ORDER_NO_LONGER_RELEVANT.
GET /ordersИстория заказов текущего пользователя. По умолчанию без cancelled и без draft. Архив отменённых: status=cancelled. Черновики: status=draft.
Для исполнителя этот же список — входящие и активные (не только из пуша).
Параметры:
role=client|executor;status (в т.ч. draft — вкладка черновиков);page, limit.POST /orders/{orderId}/repeatТолько для cancelled или для completed после принятия (обе стороны или автоподтверждение). Иначе 409 REPEAT_NOT_ALLOWED. Создаёт новый заказ с копией полей, repeated_from_order_id = исходный, статус published без кандидата. Клиент попадает в поиск. На исходном заказе поиск исполнителей не возобновляется.
Ответ 201:
{
"id": "uuid",
"status": "published",
"repeated_from_order_id": "uuid"
}
После отмены клиентом UI сразу спрашивает «искать снова?». После отмены исполнителем (плановый до начала) или системой («исполнитель не найден») тот же вопрос открывается из уведомления клиенту.
POST /orders/{orderId}/candidates/{candidateId}/withdrawКлиент снимает запрос до принятия или отказа. Кандидат → withdrawn, заказ остаётся published.
POST /orders/{orderId}/reject-negotiationКлиент или исполнитель отклоняет партнёра. Доступно на in_negotiation и awaiting_payment. Нужна причина стадии. Кандидат → negotiation_rejected, назначение снимается, заказ → published. Если на awaiting_payment был создан платёж — он отменяется / холд возвращается до смены статуса.
Для исполнителя это единственный способ выйти из заказа до начала работы.
Запрос:
{
"reason_code": "price_disagreement",
"reason_text": null
}
POST /orders/{orderId}/cancelПолная отмена. Доступно:
клиенту на draft |
published |
in_negotiation |
awaiting_payment; |
in_progress, если заказ плановый и planned_start_at ещё не наступил.Иначе 409 CANCEL_NOT_ALLOWED (в том числе исполнителю на in_negotiation / awaiting_payment — ему reject-negotiation; и любому на in_progress после начала — только спор).
На published причина не нужна; ожидающий кандидат, если есть, закрывается как withdrawn, исполнителю уведомление. На остальных — код стадии (CANCELLATION_REASONS.md; для планового до начала — стадия planned_before_start). Платёж/холд, если есть, отменяется или возвращается, затем cancelled.
Запрос:
{
"reason_code": "found_another",
"reason_text": null
}
Код должен быть разрешён стадии (400 REASON_NOT_ALLOWED_FOR_STAGE). При other обязателен reason_text; рейтинг не меняется, пока админ не разберёт.
Ответ 200 может включать флаг для клиента: показать диалог «искать снова» (prompt_repeat: true), если отменил он сам. Если отменил исполнитель — клиенту уходит уведомление с тем же вопросом.
POST /orders/{orderId}/disputesТолько из in_progress или completed (в том числе после подтверждения одной стороны). Иначе 409 DISPUTE_NOT_ALLOWED.
multipart/form-data: reason_code обязателен (коды стадии; not_paid — только при direct_payment), при other — reason_text. Фото к причине нет.
Переводит заказ в disputed, создаёт тред, отменяет таймер автоприёмки (execution_confirm_deadline_at), если он шёл. Из приложения пишут клиент и исполнитель.
Если по заказу уже есть закрытый спор — 409 USE_APPEAL (нужен POST .../dispute/appeal).
Админ в эти методы приложения не ходит.
POST /orders/{orderId}/dispute/appealПосле того как админ закрыл спор. Та же запись disputes снова open, appeal_used = true. Не больше одной апелляции.
Иначе 409 APPEAL_NOT_ALLOWED или 409 APPEAL_LIMIT_REACHED.
Тело: текст, почему не согласны с решением (reason_text).
GET /orders/{orderId}/disputeОдна запись спора и её сообщения.
POST /orders/{orderId}/dispute/messagesСообщение от клиента или исполнителя. multipart/form-data: body + опционально photos[] (файлы).
Запрос (если без фото, можно JSON):
{
"body": "Текст"
}
POST /orders/{orderId}/confirm-executionЛюбая сторона на in_progress. Когда подтвердили оба — completed и выплата при сделке. Если подтвердил один — ставится execution_confirm_deadline_at = сейчас + 1 сутки; молчание второй стороны → автоподтверждение (late_confirm −3 молчавшему). Открытый спор до истечения таймера — таймер снимается.
POST /orders/{orderId}/reviews{ "stars": 5, "body": null } после completed, по одному от каждой стороны. Иначе 409 REVIEW_NOT_ALLOWED.
Отдельной выдачи «только из пуша» нет: у исполнителя список входящих через GET /orders?role=executor и экран уведомлений.
POST /orders/{orderId}/candidates/{candidateId}/acceptПервое принятие. Только published, до дедлайна. После успеха — контакты, статус in_negotiation, terms_confirm_deadline_at = сейчас + 1 сутки. Условия здесь не фиксируются.
Если принятый заказ срочный — все остальные notified кандидаты этого исполнителя по другим срочным заказам (по любой его машине) → auto_declined; их заказы остаются published; клиентам уведомление «исполнитель отказался». Кандидаты по плановым заказам не трогаются. Если принятый заказ плановый — ничего не закрывается. Флаг accepts_urgent_orders не меняется.
Требования:
Idempotency-Key;Ответ 200:
{
"result": "accepted",
"order_id": "uuid",
"status": "in_negotiation",
"terms_confirm_deadline_at": "2026-09-08T11:20:00Z"
}
После успеха фиксируется обмен контактами. Цена заказа здесь не выставляется.
Ошибки:
409 ORDER_NO_LONGER_RELEVANT — дедлайн прошёл, уже отказ/автоотказ/истечение, заказ не в published;409 PROFILE_INCOMPLETE — нет имени или ни одного канала;409 SUBSCRIPTION_REQUIRED — при включённом флаге подписки без активной подписки;409 INVALID_STATE_TRANSITION.POST /orders/{orderId}/candidates/{candidateId}/declineПервый отказ исполнителя. Заказ остаётся published. Нужна причина (пишется в order_candidates.reject_reason_*, не в заказ).
Запрос:
{
"reason_code": "busy_or_broken",
"reason_text": null
}
Фиксация условий — POST /orders/{orderId}/confirm-terms, не этот метод.
Обмен фиксируется при первом accept. Показываются contact_phone, telegram_url, whatsapp_url, max_url, доп. messengers.
Этот раздел может быть отключен feature-флагом до этапа 2.
POST /orders/{orderId}/secure-deal/create-paymentСоздание платежа у провайдера. Только awaiting_payment + secure_deal. Сумма = order_pricing.computed_amount (клиент сумму не передаёт). После холда authorized → in_progress.
Запрос:
{
"return_url": "spectaxi://payment-return"
}
Ответ 200:
{
"payment_id": "uuid",
"provider": "yookassa",
"provider_payment_id": "2f8f...",
"status": "pending",
"payment_url": "https://..."
}
GET /orders/{orderId}/payment-statusПолучение текущего платежного статуса.
POST /payments/webhooks/{providerCode}Webhook от платежного провайдера.
Требования:
provider_event_id;payment_webhook_events.Если провайдер вернул холд по expires_at и статус платежа был authorized — заказ → disputed. Неоплаченный заказ провайдер не трогает; наш таймаут возвращает заказ в published. Срок жизни холда у провайдера — открытый вопрос (PRODUCT_FOUNDATION.md, раздел 12).
Перечень событий — USER_SCENARIOS.md, раздел 13.
GET /notificationsЛента текущего пользователя, новые сверху.
Параметры: page, limit, unread=true (опционально).
POST /notifications/{notificationId}/readПометить прочитанным. На статус кандидата не влияет.
Закрытие спора, сообщения админа, блокировки, отмена заказов и разбор other — только здесь.
POST /admin/licenses/{licenseId}/review{ "decision": "approved" | "rejected", "comment": null }. Исполнителю уведомление.
POST /admin/orders/{orderId}/dispute/messagesСообщение от админа. Тоже можно прикрепить фото (photos[]).
POST /admin/orders/{orderId}/dispute/closeАдмин выбирает статус, в который перевести заказ: только completed, cancelled, in_progress. Нельзя published.
Запрос:
{
"status": "cancelled",
"sanction_client_delta": null,
"sanction_executor_delta": null
}
Деньги:
authorized (холд, ещё не выплата) — админ может выплатить или вернуть;captured — денег не трогать, только статус и опциональные санкции; закрытие в cancelled возврата не делает;POST /admin/orders/{orderId}/cancelОтмена заказа админом из любого статуса, кроме completed. На published ожидающий кандидат закрывается как withdrawn. На disputed одновременно закрывает спор с исходом cancelled. Холд authorized возвращается; captured не трогается. cancelled_by_role = admin, штрафа нет, обеим сторонам уведомление.
Запрос: { "comment": "Причина для аудита" }.
Типовое применение — открытые заказы заблокированного пользователя.
POST /admin/users/{userId}/block / POST /admin/users/{userId}/unblock{ "reason": "..." }. Заполняет / очищает blocked_at, blocked_reason. Заблокированный: не в поиске, не создаёт и не принимает заказы; его ожидающие кандидаты закрываются как declined без штрафа; активные заказы админ отменяет отдельно POST /admin/orders/{id}/cancel.
GET /admin/rating-events?status=pending_adminОчередь событий с причиной other.
POST /admin/rating-events/{eventId}/resolve{ "delta": -6, "comment": null }. Проставляет дельту (в пределах шкалы) и закрывает событие. delta: 0 — без штрафа.
orders.status)draftpublishedin_negotiationawaiting_paymentin_progresscompleteddisputedcancelledorder_candidates.candidate_status)notifiedaccepteddeclinedauto_declinedexpiredwithdrawnnegotiation_rejectedterms_expiredpayment_records.payment_status)not_requiredpendingauthorizedcapturedfailedcancelledrefundednotified → expired, заказ published, исполнителю −3.terms_confirm_deadline_at → terms_expired, заказ published, −3.payment_deadline_at → платёж отменить, заказ published, клиенту −3.execution_confirm_deadline_at → автоподтверждение, completed, late_confirm −3.published без notified — срочный через 24 ч после search_activity_at (последняя отправка запроса / правка / вход в поиск), плановый при planned_start_at → cancelled (system).accept / истечение 5 минут / автоотказ остальных срочных: атомарность важнее гонки.GET /search/machinery: p95 < 600 мс при базовой геовыборке.direct_payment продолжает работать.