> ## 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.

# Uploading a file

> A file of any size: create a recording, upload the parts to storage, confirm the upload.

The file does not go to the API. It goes straight to object storage through temporary links. The API issues the links and accepts the confirmation.

<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">Your service</div>
    <div className="py-2.5">AiGenda API</div>
    <div className="py-2.5">Storage</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">part upload URLs</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 part</div><div className="text-xs text-gray-500 dark:text-gray-400">to 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">in the response header</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 and 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">until 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">Your service → AiGenda API</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">AiGenda API → Your service</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">part upload URLs</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">Your service → Storage</div>
      <div className="mt-0.5 font-mono text-xs text-gray-900 dark:text-gray-100">PUT part</div>
      <div className="text-xs text-gray-500 dark:text-gray-400">to 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">Storage → Your service</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">in the response header</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">Your service → AiGenda API</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 and 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">Your service → AiGenda API</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">until is\_uploaded: true</div>
    </div>
  </div>
</div>

## Steps

<Steps>
  <Step title="Create a recording">
    ```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": "Sales team standup.mp3", "size": 18874368, "content_type": "audio/mpeg"}'
    ```

    | Field | Rule |
    | - | - |
    | `title` | Up to 255 characters. **Ends with the file extension**: `.mp3`, `.m4a`, `.wav`, `.ogg`, `.webm`, `.mp4`. AiGenda detects the format by the extension |
    | `size` | Exact file size in bytes |
    | `content_type` | MIME type: `audio/mpeg`, `audio/mp4`, `audio/wav`, `audio/ogg`, `audio/webm`, `video/mp4` |

    ```json Response 201 theme={null}
    {
      "id": 51234,
      "title": "Sales team standup.mp3",
      "url": [
        "https://storage.yandexcloud.net/aigenda/3f6c1e0a.mp3?partNumber=1&uploadId=…&X-Amz-Signature=…",
        "https://storage.yandexcloud.net/aigenda/3f6c1e0a.mp3?partNumber=2&uploadId=…&X-Amz-Signature=…"
      ],
      "created_at": "2026-10-17T12:30:00.481233+03:00",
      "duration": null,
      "size": 18874368,
      "content_type": "audio/mpeg",
      "space_id": 1024,
      "is_uploaded": false,
      "was_classified": false,
      "result_is_ready": false
    }
    ```

    `url` is always an array: one link per file part, each part about 10 MB. So you usually get several links: an 18 MB file gets two.
  </Step>

  <Step title="Upload the parts">
    Split the file into as many parts as there are links:

    * part size: `ceil(size / len(url))` bytes, the last part is shorter;
    * part number `i` (starting from one) goes to the link `url[i-1]`;
    * method `PUT`, request body: the part's bytes, no JSON and no multipart form;
    * save the `ETag` header from the storage response together with its double quotes: `"9b2cf535f27731c974343645a3985328"`.

    The links live for **5 hours** after the recording is created. The storage allows browser requests (CORS is open and `ETag` is readable), so you can upload from a frontend too.
  </Step>

  <Step title="Complete the upload">
    Pass the part numbers and their `ETag` values in the order you uploaded them.

    ```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\""}]}'
    ```

    Response: `200` with an empty body.
  </Step>

  <Step title="Wait for confirmation">
    Poll [`info/`](/en/api-reference/audio/info) every 2 seconds until you get `is_uploaded: true`. It usually takes less than a second. Then [start processing](/en/processing/start).
  </Step>
</Steps>

## Ready-to-use code

<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:
      """Uploads a file to a space and returns audio_id once AiGenda has confirmed the file."""
      headers = {"Authorization": f"Bearer {token}"}
      size = os.path.getsize(path)
      title = os.path.basename(path)  # name with extension, e.g. "Standup.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 did not confirm the file within 2 minutes")
  ```

  ```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";

  // Uploads a file to a space and returns audioId once AiGenda has confirmed the file.
  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 did not confirm the file within 2 minutes");
  }
  ```
</CodeGroup>

## If something goes wrong

| Situation | What to do |
| - | - |
| Storage responded `403` to `PUT` | The links have expired (more than 5 hours have passed). Create the recording again and [delete](/en/api-reference/audio/delete) the old one |
| No response to the create request (timeout, dropped connection) | Do not blindly create the recording a second time: every call adds a new recording. Find it by title, see [Reliable integration](/en/reference/best-practices) |
| A part failed to upload | Retry `PUT` for that part to the same link while the links are still valid |


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