# 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 по-прежнему требует как раньше — это решение только про API-эндпоинт, не отменяет 2FA в целом. Выход — отзывает текущий токен: ```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, "payout_wallet_network": null, "payout_wallet_address": null, "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_network`/`payout_wallet_address` — крипто-кошелёк для выплат по партнёрской программе. Меняются через `PATCH /me/payout-wallet` ниже. `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`. ### `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/payout-wallet` — крипто-кошелёк для выплат Добавлено 22.09.2026. Адрес проверяется по формату выбранной сети — Ethereum (`0x` + 40 hex) или Tron (`T` + 33 base58) — неверный формат = 422, чтобы не уйти на битый адрес при реальной выплате. ```json { "payout_wallet_network": "tron", "payout_wallet_address": "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb" } ``` ### `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[]` выше. ### `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), это меняет и поведение каскадного удаления, не тихая правка. --- ## 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 | Не прошла валидация | | 429 | Превышен лимит запросов — см. `Retry-After` | | 507 | Нода хранения временно переполнена | --- ## 11. Область действия v1 и дорожная карта v1 работает **только с личным пространством пользователя**. Команды (Team) — на очереди следующим шагом, если появится реальный спрос от интеграторов на уровне организации (сейчас у Team в кабинете свои права на папки — переносить их в API раньше, чем кто-то попросил, означало бы усложнять API тем, что никто не использует). Также запланировано (без даты): - Более узкие права токена (`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`) — сделано для десктоп-клиента (`unixanet/backup`). - **Карточная оплата (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` продолжит работать.