Open Beta — all 133 formats free, no limits Learn more

osm2cdr API: Quick Start with Python and JavaScript

2026-06-106 min read
APIPythonJavaScriptавтоматизацияэкспорт

The osm2cdr API lets you automate exporting OpenStreetMap maps to 127 formats. Instead of manually selecting an area and format on the website, you can send an HTTP request and get the finished file. This is useful for batch processing, CRM integration, report generation, and building your own services on top of osm2cdr.

Registration and API Key

Step 1: Create an Account

Register at osm2cdr.ru via email or OAuth (Google/GitHub). After registration, the "API" section appears in your dashboard.

Step 2: Generate a Key

In the API section, click "Create Key." The key looks like:

ocd_abc123def456ghi789jkl012mno345

Store the key securely. It's passed in the X-API-Key header with every request.

Access Tiers

Tier Request Limit Max Area Formats
Free 10/day 25 km2 20 basic
Basic 100/day 100 km2 60 formats
Pro 1000/day 500 km2 All 124
Enterprise Unlimited 20,000 km2 All + priority

API Structure

Export works asynchronously in three stages:

  1. POST /api/render — create a task
  2. GET /api/status/{task_id} — check status
  3. GET /api/download/{task_id}/{format} — download the result (format is required in the path)

Python: Complete Example

Installation

pip install requests

Minimal Example

import requests
import time

API_URL = "https://osm2cdr.ru/api"
API_KEY = "ocd_your_key_here"

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

# 1. Create export task
# NOTE: payload matches api/schemas.py::RenderRequest — bbox is an object,
# formats is an array (1-5 items), map_style and detail_level are top-level
# (the schema validator hoists them into `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 created: {task_id}")

# 2. Wait for completion
while True:
    status = requests.get(f"{API_URL}/status/{task_id}", headers=headers).json()
    print(f"Status: {status['status']} ({status.get('progress', 0)}%)")

    if status["status"] == "completed":
        break
    elif status["status"] == "failed":
        raise Exception(f"Error: {status.get('error', 'unknown')}")

    time.sleep(2)

# 3. Download result (format is required in the path)
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"Saved: {filename} ({len(download.content)} bytes)")

Advanced: Batch Export

import requests
import time
from concurrent.futures import ThreadPoolExecutor

API_URL = "https://osm2cdr.ru/api"
API_KEY = "ocd_your_key_here"
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:
    """Export a single city."""
    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"]

    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}"

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: Complete Example

Node.js (with fetch)

const API_URL = "https://osm2cdr.ru/api";
const API_KEY = "ocd_your_key_here";

async function exportMap(bbox, format = "pdf") {
  // 1. Create task
  // NOTE: payload matches api/schemas.py::RenderRequest — bbox as object,
  // formats as array, top-level map_style is hoisted into 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. Poll status
  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. Download (format is required in the path)
  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]; // Central Moscow
exportMap(bbox, "pdf").then((resp) => {
  console.log(`Downloaded: ${resp.headers.get("content-length")} bytes`);
});

Browser (with Progress Bar)

async function exportWithProgress(bbox, format, onProgress) {
  const API_URL = "https://osm2cdr.ru/api";
  const API_KEY = "ocd_your_key_here";
  const headers = { "X-API-Key": API_KEY, "Content-Type": "application/json" };

  // See api/schemas.py::RenderRequest for the canonical payload shape.
  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();

  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);
  });
}

Request Parameters for /api/render

Parameter Type Required Description
bbox object Yes {min_lon, min_lat, max_lon, max_lat} (see BBox in schemas.py)
formats array[string] Yes 1-5 formats: ["svg"], ["pdf","geojson"], ["dxf"]...
map_style string No Map style: default, minimal, blueprint... (top-level, hoisted into config)
detail_level int (1-6) No Detail level (default 3)
config.crs string No Coordinate system: EPSG:4326, EPSG:32637...
config.layers array No Layer filter: ["buildings", "roads", "water"]

Note: downloads always use /api/download/{task_id}/{format} — the format is required in the path, otherwise you get a 404.

Error Handling

The API returns standard HTTP codes:

  • 400 — invalid parameters (bbox out of bounds)
  • 401 — invalid or missing API key
  • 402 — tier limit exceeded (upgrade needed)
  • 429 — rate limit (too many requests)
  • 500 — internal server error

Always check response codes and handle errors:

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

Real-Time Progress via Polling

In the current API version, task progress is tracked via REST polling — the WebSocket endpoint /ws/task/{task_id} exists only as a stub (it immediately closes with code 1001) and is reserved for a future Celery -> Redis Pub/Sub bridge. Do not rely on it.

The recommended approach is to poll GET /api/status/{task_id} every 2 seconds; the response includes progress, status, step, error, and files (on completion):

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));
  }
}

For long-running tasks, increase the interval to 5 seconds to avoid hitting rate limits. On HTTP 429, honour the Retry-After header.

Conclusion

The osm2cdr API is suitable for automation at any scale: from a single export to a pipeline of thousands of maps. Python and JavaScript are equally well supported. Start with the free tier (10 requests per day), and upgrade to Pro or Enterprise as your needs grow.

← All articles