# upload.am API v1 Публичный REST API upload.am — загружайте, храните, скачивайте и делитесь файлами из собственных скриптов, серверов и приложений, теми же возможностями, что доступны на сайте (пароль на файл, лимит скачиваний, срок жизни ссылки, публичные ссылки, групповые ссылки на несколько файлов). Базовый URL: `https://upload.am/api/v1` Все ответы — JSON (`Content-Type: application/json`). Отправляйте `Accept: application/json` в каждом запросе. --- ## 1. Аутентификация Два способа получить токен. ### 1.1 Логин email + пароль (официальный клиент Upload.am) Без заголовка `Authorization`: ```bash curl -X POST https://upload.am/api/v1/auth/login \ -H "Accept: application/json" -H "Content-Type: application/json" \ -d '{"email":"you@example.com","password":"your-password","device_name":"Upload.am for Windows"}' ``` `device_name` — необязательное, до 80 символов, отображается в /cabinet/api как имя выданного токена (чтобы разные клиенты/устройства были различимы в списке активных токенов). Если не передать — токен получит имя `Upload.am for Windows` (фоллбек для обратной совместимости, не значит, что клиент действительно Windows). ```json { "token": "ua_live_xxxxxxxx", "token_name": "Upload.am for Windows", "user": { "id": 1, "email": "you@example.com", "name": "...", "plan": {}, "storage_used_bytes": 0, "storage_remaining_bytes": 0 } } ``` Ошибки: `401 invalid_credentials`, `403 account_disabled`. Лимит — 10 попыток в минуту с IP. **`POST /auth/login` по умолчанию не проверяет 2FA** — решение владельца, 21.09.2026 (см. `BACKLOG.md`, "Отклонённое"): API-логин остаётся email+пароль независимо от `two_factor_enabled` и «подтверждения новых устройств» на аккаунте; веб-логин (`/login`) 2FA требует как раньше. Это сознательный компромисс: через приложение защиту можно обойти. Если владелец включит в `/admin/settings` «Требовать второй фактор при входе из приложений и API», `POST /auth/login` для аккаунтов с 2FA или подтверждением нового устройства вернёт `202`: ```json { "error": "verification_required", "challenge": "…" } ``` и пришлёт код на почту; токен выдаёт `POST /auth/verify` с телом `{ "challenge": "…", "code": "123456" }` (код из письма или резервный код; challenge живёт 10 минут и одноразовый, после 5 неверных кодов код сгорает → `401 invalid_code`). Клиентам нужно поддержать этот шаг до включения переключателя. **Вход через Google (13.10.2026, для официальных приложений).** Системный браузер открывает `GET https://upload.am/auth/google/app?port=<порт>&cs=<случайная hex-строка 16–64>&l=<язык>`, где `port` — loopback-порт (1024–65535), на котором приложение слушает `127.0.0.1`, а `cs` — секрет, известный только приложению. После выбора аккаунта сервер перенаправляет браузер на `http://127.0.0.1:<порт>/callback?code=<одноразовый код>&state=` (или `?error=…&state=`). Приложение сверяет `state` с `cs` и меняет код: `POST /auth/google/exchange {"code","state","device_name"}` → тот же ответ, что у `/auth/login`: `200 {token,user}`, либо `202 verification_required` (дальше `/auth/verify`), `401 invalid_code` (неверный/просроченный/уже использованный код или чужой `state`), `403 account_disabled`. Код живёт 5 минут и сгорает при первой попытке. Новый Google-аккаунт создаётся на Free, как на сайте. Выход — отзывает текущий токен: ```bash curl -X POST https://upload.am/api/v1/auth/logout \ -H "Authorization: Bearer ua_live_xxxxxxxx" -H "Accept: application/json" ``` Ответ `204`. ### 1.2 Токен из кабинета Токен по-прежнему можно создать в **Account → API** (`/cabinet/api`). Показывается один раз. Все остальные эндпоинты требуют: ``` Authorization: Bearer ua_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Токен даёт полный доступ аккаунта (узких скоупов в v1 нет). ```bash curl https://upload.am/api/v1/me \ -H "Authorization: Bearer ua_live_xxx" \ -H "Accept: application/json" ``` ### Ошибки аутентификации | HTTP | `error` | Причина | |------|--------------------|---------| | 401 | `missing_token` | Заголовок `Authorization` отсутствует или пуст | | 401 | `invalid_token` | Токен не найден или отозван | | 401 | `expired_token` | У токена истёк срок действия | | 401 | `invalid_credentials` | Неверные email или пароль на `/auth/login` | | 403 | `account_disabled` | Аккаунт заблокирован администрацией | --- ## 2. Лимиты запросов (rate limits) У каждого эндпоинта свой независимый счётчик (см. таблицу ниже). При превышении — `429 Too Many Requests` с заголовком `Retry-After`. Лимиты считаются на токен, не на IP. Квота хранилища и лимит размера одного файла — по тарифу аккаунта, тому же самому, что и на сайте (`GET /me` возвращает точные цифры). Скорость скачивания не ограничена ни на одном тарифе — как и на сайте. --- ## 3. `GET /me` Информация об аккаунте, тарифе и квоте — вызывайте перед первой загрузкой, чтобы знать реальные лимиты, не хардкодить цифры с `/price`. ```bash curl https://upload.am/api/v1/me -H "Authorization: Bearer ua_live_xxx" ``` ```json { "id": 42, "email": "you@example.com", "name": "Jane", "plan": { "code": "premium", "name": "Premium", "storage_bytes": 2199023255552, "max_file_bytes": 107374182400, "retention_days": 180, "skips_download_wait": true, "max_parallel_uploads": 10, "vault_enabled": true }, "storage_used_bytes": 15728640, "storage_remaining_bytes": 2199007526912, "referral_bonus_bytes": 0, "referral_enabled": true, "referral_code": "abc123defg", "referral_url": "https://upload.am/en/register?ref=abc123defg", "referral_count": 2, "referral_bonus_per_invite_bytes": 53687091200, "notify_on_download": true, "notify_expiring_shares": true, "two_factor_enabled": false } ``` `plan.vault_enabled` — добавлено 16.09.2026. `true` на любом платном тарифе, `false` на Free. Единственный источник правды для доступа к платному Vault (шифрованный раздел) в клиентах — не хардкодьте список кодов тарифов на стороне клиента, читайте этот флаг при каждом запуске и после апгрейда/даунгрейда. `skips_download_wait` — на платных тарифах ссылка на скачивание выдаётся мгновенно; на Free перед выдачей ссылки нужно один раз "подождать" 10 секунд (см. раздел 6). `max_parallel_uploads` — сколько файлов клиент может грузить одновременно на этом тарифе (то же значение, что веб-загрузчик читает как `data-max-parallel`). Добавлено 16.09.2026, чтобы нативные клиенты (Android/Windows) могли параллелить загрузку так же, как веб, вместо жёстко зашитого числа. `referral_bonus_bytes` — добавлено 19.09.2026 вместе с реферальной программой ("позови друга, получи бонус к хранилищу"). Уже накопленный бонус в байтах, учтён в `storage_remaining_bytes` выше. `referral_bonus_per_invite_bytes` — добавлено 22.09.2026. Размер бонуса за ОДНОГО приглашённого друга (не накопленный итог, как поле выше) — настраивается в `/admin/settings` (`referral_bonus_gb`), поэтому клиент должен читать это значение, а не хардкодить число в тексте кнопки "Invite a friend". `referral_enabled`/`referral_code`/`referral_url`/`referral_count` — добавлено 22.09.2026 (десктоп-клиент, `unixanet/backup`). Раньше в `GET /me` был только `referral_bonus_bytes` — клиент не мог показать сам экран приглашения (ссылку, код, сколько друзей уже приведено). `referral_code` создаётся лениво при первом запросе, если у юзера его ещё нет (тот же побочный эффект, что уже был в `dashboard/account.blade.php`). Все четыре поля — `null`/`false`, если реферальная программа выключена в `/admin/settings`. `notify_on_download`/`notify_expiring_shares` — настройки email-уведомлений, зеркалят `dashboard/account.blade.php`. Меняются через `PATCH /me/preferences` ниже. Поля `payout_wallet_*` и `PATCH /me/payout-wallet` удалены 11.10.2026 вместе с партнёрской программой (клиенты должны перестать их читать/слать). `two_factor_enabled` — можно менять через `PATCH /me/two-factor` ниже (Раунд 13, 22.09.2026). Раньше был только для чтения — см. историю в разделе 11: `POST /auth/login` не проверял 2FA вообще, включение отсюда дало бы ложное чувство защищённости. Теперь `POST /auth/login` тоже проверяет 2FA (тот же email-код + резервные коды, что и веб-логин) — см. `POST /auth/login/verify`. ### Тариф, функции и лимиты в `GET /me` (11–12.10.2026) В ответе `GET /me` появились два блока. **Читайте их при каждом запуске и после смены тарифа — не хардкодьте коды тарифов.** ```json { "plan": { "code": "business_5", "name": "Business 5", "features": ["ocr", "esign", "templates", "online_editor", "work_tools", "backups", "block_sync", "data_room"] }, "entitlements": { "source": "own", "limits": [ { "key": "storage", "used": 15728640, "max": 5497558138880 }, { "key": "file", "used": 0, "max": 107374182400 }, { "key": "rooms", "used": 1, "max": null }, { "key": "api_tokens", "used": 2, "max": null }, { "key": "active_links", "used": 4, "max": null }, { "key": "shared_folders", "used": 0, "max": null }, { "key": "history_days", "used": 0, "max": null } ], "features": [ { "key": "esign", "allowed": true, "min_plan": null }, { "key": "sso", "allowed": false, "min_plan": "Enterprise" } ] } } ``` - `plan.features` — ключи функций, включённых в тариф: `esign`, `ocr`, `online_editor`, `templates`, `work_tools`, `data_room`, `backups`, `block_sync`, `legal_hold`, `dlp`, `sso`. Если на сервере выключена проверка функций по тарифам, приходят все ключи. - `entitlements.source` — откуда действует тариф: `own` (свой), `family` (общий семейный) или `free`. - `entitlements.limits[]` — `key`, `used`, `max`; `max: null` значит «без ограничения» (не 0!). Ключи: `storage`, `file`, `rooms`, `portals`, `household`, `seats`, `spaces`, а также `api_tokens`, `active_links`, `shared_folders`, `history_days`. - `entitlements.features[]` — каждая функция с `allowed` и названием тарифа `min_plan`, где она доступна. Для закрытых функций клиент показывает серую строку «доступно на тарифе …». - Закрытая функция в API отвечает `403 {"error":"plan_feature_locked","feature":"esign","min_plan":"Premium"}`. - Исчерпанный лимит при создании ссылки: `403 {"error":"plan_limit_reached","limit":"active_links","max":N,"message":"…"}` — показывайте `message`, повтор бессмысленен до смены тарифа или освобождения места под лимитом. - Тарифы в `GET /billing/plans` теперь содержат `family` (`personal`, `family`, `business`, `enterprise`), `price_year_cents`, `household_max` и `features`; снятые с продажи не приходят. ### `PATCH /me` — сменить имя/email Оба поля необязательны, но хотя бы одно нужно прислать. `email` проверяется на уникальность (кроме вас самих). Ответ — тот же объект, что `GET /me`. ```json { "name": "Jane Doe", "email": "jane@example.com" } ``` ### `PATCH /me/password` — сменить пароль Требует текущий пароль — без него 422. `password_confirmation` обязателен и должен совпадать с `password` (Laravel `confirmed`), минимум 8 символов. ```json { "current_password": "старый", "password": "новыйПароль123", "password_confirmation": "новыйПароль123" } ``` Смена пароля не отзывает токены других устройств — как и в веб-кабинете. ### `PATCH /me/preferences` — уведомления на почту Добавлено 22.09.2026 (десктоп-клиент). Зеркалит `dashboard/account.blade.php`. Оба поля — булевы, отправляйте оба сразу (checkbox off = поле присутствует, но `false`, не отсутствует, как обычно у HTML-форм — здесь это обычный JSON). ```json { "notify_on_download": true, "notify_expiring_shares": false } ``` Ответ — тот же объект, что `GET /me`. ### `PATCH /me/two-factor` — включить/выключить 2FA Добавлено 22.09.2026 (Раунд 13). Зеркалит `Dashboard\AccountController::updateTwoFactor`. ```json { "two_factor_enabled": true } ``` При включении (`false → true`) генерирует 8 новых резервных кодов и отдаёт их **открытым текстом ровно один раз** в этом же ответе — в базе хранятся только их хэши, как у пароля: ```json { "...": "...", "two_factor_enabled": true, "two_factor_recovery_codes": ["ABCD-1234", "..."] } ``` При выключении — `two_factor_recovery_codes: null`, старые резервные коды инвалидируются. Повторное включение (`true → true`, обычное сохранение формы) не генерирует новый набор — только реальный переход `false → true`. **Важно, честно:** `POST /auth/login` (тот эндпоинт, которым сам десктоп-клиент и логинится) 2FA не проверяет — см. раздел 1.1, решение владельца 21.09.2026. Включение здесь защищает вход через **сайт** (`/login`), как и раньше; десктоп-/Android-клиенты продолжают заходить по одному паролю независимо от этого тумблера. Клиентский UI должен показывать это прямо, не изображать полноценную защиту там, где её нет. ### `GET /billing/plans` — платные тарифы для апгрейда Добавлено 22.09.2026 (Раунд 13, десктоп-клиент). Зеркалит `Dashboard\BillingController::plans()` — тот же список (без Free/Team). ```json { "plans": [{ "code": "premium", "name": "Premium", "price_cents": 500, "storage_bytes": 107374182400, "max_file_bytes": ..., "retention_days": null }] } ``` ### `POST /billing/{planCode}/checkout` — крипто-чекаут (Shieldz) Добавлено 22.09.2026. Зеркалит `Dashboard\BillingController::checkout()` — тот же `ShieldzGateway`. Веб-версия делает `redirect()->away($url)`; здесь — тот же URL как JSON, клиент открывает его в браузере (сам процесс оплаты — на хостинге Shieldz, не внутри приложения, ровно как и у веб-кабинета): ```json { "checkout_url": "https://pay.shieldz.io/..." } ``` 404 `invalid_plan` (код не найден, план Free или Team — те продаются иначе), 503 `no_payment_method` (крипто-оплата выключена в `/admin/settings`), 500 `checkout_failed` (ShieldzGateway бросил исключение — `message` из исключения, залогировано на сервере). ### `POST /me/coupon` — активировать купон Добавлено 22.09.2026. Та же логика, что `App\Support\CouponRedeemer` на сайте (`dashboard.coupon.redeem`) — купон применяется к тарифу немедленно. ```json { "code": "WELCOME50" } ``` 422 с `{ "error": "not_found" | "not_usable" | "already_used" | "empty", "message": "..." }` при неудаче — коды и тексты те же, что на сайте (`lang/*/coupon.php`). Успех — тот же объект, что `GET /me` (с уже применённым тарифом/квотой). ### `GET /me/billing` — разбивка по типам файлов + история платежей Добавлено 22.09.2026 (десктоп-клиент). Сознательно НЕ в `GET /me` — тот эндпоинт опрашивается клиентом каждые ~30 секунд, а тут две агрегирующие SUM-выборки; вызывайте лениво, когда юзер реально открывает экран Account/Billing. ```json { "storage_breakdown": { "video": 1073741824, "image": 209715200, "file": 1048576 }, "payment_history": [ { "plan_name": "Premium", "amount_usd_cents": 999, "status": "paid", "created_at": "2026-09-01T12:00:00Z" } ] } ``` `storage_breakdown` — ключи те же категории, что `App\Support\FileIcon::category()` (`image`/`video`/`audio`/`file` и т.д.), отсортировано по убыванию байт. Пустой объект, если файлов нет. `payment_history` — последние 20, новые сверху; пустой массив, если платежей не было (например, юзер всегда на Free). ### Аватар профиля `GET /me` и `PATCH /me` включают `avatar_url` — presigned-ссылка (кэш на 20 часов) или `null`, если аватар не задан. Загрузка — тот же двухшаговый поток, что и обычные файлы, только отдельным object_key под `avatars/` (не считается в квоту, лимит 2 МБ): 1. **`POST /me/avatar/token`** → `{ "upload_url": "...", "object_key": "avatars/...", "node_id": 1 }` 2. `PUT` байты изображения напрямую на `upload_url` (как и при обычной загрузке файла). 3. **`POST /me/avatar/confirm`** `{ "object_key": "...", "node_id": 1 }` → `{ "avatar_url": "..." }`. Старый аватар (если был) удаляется с ноды автоматически. --- ## 4. Загрузка файла Загрузка идёт напрямую в объектное хранилище по presigned S3 URL — байты файла никогда не проходят через сервер upload.am целиком, только заголовки запроса. Ровно тот же протокол, что использует сайт. ### 4.1 Маленькие файлы (до 8 МБ) — простой PUT **Шаг 1.** Получить presigned URL: ```bash curl -X POST https://upload.am/api/v1/upload/token \ -H "Authorization: Bearer ua_live_xxx" \ -H "Content-Type: application/json" \ -d '{"file_name": "report.pdf", "file_size": 204800}' ``` ```json { "upload_url": "https://files.upload.am/...(presigned, действует 30 минут)", "object_key": "b6f1...e9", "public_uuid": "b6f1...e9", "node_id": 1, "max_file_bytes": 107374182400 } ``` **Шаг 2.** Загрузить сами байты напрямую в `upload_url`: ```bash curl -X PUT "$upload_url" --data-binary @report.pdf ``` **Шаг 3.** Подтвердить загрузку — только после этого файл появляется в аккаунте: ```bash curl -X POST https://upload.am/api/v1/upload/confirm \ -H "Authorization: Bearer ua_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "object_key": "b6f1...e9", "public_uuid": "b6f1...e9", "node_id": 1, "original_name": "report.pdf", "size_bytes": 204800, "password": null, "max_downloads": null, "link_ttl": null, "folder_uuid": null }' ``` ```json { "data": { "uuid": "b6f1...e9", "name": "report.pdf", "extension": "pdf", "size_bytes": 204800, "folder_uuid": null, "batch_uuid": null, "password_protected": false, "max_downloads": null, "download_count": 0, "sha256": null, "expires_at": null, "created_at": "2026-09-05T12:00:00+00:00", "download_page_url": "https://upload.am/d/b6f1...e9" } } ``` Если в той же папке уже есть файл с тем же `original_name`, сработает версионирование кабинета: `data.uuid` может отличаться от `public_uuid`, который вернул `/upload/token`. Смотрите uuid в ответе `confirm`, не из шага 1. Опции `confirm` (все опциональны, кроме первых пяти): | Поле | Тип | Описание | |---|---|---| | `password` | string | Пароль на файл — как в дропзоне на сайте | | `max_downloads` | int, 1–1000 | Лимит публичных скачиваний с `/d/`; файл физически удаляется после исчерпания | | `link_ttl` | `10m`\|`1h`\|`24h`\|`7d` | Ссылка перестаёт работать раньше срока тарифа, если это раньше | | `folder_uuid` | string | Положить файл в существующую папку. Чужой или несуществующий uuid → `404` `folder_not_found`, файл в корень не кладётся | | `sha256` | string (64 hex) | Контрольная сумма, посчитанная на вашей стороне до загрузки — сверяется на скачивании | ### 4.2 Крупные файлы — S3 Multipart Тот же принцип, что у сайта: файл режется на части по 8+ МБ, каждая часть грузится отдельным presigned PUT. ```bash # 1. Начать multipart-сессию curl -X POST https://upload.am/api/v1/upload/multipart/create \ -H "Authorization: Bearer ua_live_xxx" -H "Content-Type: application/json" \ -d '{"file_name": "backup.tar.gz", "file_size": 5368709120}' # → { "upload_id": "...", "object_key": "...", "public_uuid": "...", "node_id": 1 } # 2. На каждую часть — подпись curl -X POST https://upload.am/api/v1/upload/multipart/sign-part \ -H "Authorization: Bearer ua_live_xxx" -H "Content-Type: application/json" \ -d '{"node_id": 1, "object_key": "...", "upload_id": "...", "part_number": 1}' # → { "url": "presigned PUT для этой части" } # ... PUT каждой части байтов на свой url, сохранить ETag из ответа PUT ... # 3. Завершить curl -X POST https://upload.am/api/v1/upload/multipart/complete \ -H "Authorization: Bearer ua_live_xxx" -H "Content-Type: application/json" \ -d '{"node_id": 1, "object_key": "...", "upload_id": "...", "parts": [{"PartNumber": 1, "ETag": "\"...\""}, {"PartNumber": 2, "ETag": "\"...\""}]}' # 4. confirm — как в разделе 4.1, теми же полями ``` Готовые SDK под S3 multipart есть почти для всех языков (`aws-sdk`, `boto3` и т.д.) — формат частей и `ETag` совместим с обычным S3 API, ничего специфичного для upload.am на этом шаге нет. **Резюмирование при обрыве связи.** `upload_id` и список уже принятых частей (номер + `ETag`) можно сохранить у себя и переиспользовать — `sign-part` для уже принятого номера части безопасно перевызывать повторно, а `complete` принимает любой полный набор частей независимо от того, за сколько сессий они были загружены. Так же и на скачивании: presigned-ссылка из `POST /files/{uuid}/download` — обычный прямой URL на S3-совместимую ноду, он поддерживает стандартный HTTP `Range: bytes={offset}-` без какой-либо специальной поддержки с нашей стороны — допишите уже полученные байты файла и продолжайте с этого offset. Именно так это реализовано в официальном десктоп-клиенте (`unixanet/backup`, `UploadResumeStore`/`DownloadToFileAsync`). ### 4.3 `POST /remote-upload` — заливка по ссылке (без скачивания к себе) Добавлено 22.09.2026 (десктоп-клиент, `unixanet/backup`, п.14 владельца — "remote download отдельным меню"). Зеркалит веб-версию (`dashboard.remote-upload`) 1:1 — тот же `App\Jobs\FetchRemoteUpload` и `App\Support\SsrfGuard`, включая общий `RateLimiter` key (`remote-upload:{user_id}`) — лимит 10/мин делится между сайтом и клиентом, не удваивается при использовании обоих. ```bash curl -X POST https://upload.am/api/v1/remote-upload \ -H "Authorization: Bearer ua_live_xxx" -H "Content-Type: application/json" \ -d '{"url": "https://example.com/report.pdf", "folder_uuid": null}' ``` Синхронно (до ответа) проверяется, что адрес не приватный/внутренний (`SsrfGuard::resolveAndAssertSafe`) — 422 `{ "error": "unsafe_url" }`, если нет. Иначе `{ "status": "queued", ... }` немедленно — сама загрузка асинхронна (джоб), письмо на почту при провале, файл появляется в `GET /files` по готовности. Нет отдельного эндпоинта "проверить прогресс" — как и на сайте, узнать про готовность можно только увидев файл в списке (или письмо о провале). --- ## 5. Файлы ### `GET /files` Список файлов личного пространства аккаунта. Параметры: `folder_uuid` (список файлов этой папки), `q` (поиск по имени), `per_page` (по умолчанию 25, максимум 100). Без `folder_uuid` и без `q` — только корень. `q` без `folder_uuid` ищет по всем файлам аккаунта; вместе с `folder_uuid` — только внутри этой папки. `all=1` — вообще все файлы аккаунта плоским списком, без папок/поиска (для клиентских задач вроде поиска дублей по `sha256`); `per_page` для этого режима до 200. У каждого файла есть `batch_uuid` — `null`, либо uuid уже существующей групповой ссылки `/b/…`. ```json { "data": [ { "uuid": "...", "name": "report.pdf", "...": "..." } ], "meta": { "current_page": 1, "last_page": 3, "total": 67 } } ``` ### `GET /files/{uuid}` Детали одного файла — тот же объект, что и в `data[]` выше. #### Поля объекта `file` (актуально на 12.10.2026) `uuid`, `name`, `extension`, `size_bytes`, `folder_uuid`, `batch_uuid`, `password_protected`, `max_downloads`, `download_count`, `sha256`, `has_thumbnail`, `expires_at` (ISO 8601 или `null`; у Free файл владельца без личного срока), `created_at`, `download_page_url`, а также новые: | Поле | Значение | |---|---| | `label` | `null`, `internal`, `confidential` или `restricted` — метка конфиденциальности (`POST /files/{uuid}/label`) | | `sensitive` | `true`, если в файле найдены номера карт, IBAN, ключи и т. п. | | `tags` | массив тегов из распознавания и данных камеры (`[]`, если нет) | | `has_text` | `true`, если распознан текст — такие файлы находит `GET /files?q=` | | `retain_until` | ISO 8601 или `null` — файл заблокирован до даты (удаление, переименование, перезапись дают `423 retention_locked`) | | `legal_hold` | `{uuid, name}` или `null` — файл под юридическим удержанием (`DELETE …/force` даёт `423 protected`) | ### `PATCH /files/{uuid}` Изменить настройки файла. Все поля опциональны: ```json { "name": "новое-имя.pdf", "password": "новый-пароль", "clear_password": false, "max_downloads": 5, "clear_max_downloads": false } ``` ### `POST /files/{uuid}/move` ```json { "folder_uuid": "или null для корня" } ``` ### `DELETE /files/{uuid}` Перемещает файл в корзину (как кнопка Delete в кабинете) — восстановим `POST /files/{uuid}/restore`, пока не почищена корзина по расписанию. Ответ — `204 No Content`, тело пустое. ### `DELETE /files/{uuid}/force` Работает только для файла, уже находящегося в корзине. Удаляет файл окончательно: байты на S3 освобождаются, квота уменьшается немедленно. **Необратимо.** Ответ — `204 No Content`, тело пустое. Файлы, удалённые в корзину меньше `trash_lock_days` (по умолчанию 3 дня) назад, окончательно удалить нельзя — защита на случай компрометации аккаунта: если кто-то получил доступ и пытается удалить всё безвозвратно, это окно даёт время заметить и восстановить. Ответ — `422`: ```json { "error": "trash_locked", "message": "This file was deleted too recently to be permanently erased — try again in a few days." } ``` ### `POST /files/{uuid}/download` Выдаёт короткоживущую (10 минут) прямую ссылку на скачивание файла. Пароль файла здесь **не спрашивается** — он защищает публичную страницу `/d/` от посторонних, у кого есть только ссылка; вы уже подтвердили, что это ваш файл, самим API-токеном. На Free-тарифе действует тот же анти-бот таймер 10 секунд, что и на сайте — только без cookie-сессии, вызовите эндпоинт дважды: ```bash curl -X POST https://upload.am/api/v1/files/b6f1.../download -H "Authorization: Bearer ua_live_xxx" # → 202 { "status": "waiting", "wait_seconds": 10 } sleep 10 curl -X POST https://upload.am/api/v1/files/b6f1.../download -H "Authorization: Bearer ua_live_xxx" # → 200 { "status": "ready", "download_url": "https://files.upload.am/...", "expires_in": 600 } ``` На платных тарифах первый же вызов сразу возвращает `status: "ready"`. Этот эндпоинт **не увеличивает** `download_count` и **не тратит** `max_downloads`. Лимит считается только на публичной странице `/d/` (и на `/s/`, `/b/`), когда файл качает кто-то со ссылкой, не владелец со своим API-токеном. Он всё же **проверяет** текущее значение `downloadsExhausted()` — если лимит файла уже исчерпан скачиваниями по публичной ссылке, `/download` вернёт `410` и владельцу тоже. Именно поэтому для простого просмотра (не сохранения к себе) нужен `/preview` ниже, а не этот эндпоинт. ### `GET /files/{uuid}/preview` Добавлено 14.09.2026. Отдельно от `/download` — для показа файла ВНУТРИ клиента (тап по фото/видео в галерее приложения), без анти-бот таймера и **без проверки** `downloadsExhausted()`: лимит скачиваний ограничивает раздачу другим людям через публичную ссылку, не собственный просмотр владельцем своего же файла. Поддерживаемые типы (как и на сайте): изображения, видео, аудио, PDF, txt. Для остальных типов — `422`. ```bash curl https://upload.am/api/v1/files/b6f1.../preview -H "Authorization: Bearer ua_live_xxx" # → 200 { "url": "https://files.upload.am/...", "type": "image", "expires_in": 1800 } ``` Ссылка живёт 30 минут, отдаётся с `Content-Disposition: inline` (открывается в браузере/плеере, а не скачивается). `410`, если файл истёк; `422`, если тип файла не из поддерживаемых для предпросмотра. ### `GET /files/{uuid}/thumbnail` Добавлено 16.09.2026, для Photos/Video-браузера клиентов. Тумбнейл генерируется **асинхронно** джобом `App\Jobs\GenerateThumbnail`, который диспатчится при подтверждении загрузки (обычной и remote/URL) — не мгновенно, не синхронно с самим апломдом. Картинки — через GD. Видео — через `ffmpeg`, если бинарник есть на сервере (проверяется в рантайме); если ffmpeg нет — видео-тумбнейлы просто не генерируются, ошибки нет. ```bash curl https://upload.am/api/v1/files/b6f1.../thumbnail -H "Authorization: Bearer ua_live_xxx" # → 200 { "url": "https://files.upload.am/...", "expires_in": 1800 } # → 404, если тумбнейл ещё не готов или файл не изображение/видео ``` Проверяйте `has_thumbnail` в объекте `file` (см. `GET /files`, `GET /changes`) ПЕРЕД вызовом этого эндпоинта — так клиент не долбит `/thumbnail` на файлы, которые никогда не получат тумбнейл (документы, архивы), и не спамит запросами файлы, джоб которых ещё в очереди. `404` для таких случаев вернётся всё равно, но неотличимо одно от другого — экономьте round-trip там, где можно. ### `POST /files/{uuid}/restore` Восстанавливает файл из корзины (обратное `DELETE /files/{uuid}`). 404, если файл не в корзине. ### `GET /files?trashed=1` Список файлов в корзине (только они — не смешивается с обычным списком). Сортировка по времени удаления, новые сверху. Те же `folder_uuid`/`q` тут не применяются — как и в кабинете, корзина плоская. ### Версии файла Каждая повторная заливка файла с тем же именем в ту же папку создаёт новую версию, не перезаписывает файл молча — то же самое поведение, что и в кабинете на сайте. - **`GET /files/{uuid}/versions`** — список версий, новые сверху. - **`POST /files/{uuid}/versions/{version_uuid}/restore`** — откатить файл на эту версию. Текущее содержимое перед откатом само становится новой записью в истории — не теряется. `public_uuid`/ссылки на файл не меняются. - **`POST /files/{uuid}/versions/{version_uuid}/download`** — короткоживущая ссылка на конкретную старую версию, без отката текущего файла. ### Upload Inbox — файлы, ожидающие решения Файлы, залитые кем-то через ваш `/r/{token}` (см. раздел 11), не появляются в обычном списке файлов, пока вы их не примете — `GET /files` их не отдаёт. - **`GET /files?inbox_pending=1`** — список ожидающих решения. - **`POST /files/{uuid}/accept`** — принять: файл становится обычным, попадает в общий список. - **`POST /files/{uuid}/decline`** — отклонить: файл удаляется без возможности восстановить (не в корзину, как и на сайте). --- ## 6. Папки | Метод | Путь | Описание | |---|---|---| | `GET` | `/folders?parent_uuid=` | Список папок (без `parent_uuid` — корень) | | `POST` | `/folders` | `{"name": "Отчёты", "parent_uuid": null}` | | `POST` | `/folders/ensure` | `{"path": "Contracts/2024", "parent_uuid": null}` — создать цепочку или вернуть существующую | | `PATCH` | `/folders/{uuid}` | `{"name": "Новое имя"}` | | `DELETE` | `/folders/{uuid}` | Рекурсивно (папка + всё дерево подпапок); все файлы из дерева переносятся на уровень выше удаляемой папки | `/folders/ensure` нужен клиенту Upload.am: бэкап локальной папки без выбранного приёмника создаёт в корне аккаунта папку с тем же именем. Подробности — `docs/WINDOWS_CLIENT.md`. --- ## 7. Публичные ссылки (Share) Ссылка на один или несколько файлов — с паролем, сроком, лимитом скачиваний, удалением после первого скачивания (`burn_after_download`) или email-гейтом. ### `POST /shares` ```json { "file_uuids": ["b6f1...e9", "a2c3...11"], "ttl": "24h", "max_downloads": 10, "password": "секрет", "burn_after_download": false, "title": "Материалы к встрече", "allowed_email": "client@example.com" } ``` `ttl` — один из `10m`, `1h`, `24h`, `7d`, либо не передавать поле вообще для "пока не истечёт тариф". Ответ содержит `url` — прямую публичную ссылку `/s/...`. ### `GET /shares`, `GET /shares/{uuid}`, `DELETE /shares/{uuid}` (отзыв) Стандартный CRUD, формат объекта — как в примере `POST` выше. ### `POST /shares/{uuid}/files` — добавить файлы в существующую ссылку Добавлено 16.09.2026. Не пересоздаёт ссылку — тот же `uuid`, тот же пароль/срок, просто больше файлов внутри. Это то, что имеется в виду на `/features` под "a link that stays current as you add files". Файлы должны принадлежать вашему личному пространству (`team_id = null`), как и при `POST /shares`. ```json { "file_uuids": ["c7d2...44"] } ``` Возвращает обновлённый объект `share` (формат — как в примере `POST /shares` выше). **Важно, не путать с шарингом папки как объекта:** такая фича (публичная ссылка, открывающая целиком папку) существовала в кабинете и была осознанно удалена 04.09.2026 — у папки в принципе не может быть отдельного адреса, который открывается как страница. Этот эндпоинт даёт тот же практический результат (одна стабильная ссылка, актуальная по мере добавления файлов), не воскрешая удалённую концепцию. --- ## 8. Групповые ссылки (Batch) Быстрый способ получить одну ссылку на несколько только что загруженных файлов без настройки пароля/срока — `/b/{uuid}`, публичная страница выглядит как обычная страница скачивания для каждого файла внутри. ```bash curl -X POST https://upload.am/api/v1/batches \ -H "Authorization: Bearer ua_live_xxx" -H "Content-Type: application/json" \ -d '{"uuids": ["b6f1...e9", "a2c3...11"]}' ``` Файлы должны принадлежать вашему аккаунту и ещё не входить в другую группу (`batch_uuid` у каждого должен быть `null`). Иначе `422 files_not_found`. `DELETE /batches/{uuid}` в v1 нет — снимите файлы через `DELETE /files/{uuid}`. --- ## 9. Upload Inbox — настройки Личная ссылка `/r/{token}`, по которой кто угодно без аккаунта может закинуть вам файлы (то же самое, что переключатель в `/cabinet/drop` на сайте) — теперь доступно и по Bearer-токену, не только через сессию в браузере. ### `GET /inbox` ```json { "data": { "enabled": true, "url": "https://upload.am/r/AbCdEf...", "has_password": false, "rules": { "ext": null, "max_file_bytes": 104857600, "max_files": 20, "ttl_hours": 168, "require_name": false, "require_email": false } } } ``` ### `PATCH /inbox` Все поля необязательны — присылайте только то, что меняете. Поля правил (`ext`/`max_file_mb`/`max_files`/`ttl_hours`/`require_name`/`require_email`) заданы частично — прислали одно, остальные сохраняют прежнее значение, не обнуляются. ```json { "enabled": true, "max_file_mb": 200, "max_files": 30, "require_email": true } ``` `password` / `clear_password` — так же, как у файлов и ссылок: `password` ставит пароль на страницу инбокса, `clear_password: true` снимает. ### `POST /inbox/regenerate-token` Старая ссылка `/r/{старый_token}` сразу перестаёт работать. Ответ — тот же объект, что и `GET /inbox`, с новым `url`. --- ## 9.1 `GET /changes` — инкрементальный фид для синка Добавлено 16.09.2026, для клиентов с постоянным двусторонним синком (Windows/Android Sync-движок, не Backup). Без него клиент был бы вынужден на каждый цикл гонять полный `GET /files` листинг и сверять локально — не масштабируется, не даёт быстрого синка при большом количестве файлов. ```bash curl "https://upload.am/api/v1/changes?cursor=&per_page=200" \ -H "Authorization: Bearer ua_live_xxx" ``` Первый запрос — без `cursor` (или `cursor` пустой): вернёт всё личное пространство с начала времён. Дальше — всегда подставлять `meta.cursor` из предыдущего ответа. `cursor` непрозрачный, не парсите его на клиенте — только сохраняйте и возвращайте. ```json { "data": { "files": [ { "uuid": "b6f1...e9", "name": "report.pdf", "extension": "pdf", "size_bytes": 20480, "sha256": "3a7f...", "folder_uuid": "c1a2...", "updated_at": "2026-09-16T10:00:00+00:00", "deleted": false } ], "folders": [ { "uuid": "c1a2...", "name": "Reports", "parent_uuid": null, "updated_at": "2026-09-16T09:50:00+00:00" } ] }, "meta": { "cursor": "MjAyNi0wOS0xNlQxMDowMDowMCswMDowMHwxMjM=", "has_more": false } } ``` `files[].deleted: true` — файл удалён (в корзине или удалён насовсем); уберите его локально. `per_page` (по умолчанию 200, максимум 500) — размер страницы на КАЖДУЮ из двух коллекций (files/folders) отдельно; `has_more: true` значит, что нужно сразу повторить запрос с новым `cursor`, не дожидаясь следующего цикла синка. **Известное ограничение — удаление папок не отражается здесь.** Удаление папки на сервере — жёсткое (`cascadeOnDelete` в схеме, без soft-delete), у папок нет `deleted_at`. Удалённая папка просто перестанет попадаться в выдаче `folders`, но явного сигнала "удали её локально" клиент не получит через `/changes`. Если это станет проблемой на практике — потребуется отдельное решение (перевести `folders` на SoftDeletes), это меняет и поведение каскадного удаления, не тихая правка. --- ## 9.2 Защита данных (11.10.2026) Контекст: личный аккаунт или команда (заголовок `X-Upload-Team: `; менять командное может только админ команды). | Метод | Адрес | Что делает | |---|---|---| | GET | `/protection` | Сроки хранения (`trash_days`, `version_days`, свои значения, `min_days`, `max_days`), `default_retain_days` и `block_links_sensitive` (команда), число заблокированных файлов и активных удержаний | | PATCH | `/protection/retention` | `trash_days`, `version_days` (7–3650 или null), для команды ещё `default_retain_days`, `block_links_sensitive` | | POST | `/files/{uuid}/lock` | `{until}` дата в будущем; блокировка только продлевается (`422 cannot_shorten`) | | POST | `/folders/{uuid}/lock` | то же для всех файлов папки и вложенных | | GET/POST | `/holds` | Список и создание юридического удержания: `name`, `reason`, `scope` (`team|folder|file|user`), `folder_uuid`, `file_uuid`, `custodian_user_id` (нужна команда: `422 team_required`) | | POST | `/holds/{uuid}/release` | Снять удержание | | GET | `/backups` | Глубина копии (`depth_days`), снимки по датам, запросы на восстановление | | POST | `/backups/restore-requests` | `{target_date, path_prefix?, mode: missing|full, note?}`; выполняет поддержка | | GET | `/alerts` | Оповещения о подозрении на шифровальщик | | POST | `/alerts/{id}/dismiss`, `/alerts/{id}/restore` | Закрыть или откатить все изменения с начала окна | | GET | `/sensitive` | Файлы с меткой или находками сканера | | GET | `/teams` | Ваши команды: `uuid`, `name`, `role`, `guest`, `seats_used`, `seats_limit`, `guests` | | GET | `/files/{uuid}/blocks?block_size=` | Хеши SHA-256 блоков текущей версии (`block_size` 8–64 МиБ, по умолчанию 8); считаются один раз на версию | | POST | `/files/{uuid}/delta` | `{size, block_size, changed: [индексы с нуля], sha256?}` → `{delta_id, copied_blocks, upload: [{part_number, size, url}]}`; неизменные блоки копируются на стороне хранилища; 422 `block_not_reusable`, 423 `retention_locked`, 501 `storage_unsupported` | | POST | `/files/{uuid}/delta/{delta_id}/complete` | `{parts: [{part_number, etag}]}` для изменённых блоков; возвращает файл с новой версией; 409 `base_changed`, 422 `missing_part` / `size_mismatch` | | GET | `/team/members` | Участники и действующие приглашения команды (заголовок `X-Upload-Team`) | | POST | `/team/invites` | `{email, role?, guest?}`; роли admin, editor, contributor, viewer, member; `guest:true` — место не занимает. Только админ | | DELETE | `/team/invites/{id}` | Отозвать приглашение | | PATCH | `/team/members/{id}` | `{role?, status?: active|frozen}` | | DELETE | `/team/members/{id}` | Удалить участника; его файлы переходят владельцу команды (`files_reassigned`) | | POST | `/files/{uuid}/label` | `{label: internal|confidential|restricted|null}` | В объекте файла: `retain_until`, `legal_hold` (`{uuid,name}` или null), `label`, `sensitive`. Ошибки: `423 retention_locked` (удаление, переименование, смена версии заблокированного файла), `423 protected` (окончательное удаление при удержании или блокировке). ## 9.3 Устройство и удалённая очистка (10.10.2026) Пользователь может пометить потерянное устройство в кабинете (Account → Devices). Приложение при запуске и раз в несколько минут вызывает: - `GET /device` → `{ "wipe": true|false, "requested_at": "ISO 8601|null" }` (заголовок токена приложения). При `wipe: true` клиент стирает локальные копии файлов, токен и ключи. - `POST /device/wiped` — подтвердить, что очистка выполнена (`409 no_wipe_requested`, если её не запрашивали). Пока очистка не подтверждена, остальные запросы с этим токеном получают `403 device_wipe_pending`. ## 9.4 Администрирование команды, устройства, токены, счета (12.10.2026) Все маршруты — Bearer-токен, `Content-Type: application/json`. Ошибки: `{"error":"код","message":"..."}`; валидация `422` — стандартный формат Laravel; чужой ресурс — `404`. Файл маршрутов: `routes/api/admin.php`. **Команда**: заголовок `X-Upload-Team: ` обязателен для всех `/team/*` (кроме `POST /team/trial`) и для командного бренда. Нет заголовка — `422`; вы не участник команды — `403`. Если не сказано иное, менять может только владелец или админ команды (иначе `403 {"error":"forbidden"}` или `403`). Люди в групповых и «правовых» методах задаются `user_id` (поле `user_id` в `GET /team/members`). ### 1. Настройки и политики команды | Метод | Адрес | Роль | Что делает | |---|---|---|---| | GET | `/team` | любой участник | Команда: `uuid`, `name`, `role`, `can_manage`, `is_owner`, `owner`, `plan_code`, `plan_expires_at`, `trial_ends_at`, `storage_limit_bytes`, `storage_used_bytes` (общий пул), `seats_used`/`seats_limit`, `guests`/`guests_limit`, `spaces_limit`, `e2ee_enabled`, `inbox_enabled`, `policies` | | PATCH | `/team` | только владелец | `{name}` (1–80 символов) | | GET | `/team/policies` | любой участник | Политики (см. ниже) | | PATCH | `/team/policies` | админ | Частичное обновление: меняются только присланные поля | Поля политик (`GET`/`PATCH /team/policies`): | Поле | Тип | Смысл | |---|---|---| | `auto_delete_days` | int 7–3650 или `null` | автоудаление файлов через N дней | | `allowed_domains` | массив доменов (при записи — массив или строка через запятую) | приглашать только с этих доменов (пусто = любые) | | `link_domains` | то же | адресные ссылки только на эти домены получателей | | `links_disabled` | bool | запретить публичные ссылки | | `link_require_password` | bool | пароль на ссылках обязателен | | `link_max_days` | int 1–3650 или `null` | максимальный срок ссылки | | `link_require_login` | bool | получатель должен войти | | `link_no_direct` | bool | запретить прямые ссылки на файл | | `guests_allowed` | bool | разрешены внешние гости | | `stamp_downloads`, `stamp_text` | bool, string ≤200 | штамп на скачиваемых PDF/картинках | | `block_links_sensitive` | bool | не создавать ссылки на конфиденциальные файлы | | `trash_days`, `version_days` | int 7–максимум или `null` | срок корзины и версий (в `GET` — действующие значения, `own_trash_days`/`own_version_days` — заданные у команды) | | `default_retain_days` | int 1–3650 или `null` | срок блокировки новых файлов по умолчанию | Некорректный домен — `422` на поле `allowed_domains` / `link_domains`. Изменение пишется в журнал аудита (`policies_updated`). ```json PATCH /api/v1/team/policies {"links_disabled": true, "allowed_domains": ["acme.com"]} { "data": { "links_disabled": true, "allowed_domains": ["acme.com"], "link_max_days": null, "trash_days": 10, "...": "..." } } ``` ### 2. Журнал аудита и статистика (админ) | Метод | Адрес | Что делает | |---|---|---| | GET | `/team/audit` | Фильтры: `action` (из списка `actions` в ответе, иначе `422`), `actor` (user_id), `from`, `to` (даты), `per_page` (по умолчанию 50, максимум 200), `page`. Ответ: `data[]` = `{id, action, created_at, ip, actor:{user_id,name,email}\|null, file:{uuid,name}\|null, meta}`, `meta` = `{current_page,last_page,total,per_page}`, `actions` | | GET | `/team/audit/export` | CSV потоком (`text/csv`), те же фильтры; параметр `format` принимает только `csv`. Лимит 10 запросов в минуту | | GET | `/team/usage?days=7\|30\|90\|365` | Использование по участникам из журнала: `data.members[]` = `{user_id,email,name,uploads,uploaded_bytes,downloads,shares,deleted,last_activity_at}`, `external_downloads`; другое значение `days` = 30 | ### 3. Группы, права на папки, передача файлов (админ) | Метод | Адрес | Что делает | |---|---|---| | GET | `/team/groups` | `data[]` = `{id,name,members:[{user_id,name,email}]}` | | POST | `/team/groups` | `{name, members?: [user_id]}` → `201`; `422 group_exists` при повторе имени. В группу попадают только действующие участники | | PATCH | `/team/groups/{id}` | `{name?, members?}`; если `members` не прислан — состав не меняется | | DELETE | `/team/groups/{id}` | `{data:{deleted:true}}` | | GET | `/team/folders/{folderUuid}/grants` | `{folder_uuid, grants:[{user_id,name,email,permission}], group_grants:[{group_id,name,permission}]}`. Папка другой команды — `404`; нет права управлять папкой — `403` | | PUT | `/team/folders/{folderUuid}/grants` | `{grants: {"": "view\|upload\|manage"}}`: **полная замена** прав участников с ролью `member` (кого нет в объекте — права снимаются; `null` или пусто — снять) | | PUT | `/team/folders/{folderUuid}/group-grants` | `{groups: {"": "view\|upload\|manage"}}`: полная замена прав групп | | POST | `/team/members/{id}/transfer` | `{to_user_id}` — передать все файлы участника (`id` — id участника из `GET /team/members`) другому действующему участнику → `{data:{files_reassigned}}`; `422` если получатель не участник | Для не-админов (даже участников) методы группы и прав дают `403`. ### 4. Общий Inbox команды, шифрование, SSO | Метод | Адрес | Роль | Что делает | |---|---|---|---| | GET | `/team/inbox` | админ | `{enabled, url, folder_uuid, has_password, rules, brand_name, brand_color}`; `url` вида `/tr/<токен>` | | POST | `/team/inbox/toggle` | админ | Все поля необязательны, меняются только присланные: `enabled`, `folder_uuid` (папка команды, `null` — корень; `422 folder_not_found`), `ext`, `max_file_mb`, `max_files`, `ttl_hours`, `require_name`, `require_email`, `password` (3–100) / `clear_password`, `brand_name`, `brand_color` (`#RRGGBB`) | | POST | `/team/inbox/regenerate` | админ | Новая ссылка, старая перестаёт работать | | GET | `/team/encryption` | любой участник | `{enabled, e2ee_verifier}` — проверочный блок `{v,it,salt,iv,ct}` для сверки пароля на клиенте; сам пароль сервер никогда не получает | | PUT | `/team/encryption` | админ | `{e2ee_verifier: {v:1, it:100000..5000000, salt, iv, ct}}` (объект или JSON-строка) либо `{clear:true}`; неверный блок — `422`. Смена пароля не перешифровывает ранее загруженные файлы | | GET | `/team/sso` | админ | Только статус: `{available, configured, enabled, active, provider, issuer, domain, domain_verified, domain_verified_at, dns_txt:{name,value}\|null, manage_in_web:true}`. Секретов нет | **SSO настраивается только в веб-кабинете** (Team → SSO: сервер OIDC, client_id/secret, подтверждение домена TXT-записью). В API есть только чтение статуса: управление намеренно не вынесено (секреты клиента и проверка домена требуют веб-интерфейса). ### 5. Пробный период команды `POST /team/trial` (без `X-Upload-Team`, команда создаётся) — как `TrialController::start`: 14 дней (настройка `trial_days`) тарифа `trial_plan_code`, один раз на человека. `201 {data:{team_uuid,name,plan_code,days,trial_ends_at}}`. Ошибки: `422 trial_used`, `422 trial_has_team` (уже в команде или владелец), `404 trial_unavailable`. Лимит 5 запросов за 10 минут. ### 6. Устройства и вход | Метод | Адрес | Что делает | |---|---|---| | GET | `/devices` | `data[]` = `{id,label,kind:web\|app,country,current,first_seen_at,last_seen_at,wipe_pending,wipe_requested_at,wiped_at}`; `current` = устройство этого токена | | POST | `/devices/{id}/sign-out` | Закрыть вход на устройстве (для `web` — сессия, для `app` — токены с именем устройства). Текущее устройство: `422 cannot_sign_out_current` (используйте `POST /auth/logout`) | | POST | `/devices/{id}/wipe` | Запросить удалённую очистку устройства-приложения (дальше клиент на нём увидит `wipe:true` в `GET /device`, см. 9.3). `422 wipe_no_session` — на устройстве нет действующего входа; `422 cannot_wipe_current`; не приложение или чужое — `404` | | DELETE | `/devices/{id}/wipe` | Отменить запрос, пока клиент не подтвердил очистку (идемпотентно) | | POST | `/me/two-factor/recovery-codes` | Новые 8 резервных кодов (старые перестают работать), `{data:{two_factor_recovery_codes:[...]}}` — показываются один раз. Пароль не нужен (как в вебе). `422 two_factor_disabled`, если 2FA выключена | | POST | `/me/export` | `{password}` → файл JSON потоком (`Content-Disposition: attachment`) — выгрузка данных аккаунта (GDPR). `422 wrong_password`. Лимит 3 в минуту | | DELETE | `/me` | `{password, confirm:"DELETE"}` — удаление аккаунта без возврата, файлы стираются сразу → `{data:{deleted:true}}`. `422 wrong_password`; `409 delete_blocked` с `reason` = `admin\|team\|protected` (владелец команды, администратор, файлы под блокировкой); `500 delete_failed` — повторите (операция идемпотентна) | ### 7. API-токены | Метод | Адрес | Что делает | |---|---|---| | GET | `/tokens` | `data[]` = `{id,name,last_four,created_at,last_used_at,expires_at,current}` (значений нет), `limit` = `{used,max}` (`max: null` — без ограничения) | | POST | `/tokens` | `{name (≤80), password}` — нужен пароль аккаунта (как в вебе: украденный токен не должен выпускать новые; неверный — `403 wrong_password`) → `201 {data:{id,name,...,token}}`; `token` (`ua_live_…`) показывается **один раз**. Лимит тарифа: `403 plan_limit_reached` (`limit:"api_tokens"`, `max`) | | DELETE | `/tokens/{id}` | Отозвать токен; чужой — `404`. Можно отозвать и текущий токен | Внимание: токен с правами полного доступа может выпустить новые токены — храните его как пароль. ### 8. Счета, договор с SLA, «Мой тариф» | Метод | Адрес | Что делает | |---|---|---| | GET | `/invoices` | `{enabled, min_usd_cents, data:[{number,status,amount_usd_cents,billing_period,plan:{code,name},company,email,issued_at,due_on,paid_at,pdf_url,agreement_url}]}`. `enabled:false` — продавец не настроен | | POST | `/invoices` | `{plan_code, period?: month\|year (по умолчанию year), company, address, email, vat_id?, country? (2 буквы), notes?}` → `201 {data: счёт}`; письма (клиенту и админу) уходят как в вебе. Ошибки: `404 invoices_disabled`, `404 invalid_plan`, `422 amount_too_small` (`min_usd_cents`), `422 too_many_open_invoices` (`max`: 3 неоплаченных) | | GET | `/invoices/{number}/pdf` | PDF счёта (`application/pdf`); чужой номер — `404` | | GET | `/invoices/{number}/agreement` | PDF договора с SLA | | GET | `/me/plan` | `{data:{plan, source (free\|own\|family), expires_at, entitlements, usage, features, enforced}}`. `plan` и `entitlements` — как в `GET /me`; `usage` — объект по ключам лимитов (`storage, file, rooms, portals, api_tokens, active_links, shared_folders, history_days, …`) = `{used, max (null = без ограничения), unlimited, percent}`; `features[]` = `{key, allowed, min_plan (название), min_plan_code}` — самый дешёвый тариф, где функция доступна | Оплата картой и криптой — `POST /billing/{planCode}/checkout` (см. раздел 3). Подтверждает оплату по счёту администратор сервиса вручную. ### 9. VPN и бренд | Метод | Адрес | Что делает | |---|---|---| | GET | `/vpn/profile` | Профиль OpenVPN (`application/x-openvpn-profile`, файл `upload-am-vpn.ovpn`); `404 vpn_profile_missing`, если файл не загружен. Скачивание считается как на сайте (`download`) | | GET | `/brand` | `{scope: personal\|team, brand_name, brand_color, logo_url, embed_domains}`; с `X-Upload-Team` — бренд команды (читает любой участник), `embed_domains` только у личного | | PATCH | `/brand` | `{brand_name? (≤80), brand_color? (#RRGGBB или null), embed_domains? (≤1000, только личный)}`; меняются только присланные; бренд команды — админ | | POST | `/brand/logo/token` | Как аватар: `{content_type: image/png\|jpeg\|webp\|gif}` → `{upload_url, object_key, node_id}`; клиент делает `PUT` файла (до 2 МБ) на `upload_url` | | POST | `/brand/logo/confirm` | `{object_key, node_id}` → `{logo_url}`; `413` если больше 2 МБ, `422` если файла нет в хранилище | | DELETE | `/brand/logo` | Убрать логотип → `{data: бренд}` | Логотип не создаёт файл и не занимает квоту. Логотипы комнат/порталов настраиваются в разделе комнат. ## 9.5 Комнаты, чек-лист клиента, дата-рум, выбор фото, шаблоны Все пути с префиксом `/api/v1`, заголовок `Authorization: Bearer <токен>`. Комната принадлежит пользователю (команда и заголовок `X-Upload-Team` для комнат не используются; исключение — `templates/apply-case` со `scope=team`). Чужая или несуществующая комната, файл, папка, пункт чек-листа, шаблон — `404 {"error":"not_found"}`. Ошибки валидации — стандартный 422 Laravel; прочие ошибки — `{"error":"code","message":"..."}`. Даты — ISO 8601, `null` = без ограничения. Функция тарифа закрыта — `403 {"error":"plan_feature_locked","feature":"data_room","message":"...","min_plan":"business_3"}`. ### Объект комнаты ```json { "uuid": "…", "type": "room|portal", "name": "Сдача проекта", "client_name": null, "company_name": null, "slug": null, "url": "https://upload.am/room/", "status": "active|destroyed", "expired": false, "password_protected": true, "welcome_message": null, "allow_visitor_upload": true, "allow_visitor_download": true, "require_visitor_name": true, "require_signature": false, "signature_file_uuid": null, "client_email": null, "remind_every_days": null, "nda_text": null, "qa_enabled": false, "selection_enabled": false, "selection_limit": null, "brand_name": null, "brand_color": null, "expires_at": "2026-10-15T12:00:00+00:00", "destroyed_at": null, "created_at": "…", "limits": { "max_files": 200, "files_count": 3, "plan_max_duration_days": 30, "requests_max": 100, "signature_available": true, "branding_available": true } } ``` `expires_at` у Drop Room (`type=room`) — дедлайн (автоуничтожение), у портала (`type=portal`) всегда `null` (бессрочный). `max_files` — `null` у портала (без ограничения); у Drop Room лимит фиксируется при создании по тарифу. `url` — публичная ссылка (у Drop Room — непредсказуемый uuid, у портала — slug, если задан). Файл комнаты — объект file из `docs/API.md` плюс `room_folder_uuid` (папка комнаты или `null`) и `visitor_access` (`open|stamped|view_only|hidden`). ### Комнаты #### GET /rooms Параметры: `type=room|portal`, `status=active|destroyed|all` (по умолчанию `all`). Ответ: ```json {"data": [<комната>], "limits": { "rooms": {"active": 1, "max_active": 25, "max_files": 200, "max_duration_days": 30}, "portals": {"active": 0, "max_active": 5}, "signature_available": true, "branding_available": true, "data_room_available": false, "templates_available": true}} ``` #### POST /rooms Создать комнату. Тело (JSON): - `type` — `room` | `portal` (обязателен, если не задан `template`); - `name` (обязательно, до 120), `client_name`, `company_name` (до 120), `welcome_message` (до 2000); - `slug` — только для портала (`alpha_dash`, до 60, уникален глобально); - `duration_days` — срок Drop Room в днях, ограничивается тарифом (`plan_max_duration_days`); для портала игнорируется; - `password` (3–100 символов), `allow_visitor_upload` (по умолчанию `true`), `allow_visitor_download` (`true`), `require_visitor_name` (`true`); - `template` — ключ шаблона комнаты (`b:kyc`, `u:12`, см. «Шаблоны»): подставляет приветствие, права посетителей и создаёт чек-лист; явно переданные поля важнее шаблона; - `nda_text`, `qa_enabled` — сразу создать дата-рум (нужна функция `data_room`). Ответ `201 {"data": <комната>}`. Ошибки: `403 portal_not_on_plan` (тариф без порталов), `422 limit_reached` (достигнут лимит активных комнат/порталов), `403 plan_feature_locked` (`data_room`), `422` (валидация; неизвестный `template` — ошибка поля `template`). Дата-рум — это комната с NDA и вопросами-ответами, отдельного типа нет. #### GET /rooms/{uuid} Настройки + `files`, `folders` (с `files_count`), `participants` (`id,name,email,nda_accepted_at,selection_submitted_at,created_at`), `requests` (чек-лист), `signatures` и `events` (последние 80 событий). Полная лента — `/activity`. #### PATCH /rooms/{uuid} Частичное обновление (передавайте только меняемые поля): `name`, `client_name`, `company_name`, `welcome_message`, `password` (задать), `clear_password` (`true` — снять), `allow_visitor_upload`, `allow_visitor_download`, `require_visitor_name`, `require_signature`, `signature_file_uuid` (файл внутри комнаты; `null` — снять; тариф с подписью), `extend_days` (только Drop Room; новый срок считается от сегодня и не больше лимита тарифа), `brand_name`, `brand_color` (`#RRGGBB`; тариф с брендированием). Ответ `200 {"data": <комната>}`. Ошибки: `403 plan_feature_locked` (`feature` = `room_signature` или `portal_branding`), `422`. #### DELETE /rooms/{uuid} Уничтожить комнату: файлы удаляются окончательно (файлы под блокировкой/удержанием остаются), история остаётся владельцу, публичная ссылка перестаёт работать. Ответ `200 {"data": <комната> + "files_deleted": 3, "files_protected": 0}`. #### GET /rooms/{uuid}/activity Лента событий (новые первыми). Параметры: `type` (например `uploaded`, `downloaded`, `viewed`, `signed`, `message`, `request_done`, `nda_accepted`, `selection_sent`), `per_page` (до 100, по умолчанию 30), `page`. ```json {"data": [{"id": 9, "type": "uploaded", "participant": {"id": 1, "name": "Ivan", "email": null}, "file_uuid": "…", "file_name": "a.pdf", "meta": {"name": "a.pdf"}, "created_at": "…"}], "meta": {"current_page": 1, "last_page": 3, "total": 61}} ``` `participant = null` — действие владельца. ### Файлы и папки комнаты - `POST /rooms/{uuid}/files` — прикрепить свои личные файлы (без команды, не лежащие в другой комнате): `{"file_uuids": ["…"], "folder_uuid": null}` (до 200; `folder_uuid` — папка комнаты, необязательно). Файлы не копируются. Ответ `201 {"data": {"attached": 2, "files": [<файл комнаты>]}}`. Ошибки: `404` (ни один файл не найден), `422 too_many_files` (не помещается в `max_files` — прикрепляется всё или ничего), `410 room_closed`. - `DELETE /rooms/{uuid}/files/{fileUuid}` — удалить файл комнаты окончательно. `204`. Файл под блокировкой/удержанием — `423 file_protected`. - `PATCH /rooms/{uuid}/files/{fileUuid}` — переместить в папку комнаты: `{"folder_uuid": "…"}` или `{"folder_uuid": null}` (в корень; ключ обязателен). Ответ `200 {"data": <файл комнаты>}`. - `POST /rooms/{uuid}/folders` — `{"name": "Документы"}` → `201 {"data": {"uuid","name","files_count","created_at"}}`. `410 room_closed` для мёртвой комнаты. - `POST /rooms/{uuid}/folders/attach` — перенести целую личную папку: `{"folder_uuid": "…"}`; создаёт одноимённую папку комнаты и прикрепляет файлы без комнаты. `201 {"data": {"attached": 5, "folder": {…}}}`. Ошибки: `422 folder_empty`, `422 too_many_files`, `410`, `404`. - `DELETE /rooms/{uuid}/folders/{folderUuid}` — удалить папку комнаты; файлы остаются в комнате (в корне). `204`. ### Чек-лист клиента (документы, анкеты, согласования) Пункт чек-листа: ```json {"id": 5, "kind": "document|form|approval", "title": "Паспорт", "description": null, "fields": [{"key":"f1","label":"ФИО","type":"text|textarea|choice|yesno","options":[]}], "required": true, "due_on": "2026-10-20", "status": "open|done", "overdue": false, "file_uuid": null, "answers": null, "completed_by": null, "completed_at": null, "reminders_sent": 0, "last_reminded_at": null} ``` - `GET /rooms/{uuid}/requests` → `{"data": [пункты], "meta": {"total","done","max":100}, "settings": {"client_email","remind_every_days"}}`. - `POST /rooms/{uuid}/requests` — `kind` (обязательно), `title` (до 200), `description`, `due_on` (не раньше сегодня), `required` (по умолчанию `true`), `fields` — для `form` обязательно, текст по строке на поле: `Название|тип|вариант1,вариант2` (типы `text`, `textarea`, `choice`, `yesno`; до 30 полей). `201 {"data": пункт}`. Ошибки: `422 requests_limit` (максимум 100), `410 room_closed`, `422`. - `DELETE /rooms/{uuid}/requests/{id}` — `204`. - `POST /rooms/{uuid}/requests/{id}/reopen` — вернуть пункт в «открыт», сбросив ответ/файл. `200 {"data": пункт}`. - `PATCH /rooms/{uuid}/requests-settings` — `client_email` (email, `null` — выключить), `remind_every_days` (1–60; без `client_email` сбрасывается в `null`). Ответ `{"data": {"client_email","remind_every_days"}}`. ### Дата-рум (тариф с функцией `data_room`) - `PATCH /rooms/{uuid}/data-settings` — `nda_text` (до 20000, пусто/`null` — убрать NDA), `qa_enabled`. При заданном NDA включается `require_visitor_name`. Ответ `{"data": <комната>}`. - `PATCH /rooms/{uuid}/files/{fileUuid}/rights` — `{"mode": "open|stamped|view_only|hidden"}`: открыт / со штампом / только просмотр (без скачивания) / скрыт от посетителей. Ответ `{"data": {"file_uuid","visitor_access"}}`. - `GET /rooms/{uuid}/questions?status=all|open|answered` → `{"data": [{"id","question","answer","is_public","asked_by":{"id","name","email"},"answered_at","created_at"}]}`. - `POST /rooms/{uuid}/questions/{id}/answer` — `{"answer": "…" (до 4000), "is_public": false}`; спросившему уходит письмо, если известен email. `200 {"data": вопрос}`. - `GET /rooms/{uuid}/index` — нумерованный индекс документов: `{"data": [{"no":"1.1","folder":"Договоры","name":"a.pdf","size":10,"date":"2026-10-12","sha256":"…"}]}`; `?format=csv` — CSV-файл (как на сайте). - `GET /rooms/{uuid}/reads` — отчёт о чтении: `{"data": [{"file_uuid","document","reader":{"id","name","email"},"opens","seconds","last_read_at"}]}`; `?format=csv` — CSV. ### Выбор фото клиентом (без ограничения тарифом) - `PATCH /rooms/{uuid}/selection-settings` — `selection_enabled` (обязательно), `selection_limit` (1–5000 или `null`; при выключении сбрасывается). При включении `require_visitor_name` становится `true`. Ответ `{"data": <комната>}`. - `GET /rooms/{uuid}/selection` — выбранные файлы: `{"data": {"selection_enabled","selection_limit","count","items":[{"file_uuid","name","note","participant":{"id","name","email"},"submitted_at"}]}}`. Параметры: `participant` (id участника), `format`: `lightroom` (имена через запятую), `capture_one` (имена без расширения по строке), `lines`, `commas`, `noext`, `noext_commas` — тогда в ответ добавляется `data.export = {"format","text"}`; `format=csv` — CSV-файл (File, Client, Note). ### Шаблоны (тариф с функцией `templates`) Ключ шаблона: `b:<имя>` — встроенный (`b:legal`, `b:client_project`, `b:accounting`, `b:real_estate` для дел; `b:kyc`, `b:handover`, `b:nda` для комнат), `u:` — свой. Тексты встроенных шаблонов зависят от языка запроса (ru или en). - `GET /templates` → `{"data": {"cases": [...], "rooms": [...]}}`. Дело: `{"key","kind":"case","name","builtin","folders":["01 Бриф","Отчётность/Налоги"]}`; комната: `{"key","kind":"room","name","builtin","room_type","welcome_message","allow_visitor_upload","require_visitor_name","requests":[{"kind","title","due_days","required","fields"}]}`. У своих шаблонов дополнительно `id` и `created_at`. - `POST /templates` — сохранить свой шаблон дела: `{"name": "…", "folders": ["Docs", "Docs/Sub"]}` (или одна строка с переносами; «Папка/Подпапка», до 60 путей, глубина до 4). `201 {"data": шаблон}`; `422`, если нет ни одной папки. - `DELETE /templates/{id}` — удалить свой шаблон. `204`; чужой — `404`. - `POST /templates/apply-case` — создать структуру папок: `{"template": "b:legal", "name": "Иванов 2026", "scope": "personal|team", "grants": {"": "view|upload|manage|none"}}`. `scope=team` — в текущей команде (`X-Upload-Team`), нужны права на загрузку; `grants` применяются, если вы администратор команды. Ответ `201 {"data": {папка как в /folders, "scope", "team_uuid"}}`. Ошибки: `404` (шаблон), `403 forbidden`. - `POST /rooms/{uuid}/save-template` — сохранить комнату (приветствие, права посетителей, чек-лист) как шаблон комнаты: `{"name": "…"}` → `201 {"data": шаблон комнаты}`. - Создать комнату по шаблону — `template` в `POST /rooms`. ## 9.6 Передача файлов, Inbox, сообщения, общие папки, семья (12.10.2026) Все эндпоинты — Bearer-токен, личное пространство пользователя (заголовок `X-Upload-Team` здесь не используется: как и в вебе, это функции личного кабинета). Ошибки — `{"error":"код","message":"..."}`, валидация — 422 в формате Laravel. Объекты ответа обёрнуты в `data`. ### 1. Передача файла другому пользователю | Метод | Адрес | Что делает | |---|---|---| | POST | `/transfers/lookup` | `{email}` → `{"data":{"found":true,"name":"Bob"}}`. `found:false` для несуществующего, заблокированного, забаненного аккаунта и для себя (блокировку нельзя нащупать). Лимит 20/мин | | POST | `/transfers` | Создать запрос на перевод | | GET | `/transfers` | Входящие и исходящие. Параметры: `direction=incoming\|outgoing\|all` (по умолчанию `all`), `status=pending\|accepted\|declined\|cancelled\|expired\|all` (по умолчанию `pending` — только живые, не истёкшие), `per_page` (1–100, по умолчанию 50), `page` | | POST | `/transfers/{uuid}/accept` | Принять (получатель) | | POST | `/transfers/{uuid}/decline` | Отклонить (получатель) | | POST | `/transfers/{uuid}/cancel` | Отозвать (отправитель) | `POST /transfers`: `{"email":"bob@example.com","file_uuids":["..."],"mode":"move|copy","ttl":"24h|7d|30d"}` (`mode` по умолчанию `move`, `ttl` — `7d`). Берутся только ваши личные файлы (не командные). По файлу, у которого уже висит запрос, новый не создаётся. Ответ `201`: ```json { "data": { "sent": 1, "skipped": 0, "transfers": [ { "uuid": "…", "direction": "outgoing", "status": "pending", "mode": "move", "file": { "uuid": "…", "name": "a.txt", "extension": "txt", "size_bytes": 100 }, "counterparty": { "name": "Bob", "email": "bob@example.com" }, "expires_at": "2026-10-13T10:00:00+00:00", "responded_at": null, "created_at": "2026-10-12T10:00:00+00:00" } ] } } ``` `mode=move` — файл переходит получателю (место освобождается у отправителя, занимается у получателя), `copy` — получатель получает копию, у отправителя файл остаётся (копия занимает место получателя). `status` в списке для просроченного pending-запроса — `expired`. `POST /transfers/{uuid}/accept` → `{"data":{"transfer":{…},"file":{…объект файла…}}}`: `file` — файл у получателя (при `copy` это новый файл с новым `uuid`). Ошибки: `404 recipient_not_found` (нет такого аккаунта, или он вас заблокировал), `404 files_not_found`, `404 not_found` (чужой перевод), `403 child_family_only` (детский профиль шлёт только своей семье), `403 forbidden` (не та роль: принять/отклонить может получатель, отозвать — отправитель), `422 transfer_self`, `409 already_pending` (по всем файлам уже есть запросы), `409 not_pending` (перевод уже обработан), `410 transfer_expired`, `413 quota_exceeded` (файл не помещается в квоту получателя), `409 transfer_stale` (файл удалён или уже передан). ### 2. Inbox кабинета | Метод | Адрес | Что делает | |---|---|---| | GET | `/inbox/items` | Единый список ожидающих решения: файлы, присланные по публичной ссылке приёма (`type:"drop"`), и входящие переводы (`type:"transfer"`). Фильтр `type=drop\|transfer`. Сначала новые | | GET | `/inbox/badge` | Счётчик для красной цифры: `{"data":{"pending":3,"transfers":1,"drops":1,"unread_messages":1}}` (`pending` = сумма всех трёх) | Элемент списка: `{type, uuid, file:{uuid,name,extension,size_bytes}, from:{name,email}, mode, expires_at, created_at}` (`mode` и `expires_at` — только у перевода). Ответ: `meta` = `{total, drops, transfers}`. Решение: для `drop` — существующие `POST /files/{uuid}/accept|decline`, для `transfer` — `POST /transfers/{uuid}/accept|decline`. Настройки ссылки приёма (`GET/PATCH /inbox`) не менялись. ### 3. Сообщения и поддержка | Метод | Адрес | Что делает | |---|---|---| | GET | `/messages` | Сообщения Inbox (ответы поддержки, новости, личные, системные), новые сверху. Параметры: `unread=1`, `type=support_reply\|news\|direct\|review\|system`, `per_page` (1–100, по умолчанию 30), `page` | | POST | `/messages` | Написать пользователю: `{email, body}` или ответ `{reply_to: , body}`; `body` до 5000 символов. Лимит 10/мин | | POST | `/messages/{id}/read` | Отметить прочитанным → `{"data":{"id":5,"read":true,"unread":2}}` | | POST | `/messages/read-all` | Все прочитаны → `{"data":{"unread":0}}` | | POST | `/messages/{id}/block` | Заблокировать автора личного сообщения (его сообщения исчезают из Inbox, переводы от него не приходят) → `{"data":{"blocked_user_id":7}}` | | GET | `/messages/blocked` | `{"data":[{"user_id":7,"name":"…","email":"…"}]}` | | DELETE | `/messages/blocked/{userId}` | Снять блокировку, `204` | | POST | `/support` | Обращение в поддержку: `{topic: account\|billing\|upload\|vpn\|app\|other, message: 10–5000 символов}` → `201 {"data":{"id":12,"status":"new","priority":true}}` (`priority` — у платных тарифов). Ответ придёт на email аккаунта и в Inbox. Лимит 5/мин | Элемент `GET /messages`: `{id, type, subject, body, url, from:{id,name,email}|null, read, read_at, created_at}`; `meta` = `{page, per_page, total, unread}`. Ошибки: `404 recipient_not_found` (нет аккаунта, себе, забаненному), `403 child_family_only`, `404` (чужое сообщение, блокировать можно только личное от другого пользователя, снимать — только свою блокировку). Если получатель вас заблокировал, `POST /messages` отвечает `201` как обычно, но сообщение не доставляется. ### 4. Общие папки между пользователями | Метод | Адрес | Что делает | |---|---|---| | GET | `/shared-folders` | `{"data":{"with_me":[…],"by_me":[…],"limit":25,"used":3}}` (`limit` — лимит тарифа `shared_folders`, `null` = без ограничения) | | POST | `/shared-folders` | Открыть свою личную папку: `{folder_uuid, email, permission: view\|upload}` (`edit` принимается как синоним `upload`). `201` при новом доступе, `200` при смене прав. Получателю приходит сообщение в Inbox | | DELETE | `/shared-folders/{id}` | Закрыть доступ (владелец) или отказаться от него (получатель), `204`. `id` — из списка | | GET | `/shared-folders/{folderUuid}/files` | Содержимое открытой вам папки (или вашей): `{folder:{uuid,name,kind,parent_uuid}, level: view\|upload\|owner, owner, subfolders:[{uuid,name}], files:[{uuid,name,extension,size_bytes,uploaded_by,password_protected,has_thumbnail,expires_at,created_at,download_page_url}]}`, до 500 файлов. Доступ действует на вложенные папки | Элемент списка: `{id, folder:{uuid,name,kind}, permission, owner:{id,name}}` (`with_me`) или `{…, shared_with:{id,name,email}}` (`by_me`). `view` — смотреть и скачивать, `upload` — ещё и добавлять файлы (обычной загрузкой с `folder_uuid`; файл остаётся у владельца и занимает его место). Удалять и переименовывать может только владелец. Ошибки: `404 recipient_not_found`, `404 not_found` (чужая папка, нет доступа, чужая запись), `403 child_restricted` (детский профиль открывает папки только своей семье), `403 plan_limit_reached` (`limit:"shared_folders"`, `max`), 422 валидации. ### 5. Семья | Метод | Адрес | Что делает | |---|---|---| | GET | `/household` | Всё сразу (ниже) | | POST | `/household/invite` | `{email, is_child?}` — пригласить по email (письмо уходит), `201` с объектом участника. Нужен платный тариф, не входящий в чужую семью | | POST | `/household/members/{id}/child` | Детский профиль: `{is_child: true\|false}`; без поля — переключает. Только владелец | | DELETE | `/household/members/{id}` | Убрать участника или отозвать приглашение, `204`. Только владелец | | POST | `/household/leave` | Участник выходит из семьи, `204` (`404 not_member`, если не в семье) | | POST | `/household/album` | Создать семейный альбом и открыть его всем участникам на добавление (`201`; повторно `200`). Только владелец | | GET | `/household/members/{id}/files` | Родительский контроль: файлы активного детского профиля — только `name`, `size_bytes`, `created_at`, до 300. Без ссылок и скачивания | | POST | `/household/invites/{token}/accept` | Принять приглашение (`token` из `invitations`); возвращает `GET /household` | | POST | `/household/invites/{token}/decline` | Отклонить, `204` | `GET /household`: ```json { "data": { "role": "owner", "can_invite": true, "max_members": 5, "occupied": 2, "pool": { "total_bytes": 2199023255552, "used_bytes": 1000 }, "members": [ { "id": 3, "email": "mom@example.com", "status": "active|pending", "is_child": false, "user": { "id": 9, "name": "Mom", "storage_used_bytes": 500 }, "invited_at": "…", "joined_at": "…" } ], "membership": null, "album": { "uuid": "…", "name": "Family album" }, "invitations": [ { "token": "…", "owner": { "id": 1, "name": "…", "email": "…" }, "is_child": false, "invited_at": "…" } ] } } ``` `role`: `owner` | `member` | `none`. `pool` — общее место семьи, `null` вне семьи. `membership` — для участника: `{is_child, joined_at, owner}`. `invitations` — приглашения, выписанные на email вашего аккаунта. Чужой токен приглашения даёт `404`. Ошибки: `403 forbidden` (нужен платный тариф / не владелец), `422 invalid_invite` (себя), `422 already_invited`, `422 household_full` (`max`), `422 unavailable` (аккаунт забанен или уже в семье), `409 unavailable` и `409 owner_inactive` при принятии, `404` (чужой участник, не детский профиль). ## 9.7 Рабочие инструменты и электронная подпись (12.10.2026) Всё ниже зеркалит раздел «Работа» кабинета. Общие правила: - **Контекст.** Без заголовка — личная рабочая область. С `X-Upload-Team: ` — область команды. Не участник команды или гость → `403 {"error":"forbidden"}`. Смотреть могут все участники (кроме гостей), менять — роли `owner`, `admin`, `editor`, `contributor`; `member`/`viewer` получают `403 forbidden` на изменения. Исключение: исполнитель задачи может отметить её выполненной в любой роли. - **Тариф.** Проекты, клиенты, база знаний и автоматизации требуют функцию `work_tools`; электронная подпись и её шаблоны — `esign`. Без неё: `403 {"error":"plan_feature_locked","feature":"…","min_plan":"…","message":"…"}`. - Проекты, клиенты и статьи адресуются по `uuid`; задачи, документы клиентов, правила, шаблоны и подписанты — по числовому `id` внутри своего родителя. Файлы передаются как `file_uuid` (файл должен быть в той же области: личной или команды, и виден вам); неизвестный файл → `422` с ошибкой в `file_uuid`. - Чужой или несуществующий ресурс → `404`. Даты: `due_on`, `issued_on` — `YYYY-MM-DD`; моменты времени — ISO 8601. - Удаление возвращает `{"data":{"deleted":true}}`. Ошибки валидации — стандартный `422` Laravel. Лимиты областей: 500 задач на проект, 5000 клиентов, 1000 статей, 50 правил → `422 {"error":"limit_reached"}`. - Списки с `meta.current_page/last_page/total` принимают `per_page` (до 100) и `page`. ### Проекты и задачи (тариф `work_tools`) #### `GET /projects` Параметры: `status=active|done` (необязательно), `per_page`, `page`. Сначала активные, по сроку. ```json {"data":[{"uuid":"…","name":"Website","description":null,"due_on":"2026-11-01","status":"active", "client":{"uuid":"…","name":"Anna"},"tasks_count":4,"open_tasks_count":3,"created_at":"2026-10-12T09:00:00+00:00"}], "meta":{"current_page":1,"last_page":1,"total":1}} ``` #### `POST /projects` → `201` `name` (обязательно, до 160), `description` (до 5000), `due_on`, `client_uuid` (клиент той же области; не найден → 422). Ответ — проект с `tasks` и `members`. #### `GET /projects/{uuid}` Проект целиком: к полям списка добавлены `tasks[]` и `members[]` (кого можно назначить исполнителем: `id`, `name`, `email`). ```json {"data":{"uuid":"…","name":"Website","status":"active", "tasks":[{"id":7,"title":"Send logo","due_on":"2026-10-15","status":"open","overdue":false,"done_at":null, "assignee":{"id":3,"name":"Olga"},"files":[{"uuid":"…","name":"logo.png"}],"created_at":"…"}], "members":[{"id":3,"name":"Olga","email":"olga@example.com"}]}} ``` `files[]` — стандартный объект file (как в `GET /files`). #### `PATCH /projects/{uuid}` Любые из `name`, `description`, `due_on`, `status` (`active|done`). Возвращает проект. #### `DELETE /projects/{uuid}` Удаляет проект вместе с задачами. #### `POST /projects/{uuid}/tasks` → `201` `title` (обязательно, до 200), `due_on`, `assignee_id` (id пользователя: в личной области только вы, в команде — активный участник; иначе задача создаётся без исполнителя), `file_uuid`. Ответ — объект задачи. #### `PATCH /projects/{uuid}/tasks/{id}` Любые из: `title`, `due_on` (`null` снимает срок), `assignee_id` (`null` снимает исполнителя; не участник → 422), `done` (boolean) либо `status` (`open|done`). Смена `done`/`status` разрешена редакторам и самому исполнителю, остальные поля — только редакторам. #### `DELETE /projects/{uuid}/tasks/{id}` #### `POST /projects/{uuid}/tasks/{id}/attach` Тело `{"file_uuid":"…"}` — прикрепляет файл (повторное прикрепление безвредно). Возвращает задачу. #### `GET /tasks` — мои задачи Открытые задачи, назначенные на вас, по проектам текущей области. Фильтры: `status=open|done|all` (по умолчанию `open`), `due=overdue|today|week|none`, `project=`. В каждой задаче есть `project: {uuid, name}`. ### Клиенты, договоры, счета (тариф `work_tools`) #### `GET /clients` Параметры: `q` (имя, компания, email), `per_page`, `page`. В `meta` кроме пагинации — суммы по валютам: ```json {"data":[{"uuid":"…","name":"Anna","company":"ACME","email":"a@acme.test","phone":null,"notes":null,"documents_count":2,"created_at":"…"}], "meta":{"current_page":1,"last_page":1,"total":1, "unpaid":[{"currency":"USD","total":150.5,"count":2}], "overdue":[{"currency":"USD","total":120.5,"count":1}]}} ``` `unpaid` — счета (`kind=invoice`) в статусе `sent`; `overdue` — из них со сроком раньше сегодняшнего дня. #### `POST /clients` → `201`, `GET /clients/{uuid}`, `PATCH /clients/{uuid}`, `DELETE /clients/{uuid}` Поля: `name` (обязательно при создании, до 160), `company`, `email`, `phone` (до 60), `notes` (до 5000). `GET` и остальные ответы содержат `documents[]` и `projects[]`: ```json {"data":{"uuid":"…","name":"Anna","documents":[{"id":5,"kind":"invoice","number":"2026-03","title":"March","amount":120.5,"currency":"USD", "issued_on":null,"due_on":"2026-10-11","status":"sent","overdue":true,"file":{"uuid":"…","name":"inv.pdf"},"created_at":"…"}], "projects":[{"uuid":"…","name":"Website","status":"active"}]}} ``` #### `POST /clients/{uuid}/docs` → `201` `kind` (`contract|invoice|act|other`), `title` (обязательно), `number`, `amount`, `currency` (3 буквы), `issued_on`, `due_on`, `status` (`draft|sent|paid|cancelled`, по умолчанию `draft`), `file_uuid`. #### `PATCH /clients/{uuid}/docs/{id}` `status` (`draft|sent|paid|cancelled`) и/или `file_uuid` (`null` отвязывает файл). #### `DELETE /clients/{uuid}/docs/{id}` ### База знаний (тариф `work_tools`) #### `GET /knowledge-base` Параметры: `q` (заголовок, текст, категория), `category`. Список без текстов статей, отсортирован по категории и заголовку: `{"data":[{"uuid":"…","title":"Onboarding","category":"HR","shared":false,"public_url":null,"updated_at":"…"}]}` #### `POST /knowledge-base` → `201`, `GET /knowledge-base/{uuid}`, `PATCH /knowledge-base/{uuid}`, `DELETE /knowledge-base/{uuid}` `title` (до 200), `category` (до 80), `body` (Markdown, до 100000). В `GET`/`POST`/`PATCH` кроме полей списка приходят `body` (исходный Markdown), `html` (отрисованный, HTML во вводе вырезается, небезопасные ссылки не рендерятся) и `created_at`. #### `POST /knowledge-base/{uuid}/share` Секретная публичная ссылка (страница `/kb/{token}` без входа, не индексируется). Тело `{"enabled":true|false}`; без `enabled` переключает текущее состояние. Включение повторно сохраняет прежний токен. Ответ — статья, `public_url` заполнен при `shared=true`. ### Правила автоматизации (тариф `work_tools`) #### `GET /automations` ```json {"data":[{"id":4,"name":"PDF invoices","trigger":"file_uploaded","action":"move_to_folder", "conditions":{"extensions":"pdf","name_contains":"invoice"},"params":{"folder_uuid":"…"}, "enabled":true,"runs":12,"last_run_at":"2026-10-12T08:00:00+00:00","created_at":"…"}], "meta":{"triggers":["file_uploaded","review_approved","review_changes"],"actions":["move_to_folder","set_label","notify_email","create_task","lock_days"],"labels":["internal","confidential","restricted"],"max_rules":50}} ``` #### `POST /automations` → `201` `name`, `trigger` (из `meta.triggers`), `action` (из `meta.actions`), `conditions` (необязательно: `folder_uuid`, `extensions` — через запятую, `name_contains`) и `params` — зависят от действия: | action | params | |---|---| | `move_to_folder` | `folder_uuid` (папка области) | | `set_label` | `label` (`internal|confidential|restricted`) | | `notify_email` | `email` | | `lock_days` | `days` (1–3650) | | `create_task` | `project_uuid` (активный проект области), `title` (можно `:file` — имя файла), `due_days` (0–365) | Не заполнен обязательный параметр действия или папка/проект не найдены → `422` с ошибкой в `params`. В ответе для `create_task` приходит `params.project_uuid`. #### `POST /automations/{id}/toggle` Включает/выключает правило, возвращает его. #### `DELETE /automations/{id}` ### Электронная подпись PDF (тариф `esign`) Простая электронная подпись: подписанты получают письмо со ссылкой (подписывают в браузере без регистрации), порядок — по очереди или параллельно. Итоговый PDF с подписями и сертификат завершения сохраняются файлами владельца запроса. #### `GET /signatures` Параметры: `status=pending|completed|declined|cancelled|expired`, `per_page` (по умолчанию 25), `page`. ```json {"data":[{"uuid":"…","title":"NDA","status":"pending","expired":false,"order":"sequential", "file":{"uuid":"…","name":"contract.pdf"}, "signers":[{"id":11,"position":1,"name":"Bob","email":"bob@x.test","status":"sent","signature_type":null,"decline_reason":null,"last_sent_at":"…","signed_at":null}], "expires_at":"…","completed_at":null,"created_at":"…"}], "meta":{"current_page":1,"last_page":1,"total":1,"max_signers":10}} ``` Статусы запроса: `pending`, `completed`, `declined`, `cancelled`, `expired`; подписанта: `waiting` (не его очередь), `sent`, `signed`, `declined`. #### `POST /signatures` → `201` - `file_uuid` — PDF (иначе 422); - `signers` — массив до 10 объектов `{name, email}` (`name` необязателен; дубли по email склеиваются). Можно не передавать, если указан `template`; - `order` — `sequential` (по умолчанию) или `parallel`; - `message` (до 2000), `days` — срок в днях (1–90, по умолчанию 30), `title` (по умолчанию имя файла); - `template` — id шаблона: подставляет подписантов, порядок и сообщение, явно переданные поля имеют приоритет; - `template_name` — сохранить подписантов, порядок и сообщение как шаблон. Приглашения уходят сразу (при `sequential` — только первому). Ответ — полный объект запроса, как у `GET /signatures/{uuid}`. #### `GET /signatures/{uuid}` К полям списка добавлены `message`, `doc_sha256`, `signed_sha256`, `signatures_embedded` (подписи вшиты в PDF; иначе подписи только в сертификате), `events[]` (журнал: `type`, `signer_email`, `ip`, `at`) и, когда запрос `completed`, `signed_file` и `certificate_file`: ```json {"signed_file":{"uuid":"…","name":"contract (signed).pdf","download_page_url":"https://upload.am/d/…","download_url":"https://…s3…","download_url_expires_in":600}} ``` `download_url` — временная ссылка на скачивание (10 минут; `null`, если хранилище недоступно), `download_page_url` — страница файла на сайте. Остальные поля — стандартный объект file. До завершения оба поля `null`. #### `POST /signatures/{uuid}/cancel` Отменяет ожидающий запрос. Уже закрытый → `409 {"error":"not_pending"}`. #### `POST /signatures/{uuid}/signers/{id}/resend` Повторно высылает приглашение подписанту (`id` из `signers[]`). Подписанту сейчас нельзя подписать (уже подписал, не его очередь, запрос закрыт) → `409 {"error":"cannot_resend"}`. #### `GET /signature-templates` `{"data":[{"id":2,"name":"NDA","signers":[{"name":"Bob","email":"bob@x.test"}],"order":"sequential","message":null,"created_at":"…"}]}` #### `DELETE /signature-templates/{id}` ### Миграция Миграция `2024_01_01_000150_work_tools_uuids` добавляет `uuid` проектам, клиентам и статьям базы знаний (существующие строки получают значения автоматически). ## 9.8 Файловые операции и совместная работа (группа C, 12.10.2026) Все эндпоинты — Bearer-токен, база `/api/v1`. Контекст: личное пространство или команда (заголовок `X-Upload-Team: `, не участник → `403 not_team_member`). Там, где это сказано, права считаются так же строго, как на сайте: роли admin / editor / contributor / viewer, блокировка до даты (`423`/`retention_locked`), юридическое удержание, блокировка файла другим участником (`locked_by_other`). Ошибки валидации — стандартный `422` Laravel, остальные — `{"error":"code","message":"..."}`. Чужой или несуществующий ресурс — `404`. | Метод | Адрес | Что делает | |---|---|---| | POST | `/bulk` | Массовое действие над файлами и папками с раскладкой по каждому uuid | | POST | `/files/{uuid}/edit-lock` | Блокировка файла команды на время правки (24 часа) | | DELETE | `/files/{uuid}/edit-lock` | Снять блокировку правки | | GET | `/files/{uuid}/review` | Статус согласования и обсуждение | | POST | `/files/{uuid}/review/comments` | Добавить комментарий (с точкой на изображении или временем кадра) | | POST | `/files/{uuid}/review/comments/{id}/resolve` | Отметить замечание решённым / вернуть в работу | | POST | `/files/{uuid}/review/decision` | Запросить проверку / согласовать / запросить правки | | GET | `/files/{uuid}/edit-url` | Адрес онлайн-редактора (Collabora/WOPI) | | POST | `/files/{uuid}/extract` | Распаковать архив в облаке | | GET | `/duplicates`, POST `/duplicates/clean` | Дубликаты и пустые папки, очистка | | GET | `/trash/expired`, POST `/trash/restore-expired` | Истёкшие файлы, которые ещё можно вернуть | | GET | `/rollback/preview`, POST `/rollback` | Откат аккаунта или папки на дату | | GET | `/folders/{uuid}/protection` | Статус защиты папки | | PATCH | `/folders/{uuid}` | Переименовать и/или переместить папку (расширен: `parent_uuid`) | | PATCH | `/shares/{uuid}` | Изменить настройки существующей ссылки | | GET | `/shares/{uuid}/visits` | История посещений ссылки и сводки | | GET | `/activity` | Лента событий аккаунта | ### `POST /bulk` — массовые действия Тело: `action` — `delete` | `move` | `lock` | `label` | `to_team` | `to_personal`; `file_uuids[]` (до 500) и/или `folder_uuids[]` (до 200; хотя бы одно из двух); `target_folder_uuid` (для `move`, пусто = корень); `retain_until` (дата в будущем, для `lock`); `label` (`none|internal|confidential|restricted`, для `label`). Контекст: без заголовка — личные файлы; с `X-Upload-Team` — файлы и папки этой команды. Действия: - `delete` — в корзину. Файл с блокировкой до даты пропускается (`retention_locked`). Папка удаляется вместе с поддеревом (файлы уходят в родителя), папка под юридическим удержанием пропускается (`legal_hold`). - `move` — файлы и папки в `target_folder_uuid`. Папку нельзя переместить в себя или потомка (`move_into_descendant`). В команде нужно право загрузки в целевую папку (иначе `403`). - `lock` — блокировка до даты, срок только продлевается (`cannot_shorten`); в команде только админ (`403 forbidden`). `retain_until` обязателен (`422`). - `label` — метка конфиденциальности (`label` обязателен). Только файлы. - `to_team` — личные файлы в пул команды (нужен `X-Upload-Team`, админ; без заголовка `422 team_required`); `pool_full`, если в пуле нет места. - `to_personal` — файлы команды в личное пространство (заголовок, админ); `quota_exceeded`, если не хватает вашей квоты. Папки поддерживают только `delete` и `move`; при остальных действиях папка получает `skipped` / `unsupported_action`. Файлы, заблокированные на время правки другим участником, пропускаются (`locked_by_other`). ```json { "data": { "action": "move", "scope": "personal", "done": 2, "skipped": 0, "errors": 1, "results": [ { "type": "file", "uuid": "…", "status": "done" }, { "type": "file", "uuid": "…", "status": "error", "code": "not_found", "message": "File not found." }, { "type": "folder", "uuid": "…", "status": "done" } ] } } ``` `status`: `done` | `skipped` | `error`. Коды: `not_found`, `forbidden`, `locked_by_other`, `retention_locked`, `cannot_shorten`, `legal_hold`, `move_into_descendant`, `unsupported_action`, `delete_failed`, `pool_full`, `quota_exceeded`. Если ни один uuid не найден — `404 not_found` (с теми же `results`). Роль viewer получает `error`/`forbidden` по каждому файлу. ### Блокировка на время правки (команда) Работает с файлом команды (личный файл → `404`); `X-Upload-Team` не нужен. Не блокирует просмотр — запрещает другим участникам переименовывать, удалять и перемещать файл. Срок — 24 часа. - `POST /files/{uuid}/edit-lock` — нужно право менять файл (`403 forbidden` у viewer). Занято другим участником → `423 locked_by_other` (с полем `edit_lock`). Повторный вызов владельцем блокировки продлевает срок. Ответ: `{ "data": { …file…, "review_status": null, "edit_lock": { "locked_by": {"id": 1, "name": "…"}, "locked_at": "…", "expires_at": "…" } } }`. - `DELETE /files/{uuid}/edit-lock` — снимает автор блокировки или админ команды (иначе `403 forbidden`); `edit_lock` в ответе становится `null`. (Блокировка до даты — это другое: `POST /files/{uuid}/lock`, раздел 9.2.) ### Согласование и обсуждение (команда) - `GET /files/{uuid}/review` — любой, кто видит файл. Ответ: `data.file` (файл + `review_status`, `edit_lock`), `review_status` (`null` | `pending` | `approved` | `changes_requested`), `edit_lock`, `can_mutate`, `comments[]`: `{ id, kind, body, user:{id,name}, pos_x, pos_y, ts_ms, resolved, resolved_at, created_at }`. `kind`: `comment`, `review_requested`, `approved`, `changes_requested`. `pos_x`/`pos_y` — точка на изображении в процентах 0–100, `ts_ms` — время кадра видео/аудио в мс (все три могут быть `null`). - `POST /files/{uuid}/review/comments` — `{ body (до 3000), pos_x?, pos_y?, ts_ms? }` → `201`, комментарий. Остальным участникам уходит уведомление в Inbox. - `POST /files/{uuid}/review/comments/{id}/resolve` — переключатель «решено / в работе»; право: автор замечания, участник с правом менять файл или админ (иначе `403`); нет замечания → `404 comment_not_found`. - `POST /files/{uuid}/review/decision` — `{ decision: request|approve|changes, body? }`. `request` — только тем, кто может менять файл (`403`); `changes` требует `body` (`422`). Ответ: `{ "data": { "review_status": "approved" } }`. Решения `approve`/`changes` запускают автоматизации команды. ### `GET /files/{uuid}/edit-url` — онлайн-редактор Тариф: функция `online_editor` (нет → `403 plan_feature_locked`). Файл — ваш личный или файл команды, который вам виден. Форматы: doc, docx, odt, rtf, xls, xlsx, ods, csv, ppt, pptx, odp, txt до 100 МБ. Редактор открывается так: клиент отправляет `POST` формой (в окне/WebView) на `editor_url` с полями из `form`. ```json { "data": { "editor_url": "https://office.example/browser/…/cool.html?WOPISrc=…&lang=en", "method": "POST", "form": { "access_token": "…", "access_token_ttl": "1760300000000" }, "write": true, "expires_at": "2026-10-12T20:00:00+00:00" } } ``` `write=false` — режим просмотра (нет права менять файл). Сохранение создаёт новую версию файла. Необязательный `?lang=` задаёт язык интерфейса редактора (по умолчанию `en`). Ошибки: `404 editor_unavailable` с полем `reason` — `not_configured` (редактор не подключён на сервере), `unsupported_format` (формат или размер), `no_editor_for_extension`; чужой файл → `404 file_not_found`. ### `POST /files/{uuid}/extract` — распаковка архива Только личные файлы. Поддержаны zip, tar, tar.gz/tgz, rar, 7z; архив до 2 ГБ, до 5000 записей, до 10 ГБ в сумме, вложенность до 20 уровней. Небезопасные и запрещённые по типу записи пропускаются. Распаковка идёт в фоне в папку рядом с архивом, по итогу в Inbox приходит сообщение. Ответ `202`: `{ "data": { "status": "queued", "file_uuid": "…", "message": "…", "limits": {…} } }`. Ошибки: `422 archive_not_supported`, `422 archive_too_big` (`max_bytes`), `404`. ### Дубликаты и пустые папки - `GET /duplicates` — личные файлы с одинаковым SHA-256 и размером (у файлов без хеша дубликаты не ищутся). Ответ: `reclaimable_bytes`, `groups[]` (`sha256`, `size_bytes`, `copies`, `reclaimable_bytes`, `files[]` — обычные объекты файла + `in_active_share`), `empty_folders[]` (`uuid`, `name`, `parent_uuid`). До 100 групп и 200 папок. - `POST /duplicates/clean` — `{ file_uuids[], folder_uuids[] }` (хотя бы одно). Выбранные копии уходят в корзину, пустые папки удаляются. Не удаляются (`skipped`): копии с действующей ссылкой (`in_active_share`), заблокированные до даты (`retention_locked`), без хеша (`no_fingerprint`), единственные в группе (`not_a_duplicate`), непустые папки (`not_empty`). Если выбраны все копии группы, самая ранняя остаётся (`kept_last_copy`). Ответ: `removed_files`, `removed_folders`, `done`/`skipped`/`errors`, `results[]`. ### Истёкшие файлы Файл с истёкшим сроком хранения можно вернуть в течение окна `restore_days` (настройка сайта, по умолчанию 14 дней). - `GET /trash/expired` — `{ restore_days, scope, files[] }`: файлы с `expires_at` в прошлом и полем `restorable_until`. В команде видно только админу, иначе пустой список; при `restore_days = 0` список пуст. - `POST /trash/restore-expired` — `{ file_uuids[] (до 200) }`. Срок хранения считается заново по тарифу владельца (не меньше 7 дней). Результат по каждому uuid: `done` (+ новый `expires_at`), либо `skipped`: `not_expired`, `window_passed`, `restore_disabled`; чужой файл — `error`/`not_found`. Ответ: `restored`, `done`/`skipped`/`errors`, `results[]`. ### Откат на дату Защита от шифровальщика: вернуть удалённое, откатить перезаписанное до версии на момент `until`, по желанию убрать созданное позже. Параметры (query для GET, тело для POST): `until` — ISO 8601, в прошлом, не старше 90 дней; `folder_uuid?` — откат только папки с вложенными; `trash_newer?` — отправить в корзину файлы, созданные после `until`. С `X-Upload-Team` откатываются файлы всей команды (только админ, иначе `403`). - `GET /rollback/preview` — ничего не меняет. `data.counts` (`restore`, `revert`, `trash`, `total`), `too_many` (больше `max_files` = 5000), списки `restore[]` (`uuid`, `name`, `deleted_at`), `revert[]` (`uuid`, `name`, `to_version`, `replaced_at`), `trash[]` (`uuid`, `name`, `created_at`) — не больше `list_limit` = 100 каждый; `trash_days` — как долго живёт корзина (удалённое раньше уже может быть стёрто). - `POST /rollback` — применяет. `422 too_many`, если затронуто больше 5000 файлов. Ответ: `{ "data": { "until": "…", "scope": "personal", "restored": 3, "reverted": 2, "trashed": 0 } }`. Текущее содержимое откатываемых файлов сохраняется отдельной версией. Ошибки: `422` (нет `until`, дата в будущем или старше 90 дней), `404 folder_not_found`. ### Папки - `PATCH /folders/{uuid}` — теперь принимает и `name`, и `parent_uuid` (`null` = в корень); хотя бы одно поле. Переименование работает как раньше. Ошибки: `404 folder_not_found` (родитель не ваш), `422 move_into_descendant`. Только личные папки. - `GET /folders/{uuid}/protection` — статус защиты папки с вложенными: `files_total`, `locked_files`, `locked_until` (самая поздняя блокировка или `null`), `legal_hold` (действует ли юридическое удержание на команду, эту папку, её родителей или вложенные), `legal_holds[]` (`uuid`, `name`, `scope`), `can_delete` (`false`, если внутри папки цель удержания). Блокировка — `POST /folders/{uuid}/lock` (9.2). Контекст: личная папка или папка команды (заголовок). ### `PATCH /shares/{uuid}` — настройки ссылки Своя ссылка или ссылка команды, где вы админ (иначе `404`). Все поля необязательные, отсутствующее не меняется: | Поле | Значение | |---|---| | `ttl` | `10m` `1h` `24h` `7d` (считается от сейчас) или `forever` (убрать срок) | | `expires_at` | точный момент ISO 8601 (вместо `ttl`), от +1 минуты до +10 лет | | `max_downloads` | целое ≥ 1; `null` или `"unlimited"` снимает лимит; `1` включает «сгореть после скачивания» | | `password` / `clear_password` | новый пароль (по политике аккаунта, `422 weak_password`) / снять пароль | | `burn_after_download`, `require_login`, `view_only`, `direct_links` | флаги (`direct_links` только на платном тарифе; выключение `view_only` гасит `watermark`) | | `watermark` | водяной знак, только вместе с `view_only` | | `allowed_email`, `remind_after_days` | получатель (`null` снимает) и напоминание через 1/3/7 дней (только вместе с получателем) | | `allowed_ips` | список IP, CIDR или хостов через запятую/пробел; `null` снимает (`422 invalid_allowed_ips`) | | `title`, `message` | заголовок и сообщение | Для ссылок команды действуют политики команды: `422 policy_password_required`, `policy_login_required`, `policy_no_direct_links`, `policy_max_days`, `policy_link_domains_required`. Отозванную ссылку менять нельзя (`422 share_revoked`). Ответ — объект ссылки, как в `GET /shares/{uuid}`. ### `GET /shares/{uuid}/visits` — история посещений Query: `event?` (`opened`, `downloaded`, `zip`, `previewed`, `saved`), `page`, `per_page` (до 100, по умолчанию 50). Ответ: ```json { "data": [ { "id": 12, "event": "downloaded", "at": "2026-10-12T10:00:00+00:00", "file": { "uuid": "…", "name": "doc.pdf" }, "country": "DE", "device": "Desktop", "browser": "Firefox", "ip": "203.0.113.x", "anonymous": false, "viewer_email": "v@example.com" } ], "meta": { "current_page": 1, "last_page": 1, "total": 3, "per_page": 50 }, "summary": { "share_uuid": "…", "view_count": 2, "download_count": 1, "by_event": { "opened": 2, "downloaded": 1 }, "by_country": { "DE": 2, "US": 1 }, "by_device": { "Desktop": 2, "Mobile": 1 }, "files": [ { "uuid": "…", "name": "doc.pdf", "downloaded": true } ] } } ``` Сводки считаются по всем событиям (не по странице); `by_country` — до 8 стран. IP показан с замаскированным последним блоком. ### `GET /activity` — лента событий аккаунта Query: `source` — `account` (по умолчанию: ваши события — `uploaded`, `inbox_received`, `share_created`, `transfer_sent`, `file_trashed`, `rollback`; можно отфильтровать `action=`), `downloads` (скачивания по вашим ссылкам), `rooms` (события ваших комнат), `all` (всё вместе по времени, глубина страниц ограничена 200); `page`, `per_page` (до 100, по умолчанию 30). Элемент: `{ source, type, file: {uuid,name}|null, meta, created_at }`; `type` — `uploaded` …, `share_downloaded`, `room_<событие>` (`room_viewed`, `room_uploaded`, `room_downloaded`, `room_signed`, …). `meta` для `downloads`: `share_uuid`, `share_title`, `country`, `device`, `browser`; для `rooms`: `room_uuid`, `room_name` и данные события. Ответ: `{ "data": […], "meta": { current_page, last_page, total, per_page } }`. ## 10. Коды ошибок Стандартные HTTP-коды. Валидационные ошибки (`422`) — в формате Laravel: ```json { "message": "The name field is required.", "errors": { "name": ["The name field is required."] } } ``` Остальные ошибки — `{"error": "код_ошибки", "message": "человекочитаемое описание"}`. | HTTP | Значение | |---|---| | 400 | Некорректный запрос (например, пустой список файлов) | | 401 | Не аутентифицирован (см. раздел 1) | | 403 | Аутентифицирован, но действие запрещено | | 404 | Не найдено, или найдено, но не ваше — `{"error":"not_found","message":"Resource not found."}`. Для неизвестного `folder_uuid` на загрузке — `folder_not_found` | | 409 | Конфликт (например, повторное подтверждение одной и той же загрузки) | | 410 | Ресурс истёк или отозван | | 413 | Превышена квота/лимит размера | | 422 | Не прошла валидация. Для `POST /upload/multipart/complete` — также `{"error":"multipart_complete_rejected","code":"InvalidPart\|NoSuchUpload\|InvalidPartOrder\|EntityTooSmall"}`: список частей не совпал с реально загруженными или загрузка уже завершена/прервана — начните загрузку этого файла заново (сервер сам сортирует части по `PartNumber`) | | 429 | Превышен лимит запросов — см. `Retry-After` | | 507 | Нода хранения временно переполнена | ### Машиночитаемые коды ошибок (`error`) | Код | HTTP | Когда | |---|---|---| | `verification_required` | 202 | Нужен второй шаг входа: `POST /auth/verify {challenge, code}` | | `invalid_credentials`, `invalid_code`, `invalid_token`, `expired_token`, `missing_token` | 401/422 | Вход и токены, см. раздел 1 | | `account_disabled` | 403 | Аккаунт отключён | | `device_wipe_pending` | 403 | Для устройства запрошена удалённая очистка: клиент стирает локальные копии и вызывает `POST /device/wiped` | | `plan_feature_locked` | 403 | Функция не входит в тариф; поля `feature`, `min_plan` | | `plan_limit_reached` | 403 | Исчерпан числовой лимит тарифа; поля `limit`, `max` | | `child_account` | 403 | Детский профиль не создаёт публичные ссылки | | `forbidden` | 403 | Недостаточно прав (роль в команде, права на папку) | | `retention_locked` | 423 | Файл заблокирован до даты (`retain_until`) | | `protected` | 423 | Файл или папка под юридическим удержанием | | `trash_locked` | 423 | Недавно удалённый файл нельзя стереть насовсем несколько дней | | `blocked_extension` | 422 | Запрещённый тип файла | | `blocked_content` | 422 | Под видом картинки или документа — программа, скрипт или PHP | | `weak_password` | 422 | Пароль ссылки слабый, а в аккаунте включена политика сильных паролей | | `invalid_allowed_ips` | 422 | Неверный список IP, подсетей или имён хостов в `allowed_ips` | | `cannot_shorten` | 422 | Блокировку можно только продлить | | `team_required` | 422 | Действие нужно выполнять в команде (`X-Upload-Team`) | | `domain_not_allowed`, `already_member` | 422 | Приглашение в команду: домен не разрешён политикой / уже участник | | `no_wipe_requested` | 409 | `POST /device/wiped`, но очистка не запрашивалась | | `bad_block`, `block_not_reusable` | 422 | Блочная синхронизация: неверный блок / блок нельзя переиспользовать | | `base_changed` | 409 | Блочная синхронизация: файл изменился после чтения списка блоков — начать заново | | `storage_unsupported` | 501 | Хранилище не умеет блочную синхронизацию — загрузите файл целиком | | `files_not_found`, `folder_not_found` | 404/422 | Не найдены файлы/папка | | `no_snapshot`, `no_release` | 404 | Нет копии/релиза | | `wrong_password` | 403 | Неверный пароль аккаунта (например, `POST /tokens`) | | `room_closed` | 410 | Комната уничтожена или истёк срок | | `file_protected` | 423 | Файл в комнате заблокирован или под удержанием | | `locked_by_other` | 423 | Файл заблокирован на правку другим участником | | `already_pending` | 409 | По файлам уже висит запрос передачи | | `not_pending`, `cannot_resend` | 409 | Подпись: запрос закрыт / повторная отправка невозможна | | `editor_unavailable` | 404 | Онлайн-редактор не подключён или формат не поддержан | | `delete_blocked` | 409 | Удаление аккаунта заблокировано (`reason`: admin, team, protected) | | `cannot_wipe_current`, `cannot_sign_out_current` | 422 | Нельзя очистить/выйти на устройстве, с которого идёт запрос | --- ## 11. Область действия v1 и дорожная карта v1 работает с личным пространством и с командами: команда выбирается заголовком `X-Upload-Team: ` (см. `GET /teams`, раздел 9.4). С 12.10.2026 в API есть все возможности веб-кабинета, нужные клиентам для бизнеса: комнаты и дата-румы, электронная подпись, проекты/клиенты/база знаний/автоматизации, передача файлов, сообщения, семья, массовые действия, откат на дату, журнал аудита и политики команды. Исключения (только веб): управление SSO, публичные страницы посетителей (комнаты, подписание), оплата картой/криптой и страницы сайта. Также запланировано (без даты): - Более узкие права токена (`abilities`) — сейчас токен = полный доступ, поле в БД уже заведено под скоупы вроде `files:read` без новой миграции. - Webhooks (уведомление на ваш URL о скачивании/использовании квоты) — сейчас есть только email-уведомления самому пользователю. - Upload Inbox через API (сейчас — только веб-виджет `/r/{token}`). - Официальный клиент Upload.am для Windows — `docs/WINDOWS_CLIENT.md`, репо `unixanet/backup`. ### Известные пробелы (найдено при аудите документации 20.09.2026) Ничего из этого не задеплоено в v1 — фиксируется здесь явно, чтобы не потерялось перед работой над мобильным приложением: - **Регистрация нового аккаунта отсутствует в API вообще.** Есть только `POST /auth/login` (раздел 1) — клиент может залогинить уже существующего пользователя, но не создать нового. Регистрация — только через сайт (`/{locale}/register`). - 🟡 **`POST /auth/login` по-прежнему не проверяет 2FA — сознательно, не забыто.** Решение владельца, 21.09.2026 (см. `BACKLOG.md`, "Отклонённое"): не реализовывать 2FA для API-логина, веб-логин (`/login`) продолжает требовать её как раньше — решение касается только API-эндпоинта. **22.09.2026 (Раунд 13):** `PATCH /me/two-factor` (раздел 3) всё же добавлен — desktop-клиент теперь может включить/выключить 2FA, как в веб-профиле (владелец, п.12, "все функции внутри профиля должны работать тут"), но это защищает только вход через сайт, не через сам десктоп-клиент — см. честную оговорку в описании эндпоинта. Если это когда-то нужно закрыть полностью — потребуется отдельный флоу подтверждения кода для API (аналог `/login/verify` веб-версии), тот же объём работы, что описан ниже, но требует ЯВНОГО решения владельца отменить пункт из `BACKLOG.md`, не молчаливого повторного внедрения. - ~~Реферальная программа...~~ — закрыто 22.09.2026, см. раздел 3 (`referral_code`/`referral_url`/`referral_count`). Крипто-кошелёк для выплат (`PATCH /me/payout-wallet`) удалён 11.10.2026 вместе с партнёркой. - **Карточная оплата (way2bill) есть только на вебе, не в API.** 26.09.2026 добавлен второй `PaymentGateway` — `Way2BillGateway` (карты, рядом с крипто- `ShieldzGateway`), но только для `Dashboard\BillingController` (веб-кабинет, `/cabinet/upgrade`). `Api\V1\BillingController::checkout()` (раздел ниже) по-прежнему жёстко использует `ShieldzGateway` — сознательно, задача была явно scoped на веб ("внедряешь это внутри личного кабинета"). Если десктоп/мобильному клиенту понадобится карточная оплата — нужен отдельный раунд: выбор метода в `POST /billing/{planCode}/checkout` (сейчас без параметра метода), плюс собственная UI для выбора крипто/карта на стороне клиента. Обратная совместимость: `v1` не будет ломаться существующими полями — новые поля только добавляются. Ломающие изменения, если понадобятся — только в `v2`, `v1` продолжит работать.