API osm2cdr: быстрый старт на Python и JavaScript
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
Экспорт работает асинхронно в три этапа:
- POST /api/render — создание задачи
- GET /api/status/{task_id} — проверка статуса
- 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.