osm2cdr API: Quick Start with Python and JavaScript
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:
- POST /api/render — create a task
- GET /api/status/{task_id} — check status
- 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.