Файлы, импорт и настройки
Каждая команда для файлов, которые хранит ящик, импорта старого ящика или аккаунта Resend, календаря, найденного в почте, и настроек рабочего пространства и каждого адреса.
Обзор
Пять пространств имён охватывают то, что окружает саму почту. openemail files это страница «Файлы»: каждое отправленное и полученное вложение и каждый загруженный файл, с его байтами и ссылками на скачивание, в виде которых он уходил. openemail imports переносит старый ящик в один адрес, а openemail provider-imports переносит аккаунт Resend. openemail calendar читает события, найденные в почте, а openemail settings читает и меняет настройки ящика, а также подпись и отслеживание каждого адреса.
- Идентификатор говорит, что он называет:
file_для файла,imp_для импорта ящика,pimp_для импорта из Resend иcal_для события календаря, в каждом случае с 24 шестнадцатеричными символами после префикса. У ссылки на скачивание свой идентификатор, изfiles list-linksилиfiles create-link. --address-idу импорта это идентификатор адреса изopenemail domains list-addresses <id>, а не сам адрес.- Пространства имён откликаются также на
file,import,provider-importилиproviderImportsиsetting. Глаголы принимают обычные псевдонимы там, где они есть:lsдляlist,showиviewдляget,newиaddдляcreate,editдляsettings update, а такжеrm,delиremoveдляfiles delete. - Ни одна из этих команд не спрашивает у входа через браузер код подтверждения. Те, что меняют что-то навсегда, просят подтверждения, а в скрипте за вас отвечает
--yes. openemail <command> --helpперечисляет каждый аргумент и флаг с типом, scope, который нужен вызову, методом и путём и тем, что возвращается. Добавьте--json, чтобы получить ту же страницу как данные.
Файлы
openemail files читает каждый файл, который хранит ящик, и управляет им: пришедшими вложениями, ушедшими вложениями и файлами, загруженными на страницу «Файлы». openemail open files открывает ту же страницу в веб-приложении.
| Команда | Что делает |
|---|---|
| openemail files list | Показать одну страницу файлов с именем, типом, размером, направлением, адресом, на который пришёл каждый, и тем, можно ли его удалить |
| openemail files get <id> | Прочитать один файл с цепочкой и письмом, которым он принадлежит, его usage и deletable |
| openemail files stats | Посчитать файлы и занимаемые ими байты с разбивкой на полученные, отправленные и загруженные, с самыми частыми типами и адресами, последними 30 днями и ещё работающими ссылками |
| openemail files download <id> | Получить байты файла ровно так, как они хранятся, в --out <file> или в stdout при передаче в конвейер |
| openemail files list-links <id> | Показать ссылки на скачивание, в виде которых уходил файл, новые сверху, с тем, сколько раз каждую скачивали и отозвана ли она |
| openemail files create-link <id> | Опубликовать файл по новой публичной ссылке на скачивание, которая открывается без входа |
| openemail files revoke-link <id> <link-id> | Навсегда остановить публичную ссылку, в том числе в уже отправленной почте. Просит подтверждения |
| openemail files upload <data> | Загрузить файл по пути или из stdin через -, до 100 МБ |
| openemail files delete <id> | Навсегда удалить загрузку, от которой ничего не зависит. Просит подтверждения |
| openemail files delete-many <ids...> | Удалить до 100 файлов за один вызов и сообщить, какие оставлены и какие не найдены. Просит подтверждения |
files listсужается через--q(имя и тип),--kind(image,pdf,audio,videoилиtext),--direction(inbound,outboundилиuploaded),--address,--sinceи--untilи сортируется через--sort(newest,oldest,largestилиname).--sinceи--untilпринимают дату или дату со временем в ISO 8601:--sinceвключает свой момент, а--untilостанавливается перед ним.- Удалить можно только загрузку, от которой ничего не зависит, и
deletableсообщает об этом заранее.usageговорит, что удерживает любой другой файл:received,sent,linkedдля загрузки, ссылка на скачивание которой ещё работает, илиscheduledдля файла, прикреплённого к письму, которое ещё не ушло.deleteотклоняет такие с 409file_in_use.delete-manyудаляет то, что может, перечисляет остальное вkeptс причиной для каждого и перечисляет неизвестные идентификаторы вmissing. uploadчитает файл по указанному пути и сохраняет его под именем этого файла с типом, который подразумевает расширение.--filenameи--content-typeих переопределяют. Для stdin через-нужен--filename, а без--content-typeтип будетapplication/octet-stream. Загрузка ждёт до 10 минут, что меняет--timeout-ms.- Идентификатор, который возвращает
upload, прикрепляет файл к отправке как{ "fileId": "…" }вopenemail emails send --attachments, и именно так уходит файл больше встроенного предела в 5 МБ. - Пустой файл это 400
upload_empty, программа или скрипт (судя по имени) это 400upload_dangerous, а файл больше 100 МБ это 413upload_too_large. Рабочее пространство хранит до 10 ГБ загрузок и принимает 500 загрузок в час. downloadв терминале нужен--out <file>, без него она останавливается с кодом выхода2. В конвейере байты идут в stdout, и--out -тоже просит stdout. Весь файл держится в памяти.create-linkпри каждом вызове создаёт новую ссылку со своим счётчиком скачиваний, и ссылка работает, пока её не отзовут или файл не удалят. Она размещается на домене файлов домена из--domain, если он у него есть, напримерfiles.acme.com, иначе на домене файлов адреса файла, иначе на адресе API. Программа или скрипт отклоняется с 422file_unshareable.revoke-linkможно безопасно повторять: уже отозванная ссылка возвращается без изменений.- Ключ или вход, ограниченный определёнными адресами или доменами, видит только файлы, пришедшие на них, и никогда не видит файлы, загруженные для всего рабочего пространства, а его собственные загрузки принадлежат первому выданному ему адресу.
- Индекс файлов начинается со дня своего выпуска. Более старые вложения по-прежнему лежат в своих письмах, где их читает
openemail threads list-attachments <id>.
Импорт почтовых ящиков
openemail imports переносит старый ящик в один адрес: архивы Google Takeout, файлы .mbox из Apple Mail, Thunderbird и большинства настольных приложений, файлы .eml и архивы .zip или .tgz, содержащие любые из них, до 100 ГБ на файл и 50 файлов на импорт. Цепочки, даты и метки переносятся. Импортированная почта ведёт себя тихо: она не запускает правил, пересылок, уведомлений, сводок и вебхуков.
import-files это то, что нужно большинству: она создаёт импорт, загружает каждый файл частями со строкой прогресса и запускает его. Остальные глаголы это те же шаги по одному, чтобы завершить остановившуюся загрузку или вести её самому. create возвращает план, chunkBytes и chunks каждого файла, upload-chunk отправляет одну часть, upload-state говорит, какие части пришли, а start ставит импорт в очередь. openemail open imports показывает те же импорты в веб-приложении.
| Команда | Что делает |
|---|---|
| openemail imports list | Показать одну страницу импортов рабочего пространства, новые сверху, со статусом, прочитанными байтами и счётчиками. --address-id оставляет один адрес |
| openemail imports get <id> | Прочитать один импорт со статусом, прогрессом и счётчиками. Опрашивайте его, пока он не станет completed, failed или cancelled |
| openemail imports create --address-id <value> --files <json|@file|-> | Создать импорт из списка { name, bytes } и получить его план загрузки со status, равным uploading |
| openemail imports upload-state <id> | Для каждого файла индексы частей, которые уже сохранены, чтобы остановившаяся загрузка отправляла только недостающее |
| openemail imports upload-chunk <id> <file> <chunk> <data> | Загрузить одну часть одного файла по пути или из stdin через -. Повторная отправка части заменяет её |
| openemail imports start <id> | Проверить, что пришли все части, распознать формат каждого файла и поставить импорт в очередь |
| openemail imports cancel <id> | Остановить импорт в его следующей контрольной точке. Уже импортированная почта остаётся. Просит подтверждения |
| openemail imports list-failures <id> | Показать каждое письмо или элемент архива, который не перенёсся, с причиной |
| openemail imports delete-upload <id> | Удалить загруженные файлы. Уже импортированная почта остаётся. Просит подтверждения |
| openemail imports import-files --address-id <value> --files <path> | Создать, загрузить и запустить импорт одной командой и вернуться, как только он встанет в очередь |
- Адрес должен быть на собственных доменах рабочего пространства, и ключ или вход должен иметь право действовать от его имени. На адрес выполняется только один импорт: ещё один, пока первый в очереди или выполняется, даёт 409
already_running. import-filesпринимает--filesпо одному разу на файл или несколько путей через запятую и отправляет каждый под его собственным именем.--optionsпринимает JSON, например{"keepInbox":false,"includeSpam":true}. Вcreateте же три это переключатели:--options-keep-inbox,--options-include-spamи--options-include-trash, каждый выключается приставкой--no-, как в--no-options-keep-inbox.keepInboxвключён по умолчанию, поэтому почта из старых входящих попадает во «Входящие» с признаком непрочитанности. Если выключить, всё раскладывается в «Архив». Спам и корзина остаются на месте, если вы их не включите.createпринимает--filesв виде JSON, встроенно или из файла через@path, и каждыйbytesдолжен точно равняться размеру файла, который вы затем загрузите.- Каждая часть имеет длину ровно
chunkBytes, кроме последней части файла, а часть неверной длины даёт 400bad_chunk.startотклоняет файл с недостающими частями с 412missing_chunks, а файл, который не является архивом или почтовым ящиком, с 400unsupported_file. Для импорта, который уже вышел изuploading, она возвращает импорт без изменений. - Если часть по-прежнему не проходит после повторов,
import-filesостанавливается, оставив импорт вuploading. Блок ниже отправляет недостающие части первого файла и запускает импорт. - Импорт сообщает
processedBytesизtotalBytes, а такжеcountsсseen,imported,duplicate,filtered,failedиthreads.lastErrorзадан, только когдаstatusравенfailed. list-failuresдаёт каждой неудачеreason:too-large(больше 50 МБ),unparseable,no-date,storage-error,unreadable-entry,encrypted-entryилиarchive-limit, с темой и отправителем, если их удалось прочитать.cancelдля завершённого импорта даёт 409not_cancellable.delete-uploadотменяет импорт, который ещё загружается, и отклоняет импорт в очереди или в работе с 409still_running, поэтому сначала отмените его.
openemail imports get "$import_id" --json > import.jsonopenemail imports upload-state "$import_id" --json > state.jsonchunk=$(jq .chunkBytes import.json)chunks=$(jq '.files[0].chunks' import.json)for i in $(seq 0 $((chunks - 1))); do jq -e --argjson i "$i" '.received[0] | any(. == $i)' state.json > /dev/null && continue dd if=takeout-001.zip bs="$chunk" skip="$i" count=1 2> /dev/null | openemail imports upload-chunk "$import_id" 0 "$i" -doneopenemail imports start "$import_id"Импорт из Resend
openemail provider-imports переносит рабочее пространство из Resend по одному API-ключу Resend с полным доступом. Подавления сохраняют причину, сегменты становятся аудиториями, подписанные контакты приходят с именами, а шаблоны приходят как HTML-шаблоны с объявленными переменными. Вебхуки регистрируются выключенными, потому что доставки здесь подписываются иначе. Ключ подписи или токен не может покинуть Resend, поэтому домены и API-ключи возвращаются вместо этого списком дел в отчёте.
| Команда | Что делает |
|---|---|
| openemail provider-imports inspect <api-key> | Посчитать, что хранит аккаунт Resend, по ресурсам, до импорта. Ничего не сохраняется, и ключ не хранится |
| openemail provider-imports create --resend-key <value> --resources <a,b> | Поставить в очередь импорт названных ресурсов |
| openemail provider-imports list | Показать одну страницу импортов от провайдеров отправки, новые сверху, каждый с отчётом |
| openemail provider-imports get <id> | Прочитать один импорт: его статус, текущий шаг и отчёт на данный момент |
| openemail provider-imports cancel <id> | Остановить импорт и стереть ключ Resend. То, что уже перенеслось, остаётся. Просит подтверждения |
- Передайте ключ Resend как
@path, чтобы прочитать его из файла, или как-, чтобы прочитать из stdin, и он не попадёт в историю оболочки и список процессов. Ключ, набранный в командной строке, тоже работает, но с предупреждением.--dry-runвыводит тело со скрытым ключом. --resourcesпринимает любые изsuppressions,audiences,contacts,templates,webhooks,domainsиapi-keys, через запятую или повторением.- Ключ только для отправки даёт 400
restricted_key. Создайте для импорта ключ с полным доступом в Resend, а после отзовите его. inspectчитает первую страницу каждого ресурса и возвращаетcounts, по одному{ count, more }на ресурс, гдеmoreзадан, если ресурс выходит за пределы первой страницы.- В рабочем пространстве одновременно выполняется один импорт, а второй даёт 409
already_running. Двойной запуск того же импорта ничего не дублирует. Ключ хранится в зашифрованном виде только пока идёт импорт и стирается, когда он заканчивается, не проходит или отменяется. - Контакты, отписавшиеся в Resend, пропускаются и учитываются в
report.contacts.unsubscribed. report.domainsперечисляет каждый домен Resend сstate:absent, когда его нет в этом рабочем пространстве,added, когда он есть, но не подтверждён, иverified, вместе с записями, которые у него были в Resend. Недостающий добавьте черезopenemail domains create.report.apiKeysназывает ключи, которые нужно создать заново черезopenemail keys create.- При входе через браузер эти команды может использовать только владелец рабочего пространства, а участник получает 403
owner_only. На API-ключ это не распространяется. Ключ или вход, ограниченный определёнными адресами или доменами, не может осмотреть или создать импорт, поскольку аудитории, подавления и шаблоны принадлежат всему рабочему пространству. createнужен также scope каждого ресурса, который он переносит:audiences:writeдля аудиторий,contacts:writeиaudiences:writeдля контактов,templates:writeдля шаблонов,webhooks:writeдля вебхуков иdomains:readдля доменов.
Календарь
openemail calendar читает события календаря, найденные в почте. Все три команды только читают. list-events разворачивает каждое событие в его повторения внутри заданного периода, так что еженедельная встреча это одна строка в неделю, а get-event и get-event-ics читают одно событие целиком. openemail open calendar открывает календарь в веб-приложении.
| Команда | Что делает |
|---|---|
| openemail calendar list-events --from <when> --to <when> | Показать одну страницу повторений между двумя моментами, по времени начала, каждое со ссылкой на своё событие через eventId |
| openemail calendar get-event <id> | Прочитать событие с участниками и их ответами, правилом повторения, исключёнными датами, организатором и напоминаниями |
| openemail calendar get-event-ics <id> | Скачать событие как документ iCalendar, выведенный на экран или записанный в --out <file> |
--fromи--toэто дата и время в ISO 8601, например2026-10-01T00:00:00Z.--toдолжен быть позже--fromи не больше чем на 366 дней, поскольку у повторяющейся серии нет конца, до которого можно перечислять. Отсутствующая граница даёт 422invalid_parameter. Граница, которая не разбирается, перевёрнутый или слишком длинный период или неизвестный часовой пояс дают 400invalid_parameter.--timezoneэто часовой пояс IANA, напримерEurope/London, а если его не указать, UTC. Он разворачивает события, сохранённые без собственного часового пояса, и решает, на какой день попадёт событие на весь день.get-eventиget-event-icsпринимаютeventIdповторения, а неuidiCalendar. Повторяющаяся серия это одно событие, поэтому еёstartиendописывают первый экземпляр.- Отменённые события тоже перечисляются, со
status, равнымCANCELLED.myPartstatэто ответ, записанный для любого адреса, который рабочее пространство держит на своих доменах, и null, если ни один из них не участник. - Страницы продолжаются через
--cursorс тем же периодом. Ключ или вход, ограниченный определёнными адресами, видит только события, которые один из них организует или посещает, а любое другое событие даёт 404. get-event-icsобъявляетMETHOD:PUBLISH, поэтому при импорте событие показывается как есть, без предложения принять или отклонить. События со временем записываются как моменты в UTC, а события на весь день как даты.DTSTAMPэто время скачивания, поэтому два скачивания неизменного события различаются этой строкой.
Настройки
openemail settings читает и меняет настройки ящика, такие как часовой пояс, язык, формат времени и списки блокировки. Четыре настройки принадлежат адресу, а не рабочему пространству: signature, openEmailSignature (футер OpenEmail), trackOpens и trackClicks. Назовите адрес через --address или *@acme.com для catch-all домена.
| Команда | Что делает |
|---|---|
| openemail settings get | Прочитать все настройки с подставленными значениями по умолчанию. С --address четыре настройки адреса читаются так, как их определяет отправка с этого адреса |
| openemail settings update | Изменить переданные настройки, остальные сохранить. Ответ содержит все настройки, прочитанные после записи |
- Без
--addressgetчитает четыре настройки адреса как встроенные значения по умолчанию: без подписи, футер OpenEmail включён, отслеживание открытий и ссылок включено. С ним они берутся из собственных значений адреса, затем из catch-all его домена, если этот адрес подхватил catch-all, затем из значений по умолчанию. Плюс-адрес без собственных настроек читает настройки своего основного адреса. - Без
--addressupdateотклоняет--signature,--open-email-signature,--track-opensи--track-clicksс 422address_requiredи ничего не записывает. С нимupdateпринимает только эти четыре, а всё остальное даёт 422not_per_address. Адрес должен быть в этом рабочем пространстве или быть*@domainдля подтверждённого здесь домена с включённым catch-all. - Переключатели выключаются приставкой
--no-:--no-open-email-signature,--no-track-opensи--no-track-clicks. --signatureэто HTML длиной не более 150 000 символов, очищаемый при записи, так что возвращается именно то, что будет отправлено. Пустая строка удаляет подпись. Добавит ли её отправка, решает отправка: см.--signatureвopenemail emails send --help.- Флаги покрывают частые поля.
--dataпринимает любую сохранённую настройку в виде JSON, встроенно, из файла через@pathили из stdin через-, напримерtimeFormat,weekStart,undoSendEnabled,blockedSendersилиblockedDomains. Ключи, которых настройки не знают, отбрасываются без ошибки, а известный ключ с неверным типом даёт 422invalid_parameter. - Список, такой как
blockedDomains, заменяется целиком, поэтому сначала прочитайте его и отправьте обратно со своим изменением, как в примере ниже. Запись списка блокировки, слишком короткая, чтобы быть правилом, даёт 422blocklist_entry_too_broad. --timezoneи--languageсохраняются как есть, без проверки, поэтому проверьте часовой пояс или код языка до отправки.- Поля конфиденциальности,
externalImages,trustedSenders,blockedSenders,blockedDomains,blockedWordsиuseDefaultBlockedWords, принадлежат рабочему пространству. Ключ или вход, ограниченный определёнными адресами или доменами, не может их менять и может назвать в--addressтолько выданный ему адрес или catch-all на домене, который выдан ему целиком. Любой из этих отказов даёт 422capability_unsupported. - Обновление без содержимого ничего не меняет и возвращает текущие настройки.
defaultEmailAliasтолько заранее выбирает адрес From в редакторе веб-приложения, и API никогда не использует его, чтобы выбрать отправителя.
Примеры
file_id=$(openemail files upload ./price-list.pdf --json | jq -r .id)openemail files create-link "$file_id" --domain acme.com --json | jq -r .urlopenemail files list-links "$file_id"openemail files download "$file_id" --out copy.pdfКаждый create-link создаёт новую ссылку, поэтому запустите его один раз и читайте ссылку через list-links. revoke-link позже останавливает её, в том числе в уже ушедшей почте.
openemail files list --direction uploaded --all | jq -r 'select(.deletable) | .id' > deletable.txtwc -l < deletable.txtxargs -r -n 100 openemail files delete-many --yes --json < deletable.txt | jq -c '.kept[]'С --all в конвейере files list выводит по одному файлу в строке. xargs -n 100 передаёт delete-many не больше 100 идентификаторов за вызов, а файл, который тем временем начал использоваться, возвращается в kept с причиной, а не проваливает вызов.
domain_id=$(openemail domains list --all | jq -r 'select(.domain == "acme.com") | .id')address_id=$(openemail domains list-addresses "$domain_id" --all | jq -r 'select(.address == "[email protected]") | .id')openemail imports import-files --address-id "$address_id" --files ~/Downloads/takeout-001.zip --files ~/Downloads/takeout-002.zip --json > import.jsonimport_id=$(jq -r .id import.json)until openemail imports get "$import_id" --json | jq -e '.status != "queued" and .status != "running"' > /dev/null; do sleep 60doneopenemail imports get "$import_id" --json | jq '{status, counts, lastError}'openemail imports list-failures "$import_id" --json | jq -r '.data[] | [.reason, .subject] | @tsv'--dry-run у import-files выводит POST /imports, который она отправила бы, с именем и размером каждого файла, и ничего не загружает. list-failures читает по 100: передайте выведенный nextCursor как --after для следующей страницы.
openemail provider-imports inspect @resend-key.txtopenemail provider-imports create --resend-key @resend-key.txt --resources suppressions,audiences,contacts,templates,webhooks,domains,api-keys --json > resend.jsonpimp_id=$(jq -r .id resend.json)until openemail provider-imports get "$pimp_id" --json | jq -e '.status != "queued" and .status != "running"' > /dev/null; do sleep 10doneopenemail provider-imports get "$pimp_id" --json | jq '.report | {contacts, domains: [.domains[] | {name, state}], apiKeys: [.apiKeys[].name]}'openemail calendar list-events --from 2026-10-01T00:00:00Z --to 2026-11-01T00:00:00Z --timezone Europe/London --all | jq -r 'select(.status != "CANCELLED") | [.start, .summary, .myPartstat] | @tsv'openemail calendar get-event cal_8d3f0a2b9c4e41f7a6b5c2d1 --json | jq '.attendees[] | {email, partstat}'openemail calendar get-event-ics cal_8d3f0a2b9c4e41f7a6b5c2d1 --out standup.icsopenemail settings get --address [email protected] --json | jq '{signature, openEmailSignature, trackOpens, trackClicks}'openemail settings update --address [email protected] --signature "$(cat signature.html)" --no-track-clicks --dry-runopenemail settings update --address [email protected] --signature "$(cat signature.html)" --no-track-clicksopenemail settings update --address '*@acme.com' --no-track-opensopenemail settings update --timezone Europe/London --language enopenemail settings get --json | jq -c '{blockedDomains: ((.blockedDomains + ["spam.example"]) | unique)}' | openemail settings update --data -Пробный запуск выводит PATCH /settings?address=sales%40acme.com и его тело, не отправляя его. Строка с catch-all задаёт отслеживание для каждого адреса, который подхватывает catch-all acme.com, а последняя строка добавляет один домен в список блокировки, не убирая остальные.
Scope, подтверждения и ошибки
| Scope | Команды |
|---|---|
| files:read | files list, get, stats, download, list-links |
| files:write | files create-link, revoke-link, upload, delete, delete-many |
| threads:read | imports list, get, list-failures |
| threads:write | imports create, upload-state, upload-chunk, start, cancel, delete-upload, import-files |
| settings:read | provider-imports list, get, а также settings get |
| settings:write | provider-imports inspect, create, cancel, а также settings update |
| calendar:read | calendar list-events, get-event, get-event-ics |
- Вход или ключ без нужного scope останавливается с кодом выхода
4, называет недостающий scope и объясняет, как его получить. files revoke-link,files delete,files delete-many,imports cancel,imports delete-uploadиprovider-imports cancelпросят подтверждения. Ответ «нет» завершает их с кодом10и ничего не меняет. Без присмотра и без--yesони останавливаются с кодом выхода2до любой отправки.- Ни одна команда здесь не спрашивает код подтверждения, ни при входе через браузер, ни с API-ключом, поэтому скрипту нужен только
--yes. --dry-runвыводит запрос, который отправило бы изменение, с его телом и завершается с кодом0, не отправляя его и не прося подтверждения. Тело-файл показывается своим размером и типом.files list,files list-links,imports list,provider-imports listиcalendar list-eventsчитают одну страницу:--limitпринимает от 1 до 100, а если его не указать, сервер отдаёт 25, и--cursorпринимаетnextCursorпредыдущей страницы.--allчитает все страницы,--max <n>останавливается после стольких элементов, а--ndjsonили--allв конвейере выводит по одному JSON-объекту в строке. С--jsonони выводят один документ{ items, hasMore, nextCursor }.imports list-failuresлистает по-своему:--afterпринимаетnextCursorпредыдущей страницы,--limitне больше 100, что и есть значение по умолчанию, а--allнет. С--jsonона выводит{ object, data, nextCursor }.- Чтения и изменения, которые дважды дают один и тот же результат, повторяются после сбоя сети:
files revoke-link,imports upload-chunk,imports startиsettings update. Любое другое изменение отправляется один раз. После потерянного ответа посмотрите, прежде чем пробовать снова: второйuploadсохраняет вторую копию, а второйcreate-linkсоздаёт вторую ссылку, 404 на вашем второмfiles deleteзначит, что первый сработал, а второйdelete-manyперечисляет вmissingто, что удалил первый. - Отказ завершается кодом своего статуса:
4для 403,5для 404,6для 409,7для 400 или 422 и8для 429.