> For the complete documentation index, see [llms.txt](https://wiki.indeepa.com/repricer/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wiki.indeepa.com/repricer/vzaimodeistvie-s-indeepa-cherez-api/kak-skachivat-dannye-iz-otchyotov-indeepa-po-api.md).

# Как скачивать данные из отчётов INDEEPA по API

Эта инструкция универсальна. Неважно, для какого маркетплейса вы используете репрайсер — Ozon, Wildberries или Яндекс Маркет. Общая структура API-запроса одинакова для разных отчётов, но набор дополнительных параметров и состав возвращаемых данных могут отличаться.

**API** — это «окно» в систему INDEEPA, через которое можно автоматически получать данные отчётов без ручного экспорта. Его можно использовать в своих программах (Python, PowerShell, cURL и др.) для автоматической выгрузки данных, интеграции с дашбордами или анализа.

### Шаг 1. Возьмите свою ссылку из INDEEPA

1. Перейдите в раздел **«Отчёты»** и выберите нужный отчёт.

<figure><img src="https://770539834-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FC4bKZVtgehGGY1O19rud%2Fuploads%2FaiESALuArZkBEEGacSsO%2F%D0%A0%D0%B8%D1%81%D1%83%D0%BD%D0%BE%D0%BA4.png?alt=media&amp;token=a316a46a-abd6-4837-82a4-d599fa0f5634" alt=""><figcaption></figcaption></figure>

2. Для примера используется «Отчет по маржинальности Ozon». В параметрах отчёта укажите период, за который нужны данные, и нажмите кнопку **«Применить»**, чтобы отчёт обновился.

<figure><img src="https://770539834-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FC4bKZVtgehGGY1O19rud%2Fuploads%2FypCVzmoRfzJjycD8myQl%2F%D0%A0%D0%B8%D1%81%D1%83%D0%BD%D0%BE%D0%BA1.png?alt=media&amp;token=9a0fc27f-c646-4a35-b7f1-cdd92f9a8511" alt=""><figcaption></figcaption></figure>

Если отчёт использует параметры периода, даты можно позже изменить прямо в API-ссылке — об этом в Шаге 2.

3. После того как отчёт сформировался, нажмите в верхней части страницы кнопку **«Ссылка на API отчёта»**.

<figure><img src="https://770539834-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FC4bKZVtgehGGY1O19rud%2Fuploads%2FknnE2uv5bfjW2Hkk8HR5%2F%D0%A0%D0%B8%D1%81%D1%83%D0%BD%D0%BE%D0%BA2.png?alt=media&amp;token=887c005b-e96b-45f4-b52f-0855771e5a30" alt=""><figcaption></figcaption></figure>

4. В браузере автоматически откроется новая вкладка, и в ней начнёт загружаться информация. Рабочая ссылка для автоматизации — это **полный URL** из адресной строки той новой вкладки, которая открылась после нажатия кнопки. Именно её вы будете использовать в скриптах, Excel или других инструментах.

<figure><img src="https://770539834-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FC4bKZVtgehGGY1O19rud%2Fuploads%2Fhldf2JGh6O2n3oZgov5a%2F%D0%A0%D0%B8%D1%81%D1%83%D0%BD%D0%BE%D0%BA3.png?alt=media&amp;token=00f4d3d0-f286-44d6-8bc7-72d569af95d5" alt=""><figcaption></figcaption></figure>

#### Как получить свои userid, reportid и token

Эти значения нельзя сгенерировать или придумать самостоятельно через API. Все значения уже содержатся в API-ссылке, открытой после нажатия кнопки «Ссылка на API отчёта». Отдельно искать или создавать их не нужно.

* **userid** – ваш идентификатор пользователя. Обычно отображается в URL после входа в систему или в настройках профиля.
* **reportid** – идентификатор отчёта. Откройте нужный отчёт в INDEEPA; его ID часто виден в адресной строке браузера.
* **token** – ключ авторизации. Он автоматически генерируется в ссылке при запросе скачивания именно этого отчёта.

{% hint style="danger" %}
Не публикуйте API-ссылку и не передавайте её третьим лицам: `token` и `userid` могут предоставить доступ к данным отчёта. Храните ссылку в защищённом месте.\
Полную ссылку можно отправить сотруднику поддержки только по его прямому запросу через официальный канал INDEEPA.
{% endhint %}

### Шаг 2. Разбираемся с параметрами ссылки

**Пример того, как выглядит ссылка:**

<https://api.indeepa.com/prod/analytics2/getreportdatabyid?**reportid**=66ddd377-9319-4639-b016-3b8ffd1e9992&**userid**=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&**token**=5ae7f258-хххх-хххх-хххххх-e35хххххххd9b&**skip**=0&**take**=100&**parameters.varDateFrom**=2026-06-01&**parameters.varDateTo**=2026-06-07>

В примере выше параметры выделены только для вашей визуализации — чтобы вы сразу видели, о какой части ссылки пойдёт речь ниже.&#x20;

Все настройки выгрузки передаются в конце ссылки, после знака **«?»**. Каждый параметр отделён символом **«&»**.

<table data-header-hidden data-search="false"><thead><tr><th valign="top"></th><th valign="top"></th><th valign="top"></th><th valign="top"></th></tr></thead><tbody><tr><td valign="top"><strong>ПАРАМЕТР</strong></td><td valign="top"><strong>ЧТО ОЗНАЧАЕТ</strong></td><td valign="top"><strong>КАК МЕНЯТЬ</strong></td><td valign="top"><strong>ПРИМЕР</strong></td></tr><tr><td valign="top"><strong>reportid</strong></td><td valign="top">Указывает, какой именно отчёт выгружать</td><td valign="top">Не меняйте — берите из своей ссылки</td><td valign="top">reportid=a1b2c3d4-...</td></tr><tr><td valign="top"><strong>userid</strong></td><td valign="top">Ваш идентификатор в системе INDEEPA</td><td valign="top">Не меняйте — берите из своей ссылки</td><td valign="top">userid=e5f6g7h8-...</td></tr><tr><td valign="top"><strong>token</strong></td><td valign="top">Ключ доступа к отчёту</td><td valign="top">Не меняйте, <strong>храните в секрете</strong></td><td valign="top">token=x9y8z7w6-...</td></tr><tr><td valign="top"><strong>skip</strong></td><td valign="top">Сколько записей пропустить (для постраничной выгрузки)</td><td valign="top">Следующий <code>skip</code> равен предыдущему <code>skip</code> плюс <code>take</code>. Например, при <code>take=1000</code>: <code>0</code>, <code>1000</code>, <code>2000</code>.</td><td valign="top">skip=0</td></tr><tr><td valign="top"><strong>take</strong></td><td valign="top">Сколько записей получить за один раз</td><td valign="top">Максимальное значение — <code>1000</code></td><td valign="top">take=1000</td></tr></tbody></table>

#### **Дополнительные параметры конкретного отчёта**

Набор дополнительных параметров зависит от выбранного отчёта. Такие параметры не являются обязательными и могут отсутствовать в API-ссылке.

Например, отчёт с выбором периода может содержать:

* `parameters.varDateFrom` — дата начала периода;
* `parameters.varDateTo` — дата окончания периода.

В ссылке могут присутствовать дополнительные параметры конкретного отчёта, например идентификатор кабинета `parameters.cabinetId=123`, категория, бренд или другие фильтры. Не удаляйте неизвестные параметры: они могут влиять на состав выгрузки. Меняйте только те параметры, которые вам действительно нужно изменить (чаще всего — даты или пагинацию).

Если назначение параметра непонятно, обратитесь в [службу поддержки](https://wiki.indeepa.com/2.0/sluzhba-podderzhki/kak-obratitsya-v-sluzhbu-podderzhki). Укажите название параметра и приложите скриншот ссылки, предварительно скрыв `token`, `userid` и другие конфиденциальные значения. Полную API-ссылку отправляйте только по прямому запросу специалиста через согласованный канал.

Если отчёт использует параметры `parameters.varDateFrom` и `parameters.varDateTo`, даты указываются в формате `ГГГГ-ММ-ДД`. Например: `2026-05-01` и `2026-05-31`.

### Шаг 3. Как скачать данные

Выберите удобный способ в зависимости от ваших задач:

* **Через браузер:** вставьте ссылку в адресную строку, нажмите Enter. API возвращает данные в формате JSON. Браузер может показать JSON на странице или предложить сохранить его в файл. Для получения CSV данные нужно дополнительно преобразовать, например через PowerShell, Python или Excel. Подходит для разовых выгрузок.
* **Через Excel** (Power Query): Данные → Получить данные → Из веб → вставьте ссылку → Загрузить. Удобно для регулярного обновления отчётов одной кнопкой.
* **Через код** (Python, cURL, PowerShell): для автоматизации и интеграции с внешними системами.

#### Примеры получения данных через код

{% hint style="danger" %}
**Важно:** API-ссылка содержит `token` и `userid`. Не публикуйте её, не передавайте третьим лицам и не сохраняйте в открытых репозиториях.

Во всех примерах ниже замените `ВАША_ПОЛНАЯ_API_ССЫЛКА` на ссылку, скопированную из отчёта INDEEPA. Без этой замены код не сможет получить данные.
{% endhint %}

***

#### cURL: скачать одну страницу в JSON — macOS и Linux

{% hint style="info" %}
Замените `ВАША_ПОЛНАЯ_API_ССЫЛКА` на ссылку, скопированную из отчёта INDEEPA.
{% endhint %}

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```bash
#!/usr/bin/env bash

set -euo pipefail

# Вставьте полную API-ссылку между кавычками.
API_URL="ВАША_ПОЛНАЯ_API_ССЫЛКА"

if [[ "$API_URL" == "ВАША_ПОЛНАЯ_API_ССЫЛКА" ]]; then
    echo "Сначала вставьте полную API-ссылку в переменную API_URL." >&2
    exit 1
fi

timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"

# $$ — идентификатор процесса. Он дополнительно защищает
# от совпадения имён при двух запусках в одну секунду.
output_file="indeepa-report-one-page-${timestamp}-$$.json"

if ! curl \
    --fail \
    --silent \
    --show-error \
    --location \
    "$API_URL" \
    --output "$output_file"
then
    rm -f "$output_file"
    echo "Не удалось получить отчёт через cURL." >&2
    exit 1
fi

echo "Файл сохранён:"
echo "$(pwd)/$output_file"
```

{% endcode %}

После выполнения данные будут сохранены в файл. Пример имени:

```
indeepa-report-one-page-2026-01-01_01-01-01-12345.json
```

#### cURL: скачать одну страницу в JSON — Windows

Код запускается в PowerShell, но сам запрос выполняет `curl.exe`.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```powershell
# Вставьте полную API-ссылку между кавычками.
$url = "ВАША_ПОЛНАЯ_API_ССЫЛКА"

if ($url -eq "ВАША_ПОЛНАЯ_API_ССЫЛКА") {
    Write-Error "Сначала вставьте полную API-ссылку в переменную `$url."
    exit 1
}

$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss-fff"
$outputFile = "indeepa-report-one-page-$timestamp.json"

& curl.exe `
    --fail `
    --silent `
    --show-error `
    --location `
    $url `
    --output $outputFile

if ($LASTEXITCODE -ne 0) {
    Remove-Item `
        -LiteralPath $outputFile `
        -ErrorAction SilentlyContinue

    Write-Error "Не удалось получить отчёт через cURL."
    exit $LASTEXITCODE
}

Write-Host "Файл сохранён:"
Write-Host ([System.IO.Path]::GetFullPath($outputFile))
```

{% endcode %}

После выполнения данные будут сохранены в файл. Пример имени:

```
indeepa-report-one-page-2026-01-01_01-01-01-123.json
```

***

#### PowerShell: скачать одну страницу в JSON — Windows

Этот вариант не использует cURL. Запрос выполняется средствами PowerShell.

{% hint style="info" %}
Замените `ВАША_ПОЛНАЯ_API_ССЫЛКА` на ссылку, скопированную из отчёта INDEEPA.
{% endhint %}

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```powershell
# Вставьте полную API-ссылку между кавычками.
$url = "ВАША_ПОЛНАЯ_API_ССЫЛКА"

if ($url -eq "ВАША_ПОЛНАЯ_API_ССЫЛКА") {
    Write-Error "Сначала вставьте полную API-ссылку в переменную `$url."
    exit 1
}

$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss-fff"
$outputFile = "indeepa-report-one-page-$timestamp.json"

try {
    $data = Invoke-RestMethod `
        -Uri $url `
        -Method Get `
        -TimeoutSec 120

    # Оборачиваем результат в массив, чтобы даже одна строка
    # сохранилась как корректный JSON-массив.
    $rows = @($data)

    $json = ConvertTo-Json `
        -InputObject $rows `
        -Depth 100

    Set-Content `
        -LiteralPath $outputFile `
        -Value $json `
        -Encoding UTF8

    Write-Host "Получено строк: $($rows.Count)"
    Write-Host "Файл сохранён:"
    Write-Host ([System.IO.Path]::GetFullPath($outputFile))
}
catch {
    $statusCode = $null

    if (
        $_.Exception.Response -and
        $_.Exception.Response.StatusCode
    ) {
        $statusCode = [int]$_.Exception.Response.StatusCode
    }

    if ($statusCode) {
        Write-Error "API вернул HTTP $statusCode."
    }
    else {
        Write-Error (
            "Не удалось получить отчёт. " +
            "Проверьте API-ссылку, соединение и доступ."
        )
    }

    Remove-Item `
        -LiteralPath $outputFile `
        -ErrorAction SilentlyContinue

    exit 1
}
```

{% endcode %}

После выполнения данные будут сохранены в файл. Пример имени:

```
indeepa-report-one-page-2026-01-01_01-01-01-123.json
```

***

#### Python: скачать одну страницу в JSON

Работает в Windows, macOS и Linux.

> Для запуска потребуется Python 3.9 или новее и библиотека `requests`.

Для примера используется библиотека `requests`.

**Установка:**

```bash
pip install requests
```

Python 3.9 нужен из-за синтаксиса типов:

```python
list[dict[str, Any]]
```

**Код:**

{% hint style="info" %}
Замените `ВАША_ПОЛНАЯ_API_ССЫЛКА` на ссылку, скопированную из отчёта INDEEPA.
{% endhint %}

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```python
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit

import requests


# Вставьте полную API-ссылку между кавычками.
API_URL = "ВАША_ПОЛНАЯ_API_ССЫЛКА"


# Дата и время формируются один раз при запуске.
RUN_TIMESTAMP = datetime.now().strftime(
    "%Y-%m-%d_%H-%M-%S-%f"
)

OUTPUT_PATH = Path(
    f"indeepa-report-one-page-{RUN_TIMESTAMP}.json"
)


def validate_api_url() -> None:
    """Проверяет, что указана полная API-ссылка."""
    if API_URL == "ВАША_ПОЛНАЯ_API_ССЫЛКА":
        raise ValueError(
            "Сначала вставьте полную API-ссылку "
            "в переменную API_URL."
        )

    parsed_url = urlsplit(API_URL)

    if (
        parsed_url.scheme not in {"http", "https"}
        or not parsed_url.netloc
    ):
        raise ValueError(
            "API_URL должен содержать полную ссылку, "
            "начинающуюся с http:// или https://."
        )


def describe_request_error(
    error: requests.RequestException,
) -> str:
    """Описывает ошибку без вывода API-ссылки и токена."""
    if isinstance(error, requests.Timeout):
        return "превышено время ожидания ответа"

    if isinstance(error, requests.ConnectionError):
        return "не удалось подключиться к API"

    if (
        isinstance(error, requests.HTTPError)
        and error.response is not None
    ):
        return (
            f"API вернул HTTP "
            f"{error.response.status_code}"
        )

    return type(error).__name__


def main() -> None:
    try:
        validate_api_url()

        response = requests.get(
            API_URL,
            timeout=(10, 120),
        )
        response.raise_for_status()

        data: Any = response.json()

    except ValueError as error:
        print(
            str(error),
            file=sys.stderr,
        )
        raise SystemExit(1)

    except requests.RequestException as error:
        print(
            "Не удалось получить отчёт: "
            f"{describe_request_error(error)}.",
            file=sys.stderr,
        )
        raise SystemExit(1)

    if not isinstance(data, list):
        print(
            "Ожидался JSON-массив, "
            "но API вернул другой формат.",
            file=sys.stderr,
        )
        raise SystemExit(1)

    try:
        # Режим x дополнительно запрещает перезапись файла.
        with OUTPUT_PATH.open(
            "x",
            encoding="utf-8",
        ) as file:
            json.dump(
                data,
                file,
                ensure_ascii=False,
                indent=2,
            )

    except OSError as error:
        print(
            f"Не удалось сохранить файл: {error}",
            file=sys.stderr,
        )
        raise SystemExit(1)

    print(f"Получено строк: {len(data)}.")
    print(
        f"Файл сохранён:\n"
        f"{OUTPUT_PATH.resolve()}"
    )


if __name__ == "__main__":
    main()
```

{% endcode %}

Файл будет сохранён в папке, из которой запущен скрипт или команда.

Пример имени:

```
indeepa-report-one-page-2026-01-01_01-01-01-123456.json
```

***

#### Python: выгрузить все страницы отчёта

Работает в Windows, macOS и Linux.

Этот вариант последовательно меняет `skip` и получает данные частями по 1000 строк.

Вставьте полную API-ссылку в переменную `API_URL`. Скрипт сохранит все параметры отчёта и будет автоматически изменять только `skip` и `take`.

> Для запуска потребуется Python 3.9 или новее и библиотека `requests`.

Для примера используется библиотека `requests`.

**Установка:**

```bash
pip install requests
```

Python 3.9 нужен из-за синтаксиса типов:

```python
list[dict[str, Any]]
```

{% hint style="info" %}
Замените `ВАША_ПОЛНАЯ_API_ССЫЛКА` на полную ссылку, скопированную из отчёта INDEEPA. Скрипт сохранит дополнительные параметры отчёта и будет автоматически изменять только `skip` и `take`.
{% endhint %}

{% code title="" lineNumbers="true" expandable="true" %}

```python
import csv
import hashlib
import json
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import (
    parse_qsl,
    urlencode,
    urlsplit,
    urlunsplit,
)

import requests


# Вставьте полную API-ссылку между кавычками.
API_URL = "ВАША_ПОЛНАЯ_API_ССЫЛКА"

# Количество строк, запрашиваемых за один раз.
TAKE = 1000

# Защитный лимит количества страниц.
# При TAKE = 1000 это максимум 1 000 000 строк.
MAX_PAGES = 1000

# Пауза между запросами в секундах.
REQUEST_DELAY_SECONDS = 7.5


# Одна метка времени используется для всех файлов запуска.
RUN_TIMESTAMP = datetime.now().strftime(
    "%Y-%m-%d_%H-%M-%S-%f"
)

FINAL_JSON_PATH = Path(
    f"indeepa-report-all-page-{RUN_TIMESTAMP}.json"
)

FINAL_CSV_PATH = Path(
    f"indeepa-report-all-page-{RUN_TIMESTAMP}.csv"
)

PARTIAL_JSON_PATH = Path(
    f"indeepa-report-all-page-"
    f"{RUN_TIMESTAMP}-partial.json"
)

PARTIAL_CSV_PATH = Path(
    f"indeepa-report-all-page-"
    f"{RUN_TIMESTAMP}-partial.csv"
)


def validate_api_url() -> None:
    """Проверяет, что указана полная API-ссылка."""
    if API_URL == "ВАША_ПОЛНАЯ_API_ССЫЛКА":
        raise ValueError(
            "Сначала вставьте полную API-ссылку "
            "в переменную API_URL."
        )

    parsed_url = urlsplit(API_URL)

    if (
        parsed_url.scheme not in {"http", "https"}
        or not parsed_url.netloc
    ):
        raise ValueError(
            "API_URL должен содержать полную ссылку, "
            "начинающуюся с http:// или https://."
        )


def build_page_url(skip: int) -> str:
    """
    Изменяет skip и take, сохраняя остальные
    параметры исходной API-ссылки.
    """
    parsed_url = urlsplit(API_URL)

    query_pairs = parse_qsl(
        parsed_url.query,
        keep_blank_values=True,
    )

    updated_pairs: list[tuple[str, str]] = []
    skip_added = False
    take_added = False

    for key, value in query_pairs:
        if key == "skip":
            if not skip_added:
                updated_pairs.append(
                    ("skip", str(skip))
                )
                skip_added = True

        elif key == "take":
            if not take_added:
                updated_pairs.append(
                    ("take", str(TAKE))
                )
                take_added = True

        else:
            # Сохраняем дополнительные параметры отчёта,
            # в том числе повторяющиеся.
            updated_pairs.append((key, value))

    if not skip_added:
        updated_pairs.append(
            ("skip", str(skip))
        )

    if not take_added:
        updated_pairs.append(
            ("take", str(TAKE))
        )

    return urlunsplit(
        (
            parsed_url.scheme,
            parsed_url.netloc,
            parsed_url.path,
            urlencode(updated_pairs),
            parsed_url.fragment,
        )
    )


def get_page(
    session: requests.Session,
    skip: int,
) -> list[dict[str, Any]]:
    """Получает одну страницу отчёта."""
    response = session.get(
        build_page_url(skip),
        timeout=(10, 120),
    )

    response.raise_for_status()
    data = response.json()

    if not isinstance(data, list):
        raise TypeError(
            "Ожидался JSON-массив, "
            f"получен объект типа "
            f"{type(data).__name__}."
        )

    if not all(
        isinstance(row, dict)
        for row in data
    ):
        raise TypeError(
            "Ожидался JSON-массив объектов, "
            "но в ответе есть элементы "
            "другого типа."
        )

    return data


def get_page_signature(
    rows: list[dict[str, Any]],
) -> str:
    """
    Создаёт контрольную сумму страницы
    для выявления повторной выдачи.
    """
    serialized = json.dumps(
        rows,
        ensure_ascii=False,
        sort_keys=True,
        separators=(",", ":"),
    )

    return hashlib.sha256(
        serialized.encode("utf-8")
    ).hexdigest()


def save_json(
    rows: list[dict[str, Any]],
    path: Path,
) -> None:
    """Сохраняет данные в JSON."""
    with path.open(
        "x",
        encoding="utf-8",
    ) as file:
        json.dump(
            rows,
            file,
            ensure_ascii=False,
            indent=2,
        )


def save_csv(
    rows: list[dict[str, Any]],
    path: Path,
) -> bool:
    """Сохраняет данные в CSV."""
    if not rows:
        return False

    fieldnames = sorted(
        {
            field
            for row in rows
            for field in row.keys()
        }
    )

    with path.open(
        "x",
        encoding="utf-8-sig",
        newline="",
    ) as file:
        writer = csv.DictWriter(
            file,
            fieldnames=fieldnames,
            extrasaction="ignore",
        )

        writer.writeheader()
        writer.writerows(rows)

    return True


def save_result(
    rows: list[dict[str, Any]],
    *,
    partial: bool,
) -> None:
    """Сохраняет итоговый или частичный результат."""
    if not rows:
        print(
            "Полученных строк нет — "
            "сохранять нечего."
        )
        return

    if partial:
        json_path = PARTIAL_JSON_PATH
        csv_path = PARTIAL_CSV_PATH
        result_type = "Частичный"
    else:
        json_path = FINAL_JSON_PATH
        csv_path = FINAL_CSV_PATH
        result_type = "Итоговый"

    save_json(
        rows,
        json_path,
    )

    csv_created = save_csv(
        rows,
        csv_path,
    )

    print(
        f"{result_type} результат "
        f"сохранён в JSON:\n"
        f"{json_path.resolve()}"
    )

    if csv_created:
        print(
            f"{result_type} результат "
            f"сохранён в CSV:\n"
            f"{csv_path.resolve()}"
        )


def describe_request_error(
    error: requests.RequestException,
) -> str:
    """Описывает ошибку без вывода API-ссылки и токена."""
    if isinstance(error, requests.Timeout):
        return "превышено время ожидания ответа"

    if isinstance(error, requests.ConnectionError):
        return "не удалось подключиться к API"

    if (
        isinstance(error, requests.HTTPError)
        and error.response is not None
    ):
        return (
            f"API вернул HTTP "
            f"{error.response.status_code}"
        )

    return type(error).__name__


def main() -> None:
    try:
        validate_api_url()

    except ValueError as error:
        print(
            str(error),
            file=sys.stderr,
        )
        raise SystemExit(1)

    all_rows: list[dict[str, Any]] = []
    seen_page_signatures: set[str] = set()

    skip = 0
    pages_loaded = 0
    completed = False
    exit_code = 0

    try:
        with requests.Session() as session:
            while True:
                # Защита №1: лимит количества страниц.
                if pages_loaded >= MAX_PAGES:
                    raise RuntimeError(
                        "Достигнут защитный лимит: "
                        f"{MAX_PAGES} страниц. "
                        "Проверьте параметры отчёта "
                        "или увеличьте MAX_PAGES."
                    )

                page = get_page(
                    session,
                    skip,
                )

                pages_loaded += 1

                # Защита №2: получена пустая страница.
                if not page:
                    print(
                        f"Страница {pages_loaded}, "
                        f"skip={skip}: "
                        "получен пустой массив — "
                        "выгрузка завершена."
                    )

                    completed = True
                    break

                page_signature = (
                    get_page_signature(page)
                )

                # Защита №3: API повторно вернул
                # уже загруженную страницу.
                if (
                    page_signature
                    in seen_page_signatures
                ):
                    raise RuntimeError(
                        "API повторно вернул уже "
                        "полученную страницу. "
                        "Возможно, параметр skip "
                        "не применяется. "
                        "Выгрузка остановлена, "
                        "чтобы избежать зацикливания."
                    )

                seen_page_signatures.add(
                    page_signature
                )

                all_rows.extend(page)

                print(
                    f"Страница {pages_loaded}, "
                    f"skip={skip}: "
                    f"получено {len(page)} строк, "
                    f"всего {len(all_rows)}."
                )

                # Защита №4: неполная страница
                # считается последней.
                if len(page) < TAKE:
                    print(
                        "Получено меньше строк, "
                        "чем указано в TAKE — "
                        "это последняя страница."
                    )

                    completed = True
                    break

                skip += TAKE

                time.sleep(
                    REQUEST_DELAY_SECONDS
                )

    except KeyboardInterrupt:
        print(
            "\nВыгрузка остановлена "
            "пользователем. "
            "Сохраняю уже полученные данные...",
            file=sys.stderr,
        )

        exit_code = 130

    except requests.RequestException as error:
        print(
            "Ошибка запроса: "
            f"{describe_request_error(error)}. "
            "Сохраняю уже полученные данные...",
            file=sys.stderr,
        )

        exit_code = 1

    except (
        ValueError,
        TypeError,
        RuntimeError,
    ) as error:
        print(
            "Ошибка обработки выгрузки: "
            f"{error}. "
            "Сохраняю уже полученные данные...",
            file=sys.stderr,
        )

        exit_code = 1

    try:
        save_result(
            all_rows,
            partial=not completed,
        )

    except OSError as error:
        print(
            f"Не удалось сохранить результат: "
            f"{error}",
            file=sys.stderr,
        )

        exit_code = 1

    print(
        f"Получено строк: {len(all_rows)}. "
        f"Обработано страниц: "
        f"{pages_loaded}."
    )

    if exit_code:
        raise SystemExit(exit_code)


if __name__ == "__main__":
    main()
```

{% endcode %}

Файлы `indeepa-report-all-page-<дата и время>.json` и `indeepa-report-all-page-<дата и время>.csv` будут сохранены в папке, из которой запущен скрипт или команда.

При остановке через `Ctrl+C`, ошибке запроса, повторении страницы или достижении защитного лимита будут созданы частичные файлы, если к этому моменту скрипт уже получил хотя бы одну страницу данных.

* `indeepa-report-all-page-<дата и время>-partial.json`
* `indeepa-report-all-page-<дата и время>-partial.csv`

> Подробнее о параметрах `skip` и `take` — в Шаге 4.

***

### Шаг 4. Если данных много: как выгрузить всё по частям (пагинация)

Система не отдаёт все данные сразу — чтобы не перегружать сервер. Поэтому используется постраничная выгрузка через параметры skip (пропустить) и take (взять).

**Какой размер страницы выбрать?**

* `take=100` — для проверки ссылки или небольшой тестовой выгрузки.
* `take=1000` — для основной выгрузки.

Если данных больше 1000 строк, используйте пагинацию через `skip`.

{% hint style="info" %}
Максимальное значение `take` — 1000. За один запрос можно получить не более 1000 записей. Если вам нужно выгрузить большой объём данных, вы можете увеличить take до 1000 — так выгрузка пройдёт быстрее, меньшим количеством запросов.
{% endhint %}

#### **Как считать skip, если take=1000:**

<table data-header-hidden><thead><tr><th valign="top"></th><th valign="top"></th><th valign="top"></th><th valign="top"></th></tr></thead><tbody><tr><td valign="top"><p><strong>Запрос</strong></p><p> </p></td><td valign="top"><p><strong>Skip</strong></p><p> </p></td><td valign="top"><p><strong>Take</strong></p><p> </p></td><td valign="top"><strong>Что получаем</strong></td></tr><tr><td valign="top">1</td><td valign="top">0</td><td valign="top">1000</td><td valign="top">Записи 1–1000</td></tr><tr><td valign="top">2</td><td valign="top">1000</td><td valign="top">1000</td><td valign="top">Записи 1001–2000</td></tr><tr><td valign="top">3</td><td valign="top">2000</td><td valign="top">1000</td><td valign="top">Менее 1000 записей → стоп</td></tr></tbody></table>

#### Как понять, что выгрузка завершена?

* Если в ответе меньше строк, чем указано в take, это последняя порция.
* Если получено ровно take строк, запросите следующую страницу.
* Если следующая страница пустая, выгрузка завершена.

### Если данные не совпали с ожиданиями

* Проверьте даты и дополнительные фильтры в ссылке.
* Убедитесь, что skip увеличивается ровно на значение take: иначе строки могут повторяться или пропускаться.
* Если ссылка перестала открываться, сформируйте новую ссылку через интерфейс отчёта. Не пытайтесь самостоятельно создавать новый token.
* При обращении в [чат поддержки](/repricer/sluzhba-podderzhki/kak-obratitsya-v-sluzhbu-podderzhki.md) укажите название отчёта, период, значения skip и take и опишите ожидаемый результат. Секретные параметры предварительно скройте.

&#x20;
