Open Beta — все 133 форматов бесплатно, без лимитов Подробнее

API osm2cdr: быстрый старт на Python и JavaScript

2026-06-106 мин чтения
APIPythonJavaScriptавтоматизацияэкспорт

API osm2cdr позволяет автоматизировать экспорт карт OpenStreetMap в 127 форматов. Вместо ручного выбора области и формата на сайте можно отправить HTTP-запрос и получить готовый файл. Это полезно для пакетной обработки, интеграции с CRM, генерации отчётов и построения собственных сервисов поверх osm2cdr.

Регистрация и получение API-ключа

Шаг 1: Создание аккаунта

Зарегистрируйтесь на osm2cdr.ru через email или OAuth (Google/GitHub). После регистрации в личном кабинете появится раздел «API».

Шаг 2: Генерация ключа

В разделе API нажмите «Создать ключ». Ключ выглядит так:

ocd_abc123def456ghi789jkl012mno345

Сохраните ключ в безопасном месте. Он передаётся в заголовке X-API-Key при каждом запросе.

Тиры доступа

Тир Лимит запросов Макс. площадь Форматы
Free 10/день 25 km2 20 базовых
Basic 100/день 100 km2 60 форматов
Pro 1000/день 500 km2 Все 124
Enterprise Без лимита 20 000 km2 Все + приоритет

Структура API

Экспорт работает асинхронно в три этапа:

  1. POST /api/render — создание задачи
  2. GET /api/status/{task_id} — проверка статуса
  3. GET /api/download/{task_id}/{format} — скачивание результата (формат обязателен в пути)

Python: полный пример

Установка

pip install requests

Минимальный пример

import requests
import time

API_URL = "https://osm2cdr.ru/api"
API_KEY = "ocd_ваш_ключ_здесь"

headers = {"X-API-Key": API_KEY}

# 1. Создаём задачу экспорта
# NOTE: payload соответствует api/schemas.py::RenderRequest — bbox как объект,
# formats как массив (1-5 элементов), map_style и detail_level на верхнем уровне
# (они хойстятся в config валидатором схемы).
payload = {
    "bbox": {"min_lon": 37.58, "min_lat": 55.74, "max_lon": 37.65, "max_lat": 55.76},
    "formats": ["geojson"],
    "map_style": "default",
    "detail_level": 3
}

response = requests.post(f"{API_URL}/render", json=payload, headers=headers)
response.raise_for_status()
task = response.json()
task_id = task["task_id"]
print(f"Задача создана: {task_id}")

# 2. Ожидаем завершения
while True:
    status = requests.get(f"{API_URL}/status/{task_id}", headers=headers).json()
    print(f"Статус: {status['status']} ({status.get('progress', 0)}%)")

    if status["status"] == "completed":
        break
    elif status["status"] == "failed":
        raise Exception(f"Ошибка: {status.get('error', 'unknown')}")

    time.sleep(2)

# 3. Скачиваем результат (формат обязателен в пути)
download = requests.get(f"{API_URL}/download/{task_id}/geojson", headers=headers)
filename = f"export_{task_id}.geojson"
with open(filename, "wb") as f:
    f.write(download.content)
print(f"Сохранено: {filename} ({len(download.content)} байт)")

Продвинутый пример: пакетный экспорт

import requests
import time
from concurrent.futures import ThreadPoolExecutor

API_URL = "https://osm2cdr.ru/api"
API_KEY = "ocd_ваш_ключ_здесь"
headers = {"X-API-Key": API_KEY}

cities = {
    "moscow": [37.35, 55.57, 37.85, 55.92],
    "spb": [30.15, 59.85, 30.50, 60.05],
    "kazan": [49.05, 55.75, 49.20, 55.85],
}

def export_city(name: str, bbox: list) -> str:
    """Экспортировать один город."""
    payload = {
        "bbox": {
            "min_lon": bbox[0], "min_lat": bbox[1],
            "max_lon": bbox[2], "max_lat": bbox[3],
        },
        "formats": ["svg"],
        "map_style": "minimal",
    }
    resp = requests.post(f"{API_URL}/render", json=payload, headers=headers)
    task_id = resp.json()["task_id"]

    # Polling
    for _ in range(60):
        status = requests.get(f"{API_URL}/status/{task_id}", headers=headers).json()
        if status["status"] == "completed":
            data = requests.get(
                f"{API_URL}/download/{task_id}/svg", headers=headers
            )
            filename = f"{name}.svg"
            with open(filename, "wb") as f:
                f.write(data.content)
            return filename
        elif status["status"] == "failed":
            return f"FAILED: {name}"
        time.sleep(3)
    return f"TIMEOUT: {name}"

# Параллельный экспорт (по 3 одновременно)
with ThreadPoolExecutor(max_workers=3) as pool:
    futures = {pool.submit(export_city, n, b): n for n, b in cities.items()}
    for future in futures:
        print(f"{futures[future]}: {future.result()}")

JavaScript: полный пример

Node.js (с fetch)

const API_URL = "https://osm2cdr.ru/api";
const API_KEY = "ocd_ваш_ключ_здесь";

async function exportMap(bbox, format = "pdf") {
  // 1. Создаём задачу
  // NOTE: payload соответствует api/schemas.py::RenderRequest — bbox объект,
  // formats массив, map_style на верхнем уровне хойстится в config.
  const response = await fetch(`${API_URL}/render`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": API_KEY,
    },
    body: JSON.stringify({
      bbox: {
        min_lon: bbox[0], min_lat: bbox[1],
        max_lon: bbox[2], max_lat: bbox[3],
      },
      formats: [format],
      map_style: "default",
      detail_level: 3,
    }),
  });

  const task = await response.json();
  console.log(`Task created: ${task.task_id}`);

  // 2. Polling статуса
  while (true) {
    const statusResp = await fetch(`${API_URL}/status/${task.task_id}`, {
      headers: { "X-API-Key": API_KEY },
    });
    const status = await statusResp.json();
    console.log(`Status: ${status.status} (${status.progress || 0}%)`);

    if (status.status === "completed") break;
    if (status.status === "failed") throw new Error(status.error);

    await new Promise((r) => setTimeout(r, 2000));
  }

  // 3. Скачиваем (формат обязателен в пути)
  const download = await fetch(`${API_URL}/download/${task.task_id}/${format}`, {
    headers: { "X-API-Key": API_KEY },
  });

  return download;
}

// Использование
const bbox = [37.58, 55.74, 37.65, 55.76]; // Центр Москвы
exportMap(bbox, "pdf").then((resp) => {
  console.log(`Downloaded: ${resp.headers.get("content-length")} bytes`);
});

Браузер (с прогресс-баром)

async function exportWithProgress(bbox, format, onProgress) {
  const API_URL = "https://osm2cdr.ru/api";
  const API_KEY = "ocd_ваш_ключ_здесь";
  const headers = { "X-API-Key": API_KEY, "Content-Type": "application/json" };

  // Создаём задачу (см. api/schemas.py::RenderRequest)
  const resp = await fetch(`${API_URL}/render`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      bbox: {
        min_lon: bbox[0], min_lat: bbox[1],
        max_lon: bbox[2], max_lat: bbox[3],
      },
      formats: [format],
      map_style: "default",
    }),
  });
  const { task_id } = await resp.json();

  // Polling с колбэком прогресса
  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      const status = await fetch(`${API_URL}/status/${task_id}`, { headers });
      const data = await status.json();

      onProgress(data.progress || 0, data.status);

      if (data.status === "completed") {
        clearInterval(interval);
        resolve(`${API_URL}/download/${task_id}/${format}`);
      } else if (data.status === "failed") {
        clearInterval(interval);
        reject(new Error(data.error));
      }
    }, 2000);
  });
}

// Использование с прогресс-баром
const progressBar = document.getElementById("progress");
exportWithProgress(
  [37.58, 55.74, 37.65, 55.76],
  "svg",
  (percent, status) => {
    progressBar.style.width = `${percent}%`;
    progressBar.textContent = `${status}: ${percent}%`;
  }
).then((url) => {
  window.location.href = url; // Скачивание
});

Параметры запроса /api/render

Параметр Тип Обязательный Описание
bbox object Да {min_lon, min_lat, max_lon, max_lat} (см. BBox в schemas.py)
formats array[string] Да 1-5 форматов: ["svg"], ["pdf","geojson"], ["dxf"]...
map_style string Нет Стиль карты: default, minimal, blueprint... (top-level, хойстится в config)
detail_level int (1-6) Нет Уровень детализации (по умолчанию 3)
config.crs string Нет Система координат: EPSG:4326, EPSG:32637...
config.layers array Нет Фильтр слоёв: ["buildings", "roads", "water"]

Примечание: скачивание всегда по шаблону /api/download/{task_id}/{format} — формат в пути обязателен, иначе получите 404.

Обработка ошибок

API возвращает стандартные HTTP-коды:

  • 400 — неверные параметры (bbox вне допустимых пределов)
  • 401 — невалидный или отсутствующий API-ключ
  • 402 — превышен лимит тира (нужен апгрейд)
  • 429 — rate limit (слишком много запросов)
  • 500 — внутренняя ошибка сервера

Всегда проверяйте код ответа и обрабатывайте ошибки:

if response.status_code == 429:
    retry_after = int(response.headers.get("Retry-After", 60))
    time.sleep(retry_after)

Реальное время через polling

В текущей версии API прогресс задачи отслеживается через REST polling — WebSocket-эндпоинт /ws/task/{task_id} существует как заглушка (немедленно закрывается с кодом 1001) и зарезервирован для будущего перехода через Celery -> Redis Pub/Sub. Не полагайтесь на него.

Рекомендованный подход — поллинг GET /api/status/{task_id} с интервалом 2 секунды; ответ содержит поля progress, status, step, error и files (по завершении):

async function pollStatus(task_id, onProgress) {
  while (true) {
    const resp = await fetch(`${API_URL}/status/${task_id}`, {
      headers: { "X-API-Key": API_KEY },
    });
    const data = await resp.json();

    onProgress(data.progress || 0, data.status, data.step);

    if (data.status === "completed") return data;
    if (data.status === "failed") throw new Error(data.error);

    await new Promise((r) => setTimeout(r, 2000));
  }
}

Для долгих задач увеличьте интервал до 5 секунд, чтобы не упереться в rate limit. При HTTP 429 уважайте заголовок Retry-After.

Заключение

API osm2cdr подходит для автоматизации любого масштаба: от единичного экспорта до конвейера на тысячи карт. Python и JavaScript поддерживаются одинаково хорошо. Начните с бесплатного тира (10 запросов в день), а при росте нагрузки переходите на Pro или Enterprise.

← Все статьи