> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aigenda.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Загрузка файла

> Файл любого размера: создаём запись, грузим части в хранилище, подтверждаем загрузку.

Файл уходит не в API, а прямо в объектное хранилище по временным ссылкам. API выдаёт ссылки и принимает подтверждение.

<div className="not-prose my-6 hidden overflow-hidden sm:block rounded-2xl border border-gray-200 bg-white dark:border-white/10 dark:bg-white/5">
  <div className="grid grid-cols-3 border-b border-gray-200 bg-gray-50 text-center text-xs font-semibold text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-300">
    <div className="py-2.5">Ваш сервис</div>
    <div className="py-2.5">API AiGenda</div>
    <div className="py-2.5">Хранилище</div>
  </div>

  <div className="relative">
    <div className="absolute inset-y-0 w-px bg-gray-200 dark:bg-white/10" style={{ left: "16.667%" }} />

    <div className="absolute inset-y-0 w-px bg-gray-200 dark:bg-white/10" style={{ left: "50.000%" }} />

    <div className="absolute inset-y-0 w-px bg-gray-200 dark:bg-white/10" style={{ left: "83.333%" }} />

    <div className="relative h-16">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>1</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "33.333%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">POST /audio/</div><div className="text-xs text-gray-500 dark:text-gray-400">title, size, content\_type</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-primary dark:text-primary-light" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(33.333% - 14px)" }}>
        <div style={{ position: "absolute", right: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderLeft: "7px solid currentColor" }} />
      </div>
    </div>

    <div className="relative h-16 border-t border-gray-100 dark:border-white/5">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>2</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "33.333%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">id, url\[]</div><div className="text-xs text-gray-500 dark:text-gray-400">ссылки на части</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-gray-400 dark:text-gray-500" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(33.333% - 14px)" }}>
        <div style={{ position: "absolute", left: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderRight: "7px solid currentColor" }} />
      </div>
    </div>

    <div className="relative h-16 border-t border-gray-100 dark:border-white/5">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>3</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "50.000%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">PUT часть</div><div className="text-xs text-gray-500 dark:text-gray-400">по ссылке url\[i]</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-primary dark:text-primary-light" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(66.667% - 14px)" }}>
        <div style={{ position: "absolute", right: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderLeft: "7px solid currentColor" }} />
      </div>
    </div>

    <div className="relative h-16 border-t border-gray-100 dark:border-white/5">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>4</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "50.000%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">ETag</div><div className="text-xs text-gray-500 dark:text-gray-400">в заголовке ответа</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-gray-400 dark:text-gray-500" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(66.667% - 14px)" }}>
        <div style={{ position: "absolute", left: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderRight: "7px solid currentColor" }} />
      </div>
    </div>

    <div className="relative h-16 border-t border-gray-100 dark:border-white/5">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>5</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "33.333%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">POST /complete\_upload/</div><div className="text-xs text-gray-500 dark:text-gray-400">PartNumber и ETag</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-primary dark:text-primary-light" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(33.333% - 14px)" }}>
        <div style={{ position: "absolute", right: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderLeft: "7px solid currentColor" }} />
      </div>
    </div>

    <div className="relative h-16 border-t border-gray-100 dark:border-white/5">
      <div className="absolute left-3 top-5"><div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>6</div></div>
      <div className="absolute whitespace-nowrap text-center" style={{ left: "33.333%", top: 8, transform: "translateX(-50%)" }}><div className="font-mono text-xs text-gray-900 dark:text-gray-100">GET /info/</div><div className="text-xs text-gray-500 dark:text-gray-400">до is\_uploaded: true</div></div>

      <div className="absolute h-0.5 rounded-full bg-current text-primary dark:text-primary-light" style={{ top: 48, left: "calc(16.667% + 6px)", width: "calc(33.333% - 14px)" }}>
        <div style={{ position: "absolute", right: -7, top: -4, width: 0, height: 0, borderTop: "5px solid transparent", borderBottom: "5px solid transparent", borderLeft: "7px solid currentColor" }} />
      </div>
    </div>
  </div>
</div>

<div className="not-prose my-6 sm:hidden rounded-2xl border border-gray-200 bg-white dark:border-white/10 dark:bg-white/5">
  <div className="flex items-start gap-3 px-4 py-3">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>1</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">Ваш сервис → API AiGenda</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">POST /audio/</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">title, size, content\_type</div>
    </div>
  </div>

  <div className="flex items-start gap-3 px-4 py-3 border-t border-gray-100 dark:border-white/5">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>2</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">API AiGenda → Ваш сервис</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">id, url\[]</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">ссылки на части</div>
    </div>
  </div>

  <div className="flex items-start gap-3 px-4 py-3 border-t border-gray-100 dark:border-white/5">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>3</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">Ваш сервис → Хранилище</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">PUT часть</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">по ссылке url\[i]</div>
    </div>
  </div>

  <div className="flex items-start gap-3 px-4 py-3 border-t border-gray-100 dark:border-white/5">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>4</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">Хранилище → Ваш сервис</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">ETag</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">в заголовке ответа</div>
    </div>
  </div>

  <div className="flex items-start gap-3 px-4 py-3 border-t border-gray-100 dark:border-white/5">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>5</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">Ваш сервис → API AiGenda</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">POST /complete\_upload/</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">PartNumber и ETag</div>
    </div>
  </div>

  <div className="flex items-start gap-3 px-4 py-3 border-t border-gray-100 dark:border-white/5">
    <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white" style={{ background: "linear-gradient(135deg, #9FB6FF, #7C86F0)" }}>6</div>

    <div className="min-w-0">
      <div className="text-xs text-gray-500 dark:text-gray-400">Ваш сервис → API AiGenda</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">GET /info/</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">до is\_uploaded: true</div>
    </div>
  </div>
</div>

## Шаги

<Steps>
  <Step title="Создайте запись">
    ```bash theme={null}
    curl -X POST https://api.aigenda.tech/api/spaces/1024/audio/ \
      -H "Authorization: Bearer <access>" \
      -H "Content-Type: application/json" \
      -d '{"title": "Планёрка отдела продаж.mp3", "size": 18874368, "content_type": "audio/mpeg"}'
    ```

    | Поле | Правило |
    | - | - |
    | `title` | До 255 символов. **Заканчивается расширением файла**: `.mp3`, `.m4a`, `.wav`, `.ogg`, `.webm`, `.mp4`. По расширению AiGenda определяет формат |
    | `size` | Точный размер файла в байтах |
    | `content_type` | MIME-тип: `audio/mpeg`, `audio/mp4`, `audio/wav`, `audio/ogg`, `audio/webm`, `video/mp4` |

    ```json Ответ 201 theme={null}
    {
      "id": 51234,
      "title": "Планёрка отдела продаж.mp3",
      "url": [
        "https://storage.yandexcloud.net/aigenda/3f6c1e0a.mp3?partNumber=1&uploadId=…&X-Amz-Signature=…"
      ],
      "created_at": "2026-10-17T09:30:00Z",
      "duration": null,
      "size": 18874368,
      "content_type": "audio/mpeg",
      "space_id": 1024,
      "is_uploaded": false,
      "was_classified": false,
      "result_is_ready": false
    }
    ```

    `url` всегда массив: по ссылке на каждую часть файла, примерно одна часть на 1 ГБ.
  </Step>

  <Step title="Загрузите части">
    Разделите файл на столько частей, сколько пришло ссылок:

    * размер части: `ceil(size / len(url))` байт, последняя часть короче;
    * часть с номером `i` (с единицы) уходит на ссылку `url[i-1]`;
    * метод `PUT`, тело запроса: байты части, без JSON и multipart-формы;
    * из ответа хранилища сохраните заголовок `ETag` вместе с двойными кавычками: `"9b2cf535f27731c974343645a3985328"`.

    Ссылки живут **5 часов** с момента создания записи. Хранилище разрешает запросы из браузера (CORS открыт, `ETag` читается), так что грузить можно и с фронтенда.
  </Step>

  <Step title="Завершите загрузку">
    Передайте номера частей и их `ETag` в том порядке, в котором грузили.

    ```bash theme={null}
    curl -X POST https://api.aigenda.tech/api/spaces/1024/audio/51234/complete_upload/ \
      -H "Authorization: Bearer <access>" \
      -H "Content-Type: application/json" \
      -d '{"parts": [{"PartNumber": 1, "ETag": "\"9b2cf535f27731c974343645a3985328\""}]}'
    ```

    Ответ: `200` с пустым телом.
  </Step>

  <Step title="Дождитесь подтверждения">
    Опрашивайте [`info/`](/api-reference/audio/info) раз в 2 секунды, пока не придёт `is_uploaded: true`. Обычно это меньше секунды. После этого [запускайте обработку](/processing/start).
  </Step>
</Steps>

## Готовый код

<CodeGroup>
  ```python Python theme={null}
  import math
  import os
  import time

  import requests

  API = "https://api.aigenda.tech"


  def upload_file(token: str, space_id: int, path: str, content_type: str) -> int:
      """Загружает файл в пространство и возвращает audio_id, когда AiGenda подтвердила файл."""
      headers = {"Authorization": f"Bearer {token}"}
      size = os.path.getsize(path)
      title = os.path.basename(path)  # имя с расширением, например «Планёрка.mp3»

      r = requests.post(
          f"{API}/api/spaces/{space_id}/audio/",
          headers=headers,
          json={"title": title, "size": size, "content_type": content_type},
          timeout=30,
      )
      r.raise_for_status()
      audio = r.json()
      urls = audio["url"]

      part_size = math.ceil(size / len(urls))
      parts = []
      with open(path, "rb") as f:
          for number, url in enumerate(urls, start=1):
              chunk = f.read(part_size)
              put = requests.put(url, data=chunk, timeout=900)
              put.raise_for_status()
              parts.append({"PartNumber": number, "ETag": put.headers["ETag"]})

      r = requests.post(
          f"{API}/api/spaces/{space_id}/audio/{audio['id']}/complete_upload/",
          headers=headers,
          json={"parts": parts},
          timeout=30,
      )
      r.raise_for_status()

      for _ in range(60):
          info = requests.get(
              f"{API}/api/spaces/{space_id}/audio/{audio['id']}/info/",
              headers=headers,
              timeout=30,
          ).json()
          if info.get("is_uploaded"):
              return audio["id"]
          time.sleep(2)
      raise TimeoutError("AiGenda не подтвердила файл за 2 минуты")
  ```

  ```javascript JavaScript (Node.js 18+) theme={null}
  import { open, stat } from "node:fs/promises";
  import { basename } from "node:path";

  const API = "https://api.aigenda.tech";

  // Загружает файл в пространство и возвращает audioId, когда AiGenda подтвердила файл.
  export async function uploadFile(token, spaceId, path, contentType) {
    const auth = { Authorization: `Bearer ${token}` };
    const json = { ...auth, "Content-Type": "application/json" };
    const { size } = await stat(path);

    const created = await fetch(`${API}/api/spaces/${spaceId}/audio/`, {
      method: "POST",
      headers: json,
      body: JSON.stringify({ title: basename(path), size, content_type: contentType }),
    });
    if (!created.ok) throw new Error(`create: ${created.status}`);
    const audio = await created.json();

    const partSize = Math.ceil(size / audio.url.length);
    const parts = [];
    const file = await open(path);
    try {
      for (let i = 0; i < audio.url.length; i++) {
        const length = Math.min(partSize, size - i * partSize);
        const chunk = Buffer.alloc(length);
        await file.read(chunk, 0, length, i * partSize);
        const put = await fetch(audio.url[i], { method: "PUT", body: chunk });
        if (!put.ok) throw new Error(`part ${i + 1}: ${put.status}`);
        parts.push({ PartNumber: i + 1, ETag: put.headers.get("ETag") });
      }
    } finally {
      await file.close();
    }

    const done = await fetch(`${API}/api/spaces/${spaceId}/audio/${audio.id}/complete_upload/`, {
      method: "POST",
      headers: json,
      body: JSON.stringify({ parts }),
    });
    if (!done.ok) throw new Error(`complete: ${done.status}`);

    for (let attempt = 0; attempt < 60; attempt++) {
      const res = await fetch(`${API}/api/spaces/${spaceId}/audio/${audio.id}/info/`, { headers: auth });
      const info = await res.json();
      if (info.is_uploaded) return audio.id;
      await new Promise((resolve) => setTimeout(resolve, 2000));
    }
    throw new Error("AiGenda не подтвердила файл за 2 минуты");
  }
  ```
</CodeGroup>

## Если что-то пошло не так

| Ситуация | Что делать |
| - | - |
| Хранилище ответило `403` на `PUT` | Ссылки истекли (прошло больше 5 часов). Создайте запись заново, старую [удалите](/api-reference/audio/delete) |
| Ответ на создание записи не пришёл (таймаут, обрыв) | Не создавайте запись вслепую второй раз: каждый вызов добавляет новую запись. Найдите её по названию, см. [Надёжная интеграция](/reference/best-practices) |
| Часть не загрузилась | Повторите `PUT` этой части по той же ссылке, пока ссылки не истекли |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.