pycrocosm implements the OpenStreetMap API v0.6. This document covers what it offers beyond that API, across all its Django apps, and the tile server pycromap beside it. An endpoint counts as standard if openstreetmap-website's routes have it.
These endpoints are specific to pycrocosm, so a client written for openstreetmap.org won't find them there, and they may change as the server does.
| Endpoint | What it gives | Django app |
|---|---|---|
GET /api/0.6/map with format, atomic_edit_id or timestamp |
The map in binary formats, or as it was at a past edit or time | querymap |
GET /api/0.6/{node,way,relation}/{id}/bbox |
An object's bounding box | elements |
GET /replication/edit_ids |
The latest edit IDs | replicate |
GET /replication/changed |
Whether an area has changed since an edit | replicate |
GET /replication/edit_activities |
Edit records, by ID, time or area | replicate |
GET /replication/edit_activity/{id} |
One edit record | replicate |
GET /replication/diff |
A diff of any time range, up to a day | replicate |
GET /replication/now |
The server's time | replicate |
GET /replication/extract/... |
Download a stored extract | replicate |
GET /replication/extracts, /replication/planet |
Lists of extracts and planet dumps (HTML) | replicate |
GET /extra/most_active |
The most active users | extra |
GET, POST /api/interpreter |
Overpass QL queries | overpass |
GET /overpass/xapi/..., /overpass/xapi_meta |
XAPI queries | overpass |
GET /.well-known/oauth-authorization-server |
OAuth2 discovery | oauth2 |
/api/authorize, /api/token |
OAuth2, next to the API | oauth2 |
| TCP port 7500 | Notifications of new edits | notify |
GET /tiles/{z}/{x}/{y}.pbf and others |
Vector tiles and the map's configuration | pycromap's tiles |
- Login: none of these endpoints needs one, unless its section says so.
- Times are ISO 8601, such as
2026-01-31T12:00:00Z, and are taken as UTC unless they give an offset.extra/most_activediffers: see its section. - Edit IDs: an edit activity record describes one change to the map, and has its own ID. An atomic edit is one transaction, such as one upload, and may hold several records.
edit_activity_idandatomic_edit_idalways mean these two. - Errors are
400with a short message, unless a section says otherwise. - Bounding boxes are ordered differently depending on the endpoint:
| Where | Order |
|---|---|
/api/0.6/map, replication/*, XAPI's [bbox=...], Overpass's bbox= parameter |
west, south, east, north |
| Inside an Overpass QL query | south, west, north, east |
GET /api/0.6/map takes the standard bbox, and three arguments of its own. The limit on the area (AREA_MAXIMUM, 0.25 square degrees by default) applies as usual.
| Argument | Meaning |
|---|---|
format |
One of osm, osm.gz, json, json.gz, o5m, o5m.gz or pbf. Without it, the answer is XML, or JSON if the path ends in .json or the Accept header asks for it. All but osm and json come as a file to save (map.<format>). |
atomic_edit_id |
The map as it was just after this atomic edit, with each object at its version then. It takes longer the more edits have been made since. |
timestamp |
The map as it was at this time: after the latest atomic edit recorded at or before it. A time before the first recorded edit gets 400. |
atomic_edit_id and timestamp can't be combined. Both reach back only as far as the edit activity records do, and both work with any format.
curl -o map.pbf "http://localhost:8000/api/0.6/map?bbox=-1.09,50.79,-1.06,50.81&format=pbf"
curl "http://localhost:8000/api/0.6/map?bbox=-1.09,50.79,-1.06,50.81×tamp=2026-01-31T12:00:00Z"
Ways and relations are found through their nodes in these historical queries. On a map that uses stored boxes (useBboxInQuery), the answer for the latest edit can therefore differ a little from the plain map call.
An object's stored bounding box. The answer is always XML, whatever format is asked for.
<bbox version="0.6" generator="..."><way id="123" minlon="-1.1" minlat="50.7" maxlon="-1.0" maxlat="50.8"/></bbox>
The coordinates are left out if no box is known for the object. An object that doesn't exist gets 404.
These are how another server, or anything else, keeps up with the map. Their records are what an extract following another server is updated from (README).
Where the map has got to.
<?xml version='1.0' encoding='UTF-8'?>
<editids edit_activity_id="1042" atomic_edit_id="387"/>
Whether an area has been edited since an atomic edit, for something like a map renderer that needs to know whether to draw an area again.
| Argument | Meaning |
|---|---|
bbox |
Required: west, south, east, north |
since |
Required: an atomic edit ID, or 0 for any edit since the map was imported |
The answer also gives the map's latest IDs, read at the same moment. Keep atomic_edit_id to use as the next since.
<?xml version='1.0' encoding='UTF-8'?>
<changed bbox="-1.1,50.7,-1.0,50.8" since="380" changed="true" edit_activity_id="1042" atomic_edit_id="387"/>
Edit activity records, in ID order.
| Argument | Meaning |
|---|---|
id |
One record |
first_id, last_id |
A range of record IDs, either end optional. Can't be combined with id. |
atomic_edit_id |
The records of one atomic edit |
since, until |
A range of times. Can't be combined with the ID arguments. |
bbox |
Keep only records that may concern this area: west, south, east, north. Records that don't say where their objects are stay in. |
limit |
How many records to send, from 1 to EDIT_ACTIVITY_MAXIMUM_RECORDS (100 by default) |
format |
xml (the default) or msgpack.gz |
Which records you get depends on whether you ask for a range:
- Without a range of IDs or times, you get the latest records: 20 of them unless
limitsays otherwise (EDIT_ACTIVITY_DEFAULT_RECORDS). - With a range, you get records from its start, up to
limit, or else the maximum. If there are more, the root element hastruncated="true"and anext_id. Continue withfirst_idset tonext_id, whichever way you chose the records at first.
To follow the whole map, read edit_ids, then ask for each range in turn:
curl "http://localhost:8000/replication/edit_activities?first_id=1000&last_id=1042"
Each editactivity holds the objects involved, as OSM XML:
<editactivities truncated="true" next_id="1021">
<editactivity id="1020" changeset="55" timestamp="2026-01-31T12:00:00Z" action="modify" atomic_edit_id="380" block_index="0">
<existing>...</existing>
<existing_bbox>...</existing_bbox>
<updated>...</updated>
<updated_bbox>...</updated_bbox>
<affectedparents>...</affectedparents>
<affectedparents_before_bbox>...</affectedparents_before_bbox>
<affectedparents_after_bbox>...</affectedparents_after_bbox>
<related>...</related>
<relatedparents>...</relatedparents>
</editactivity>
</editactivities>
| Attribute | Meaning |
|---|---|
id |
The record's ID |
action |
The action of the osmChange block it came from |
changeset, timestamp |
Left out if the record has none |
atomic_edit_id, block_index |
The atomic edit, and the position within it. Left out if the record has none. |
| Section | Holds |
|---|---|
existing |
The objects as they were before the edit |
updated |
The objects as the edit left them |
affectedparents |
The ways and relations affected by the change. A parent keeps its version, so its boxes are the only record of how its extent changed. |
related |
Objects around those edited |
relatedparents |
The relations holding the related nodes. <relatedparents null="true"/> means it wasn't recorded, which isn't the same as there being none. |
A box section has one entry for each object of the list it goes with, in order:
<updated_bbox>
<bbox type="node" id="1" minlon="-1.1" minlat="50.7" maxlon="-1.1" maxlat="50.7"/>
<bbox type="way" id="2" null="true"/>
</updated_bbox>
A node's entry is its position. An entry with null="true" is for a deleted object. If the whole section has null="true", the boxes weren't recorded.
format=msgpack.gz gives a gzipped msgpack map, with the same content in a form that's quicker to read:
editactivitiesis the list of records, plustruncatedandnext_idif there are more.- Each record has the attributes above, with
timestampin seconds since 1970, UTC. - Each object list is an o5m document.
- Each box section is a list of
[type, id, min lon, min lat, max lon, max lat]entries ([type, id]for a deleted object), or nil if it wasn't recorded.
One record, in the same document form as edit_activities. It takes format. A record that doesn't exist gets 404.
The notify app has no URLs: it's a separate TCP server, on port 7500 by default, speaking msgpack-RPC rather than HTTP. It tells subscribers about new edits as they're made, for the whole map or for an area, so that they needn't poll edit_ids. See notify/README.md for how to run it, and notify/protocol.py for the protocol.
An osmChange diff of any time range, up to REPLICATE_DIFF_MAXIMUM_SECONDS (a day by default).
| Argument | Meaning |
|---|---|
start, end |
Required: a date or a date and time. end can't be before start, or in the future. |
compress |
no (the default) or gz |
curl "http://localhost:8000/replication/diff?start=2026-01-31T00:00:00&end=2026-01-31T06:00:00&compress=gz" -o diff.osc.gz
A time is taken as UTC unless it gives an offset, and a date means midnight UTC. In a URL, write the + of an offset as %2B. Otherwise it reads as a space, and the time gets 400.
The server's time, as JSON.
{"now": "2026-01-31T12:00:00.123Z", "time": 1769860800.123}
/replication/{minute,hour,day}/... follow the layout of planet.openstreetmap.org's replication, as .osc.gz and .state.txt files, with two differences:
- Each diff is also available uncompressed, as
.osc. - There's no
state.txtat the top of each of these folders, where replication clients such as Osmosis usually look for the latest diff.
A stored extract, by its ID or its unique name. The snapshot stored is streamed as it is: it's neither updated nor queried afresh.
| Path | Format |
|---|---|
/replication/extract/{id} |
OSM XML |
/replication/extract/{id}.osm.gz |
Gzipped OSM XML |
/replication/extract/{id}.o5m.gz |
Gzipped o5m |
/replication/extract/{id}.pbf |
PBF |
/replication/extract?name={name} |
OSM XML only |
An extract that doesn't exist gets 404, and an ambiguous name gets 400. The header of the osm, osm.gz and o5m.gz downloads records the edit the file is current to, and where to update it from.
See the README for more. Planet dumps aren't served by a view: they're files under PLANET_DUMP_URL (/static/planet/ by default), listed at /replication/planet.
The users who have made the most edits since a time, with how many objects of each type.
| Argument | Meaning |
|---|---|
start |
Required: a time within the last 31 days. It must give its offset (such as Z). Without one, or if it can't be read, the answer is 500, not 400. |
<activeusers><user uid="1" username="someone" nodes="10" ways="2" relations="0"/></activeusers>
These are the Overpass API's and XAPI's interfaces, not the main API's. pycrocosm answers a subset of each.
An Overpass QL query. It's also at /overpass/api/interpreter.
| Argument | Meaning |
|---|---|
data |
The query. A POST can instead send it as the request's body. |
bbox |
West, south, east, north, for a query using [bbox] |
curl -d 'data=[out:json];nwr[amenity=pub](50.78,-1.10,50.80,-1.05);out center;' http://localhost:8000/api/interpreter
Any site may call it (Access-Control-Allow-Origin: *). Each client address may make OVERPASS_RATE_LIMIT_REQUESTS queries in OVERPASS_RATE_LIMIT_WINDOW_SECONDS (60 in 60 seconds by default), after which the answer is 429. Queries that fail are answered with Error: ... as plain text. overpass/README.md sets out what the language covers, how it differs from the main Overpass servers, and its limits.
An XAPI query, as part of the path or as the whole query string. It starts with node, way, relation or *, followed by filters in brackets.
| Filter | Meaning |
|---|---|
[bbox=west,south,east,north] |
Within this area |
[key=value] |
Has this tag |
[key=*] |
Has this key |
At least one filter is required. The answer is OSM XML, with what's needed to draw the objects found.
curl -g "http://localhost:8000/overpass/xapi/way[highway=primary][bbox=-1.1,50.78,-1.05,50.8]"
OAuth2 itself is standard, at /oauth2/authorize and /oauth2/token: see the README. These are added for clients to find it.
The server's RFC 8414 metadata: its endpoints, scopes and supported methods, as JSON.
The same as /oauth2/authorize and /oauth2/token, next to the API, where JOSM looks when it can't use discovery.
These paths are standard, but don't behave as they do on openstreetmap.org.
| Path | Here |
|---|---|
/api/0.6/notes..., /api/0.6/gpx..., /api/0.6/trackpoints, /api/0.6/user/gpx_files |
501 Not implemented, for GET and POST. capabilities still advertises limits for notes, as editors expect to find them. |
POST /api/0.6/changeset/{id}/expand_bbox |
410 Gone. It's been removed from the standard API too. |
/oauth2/application/{client_id} |
A web page for one of your applications, where openstreetmap.org has /oauth2/applications/{id} |
/api and /api/ answer with the text API root.
These are for people rather than programs. They're listed so that the routes are all accounted for.
| Path | Page | App |
|---|---|---|
/ |
Front page | frontpage |
/history |
Recent changesets | objectinfo |
/changeset/{id} |
One changeset | objectinfo |
/register/ |
Sign up | register |
/accounts/... |
Log in and manage a password | Django |
/admin/ |
Admin panel | Django |
/replication/ |
Links to replication data and downloads | replicate |
/replication/extracts |
Stored extracts, with download links | replicate |
/replication/planet |
Planet dumps, with download links | replicate |
/oauth2/applications, /oauth2/authorized_applications |
Manage applications, and those you've authorised. Login needed. | oauth2 |
pycromap is a separate Django project in pycromap/, with its own server (port 8082 under its Docker setup). It draws the map from pycrocosm's data, and none of its endpoints needs a login. All of them are GET only.
A vector tile, in the OpenMapTiles schema. A tile that isn't ready is rendered while the request waits, for up to TILE_REQUEST_WAIT seconds (600 by default). If the client goes away, the render is cancelled.
| Status | Meaning |
|---|---|
200 |
The tile, gzipped (Content-Encoding: gzip), for a zoom that isn't cached |
302 |
A redirect to the tile's file, for a cached zoom. The file can be cached, but the redirect can't, as each rendering has its own file. |
204 |
Nothing to draw. With X-Tile-Status: placeholder, the tile's summaries won't be built soon, so ask again later. |
404 |
That zoom isn't served, or the tile isn't on the grid |
503 |
The tile couldn't be rendered, or isn't ready yet. Try again after Retry-After. |
| Path | Gives |
|---|---|
/metadata |
JSON: where the map page opens (bounds), and the zoom limits |
/spec.json |
TileJSON for the tiles. Its tile URL starts at the root of the server. |
/style.json |
The MapLibre style, pointed at /spec.json |
/tilecache/{path} |
A cached tile's file. It's served by Django only while DEBUG is on: in production, the web server does this. |
/metadata, /spec.json and /style.json are sent with Cache-Control: no-store.
/ is the map itself. These arguments change only how it's drawn, so they cause no rendering.
| Argument | Meaning |
|---|---|
lang |
The main label's language: a code such as de or zh-Hans, or local for the object's own name (the default) |
lang2 |
The second label's language, in the same form. en by default. |
prow |
1 draws UK public rights of way in colours of their own |
projection |
mercator (the default), globe or vertical-perspective |
The view is kept in the address as #zoom/lat/lon.