Rust Map Maker

Developer API

Generate Rust maps, search the catalogue by biome and monument, upload your own worlds, and export any map as an image or a 3D mesh — with the in-game coordinate grid drawn on it. Address maps by seed and world size, or by id.

Already have a client for the v4 map-generation interface? Point its base URL at https://rust-mapmaker.com/v4 and keep your code — same paths, same bodies, same envelope, same status codes.

Generation
https://rust-mapmaker.com/v4
Export
https://rust-mapmaker.com/api
Auth
API key (paid plans)
Formats
png, webp, jpeg, glb
Rate limit
120 / min / key
Updated

1Quickstart

Four steps, start to finish.

  1. Subscribe to any paid plan (pricing) — the API is not on Free.
  2. Open your account pageDeveloper APICreate API key. Copy it there and then; it is shown once.
  3. Check it works — this call is free and never counts against your credits:
    export RMM_KEY=rmm_live_…
    
    curl -H "Authorization: Bearer $RMM_KEY"   https://rust-mapmaker.com/api/me

    It returns your plan, your remaining credits and what each pull type costs. If this works, your setup is correct.

  4. Pull a map.
curl -o map.png \
  -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?grid=1&gridlabels=1&monuments=landmarks"

You can address a map by seed and world size instead of our internal id — which is usually what you already have, since it is what sits in your server config:

curl -o map.png \
  -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/by-seed/3000/1337/export?grid=1&gridlabels=1"

X-API-Key: <key> works too, if your tooling is already written against that shape.

Paid plans only. A free account cannot create a key — the button on the account page is an upgrade link instead. See section 3 for what each plan includes.

2Addressing a map

Two ways, and every endpoint accepts both.

ByPathWhen
Seed + size/api/maps/by-seed/<size>/<seed>/…You know the world you want. This is what your Rust server config already holds.
Map id/api/maps/<id>/…You have a link to the map on this site — the id is the number in /map/<id>.

Seed+size resolves to the public map for that world, preferring the one generated on the newest game build. If we have not generated it yet you get a 404 map_not_generated telling you so — generate it once on the site and it becomes available to the API.

Resolve a world to its id without paying for a render (this lookup is free):

GET https://rust-mapmaker.com/api/maps/by-seed/3000/1337

Seed+size only ever resolves public maps. Custom generations, forks and uploads are not addressable this way by design — otherwise guessing seeds would tell you what other people had generated privately.

3Credits and what each call costs

Calls are metered in credits, not requests, because they are nowhere near the same cost to serve: a monument list is one read off a parsed file, a 4096px render is a full re-render, and a 3D mesh is that plus geometry.

Pull typeCredits
Monument list (JSON)1
Map metadata (JSON)1
Cliff placement data2
Image export, 512–1024px2
Image export, 1600–2048px5
Image export, 3072px10
Image export, 4096–8192px20
Raw heightmap grid5
3D terrain mesh (.glb)25
Monument model URLs (24h)2
Map generation request (/v4)5
Map lookup by id / seed+size / url (/v4)1
Catalogue search (/v4)2
Map upload (/v4)10

Your monthly allowance comes from your plan and resets on the 1st (UTC):

PlanCredits / monthRoughly
FreenoneAPI not included
Supporter5,0001,000 standard exports
Premium50,00010,000 standard exports
Organization150,00030,000 standard exports
Organization+500,000100,000 standard exports

Every response tells you where you stand, so you never have to guess:

X-Credits-Cost:      5
X-Credits-Limit:     50000
X-Credits-Remaining: 49831
X-RateLimit-Limit:   120        # requests per minute, per key
X-RateLimit-Remaining: 119

A failed call is never charged — a 4xx costs you nothing. Track spend by pull type on your account page.

4Generating a map

The generation API lives under its own base URL and speaks the v4 map-generation interface — the request and response shapes a lot of existing Rust map tooling is already written against.

Drop-in. If you already have a client for that interface, point its base URL at https://rust-mapmaker.com/v4 and keep your code. Same paths, same request bodies, same response envelope, same status codes. Your API key goes in the same header you already use.

Everything comes back wrapped, so a client can branch on one shape whether the call succeeded or not:

{
  "meta": { "status": "Success", "statusCode": 200, "errors": null },
  "data": { … }
}

meta.statusCode always mirrors the HTTP status. meta.errors is an array of plain-English strings when something went wrong — and occasionally on a success, when part of your request could not be honoured. Read it either way.

The endpoints

MethodPathWhat it does
POST/v4/mapsRequest a map for a seed and size
GET/v4/maps/{mapId}Fetch a map by id
GET/v4/maps/{size}/{seed}Fetch a map by seed and world size
GET/v4/maps/urlResolve a download URL back to its map
GET/v4/maps/limitsYour generation quota and concurrency
POST/v4/maps/searchSearch the catalogue by biome, monuments, terrain
GET/v4/maps/filter/{filterId}Re-run a saved search
POST/v4/maps/uploadUpload your own .map file
GET/v4/maps/customThe full custom-map settings object, at its defaults
POST/v4/maps/customGenerate with custom settings
GET/v4/maps/custom/saved-configsYour saved custom configs
POST/v4/maps/custom/saved-configGenerate from a saved config by name
GET/v4/maps/{mapId}/settingsThe settings a custom map was built with

Plus a handful this site adds — see What this API adds.

Asking for a map

curl -X POST "https://rust-mapmaker.com/v4/maps" \
  -H "X-API-Key: $RMM_KEY" \
  -H "content-type: application/json" \
  -d '{"size": 4500, "seed": 1337}'

Three answers, and the status code is the whole story:

CodeMeaningWhat is in <code>data</code>
200That map already exists and is ready nowThe full map object
201Queued — we are generating itmapId, queuePosition, state
409That map is already being generatedThe same status object
403 / 429Plan limit — world size, concurrency or monthly quotaThe reason, in meta.errors

On a 201 or 409 you get a mapId. Poll it until it is ready — the id you are given at request time is the same id the finished map carries, so there is nothing to swap over:

# 409 while it is still generating, 200 when it is done
curl "https://rust-mapmaker.com/v4/maps/$MAP_ID" -H "X-API-Key: $RMM_KEY"

state moves through InQueueGeneratingProcessingActive. queuePosition is how many jobs are ahead of yours. A real world takes minutes, not seconds, so poll every 10–15s rather than in a tight loop.

There is no staging branch here. Maps are generated on Rust's public branch only. A request with "staging": true is refused with a 400 rather than quietly answered with a public-branch map — a map that does not match the build your server is running is worse than an error.

5Custom map generation

Start from the defaults, change what you want, send the whole object back:

curl "https://rust-mapmaker.com/v4/maps/custom" -H "X-API-Key: $RMM_KEY" > settings.json
curl -X POST "https://rust-mapmaker.com/v4/maps/custom" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/json" \
  -d '{
    "mapParameters": { "size": 4500, "seed": 1337 },
    "customMapSettings": {
      "trySpawningOutpostInCenter": true,
      "removeRivers": true,
      "largeMonuments": [ { "type": "Launch Site", "blocked": true } ]
    }
  }'

Blocking works across every monument group — safe zones, large monuments, small monuments, harbors, water wells, caves, mountains, quarries, ice lakes, ruins and oil rigs — plus the biome mix, the road and rail toggles, powerlines, rivers, underground tunnels and a centred Outpost.

Settings that cannot be honoured are refused, not ignored

This is the part worth reading twice. If your settings object changes something this generator cannot do, the request comes back 400 naming exactly which fields — it does not generate a map that quietly ignores them.

{
  "meta": {
    "status": "Failed", "statusCode": 400,
    "errors": [
      "These settings cannot be honoured by this generator, so the request was REFUSED rather than generating a map that quietly ignores them: removeCarWrecks.",
      "Re-send with ?ignoreUnsupported=1 to generate anyway…"
    ]
  }
}

Sending the default settings object unchanged never trips this — only a field you actually altered. If you would rather have an approximate map than an error, add ?ignoreUnsupported=1 and the listed fields are skipped.

GET /v4/capabilities returns the full support matrix — every setting, whether it is honoured, and why not if it is not. Read it once when you build your integration rather than discovering it a field at a time.

Saved configs

Store a settings object under a name and generate from it later:

curl -X PUT "https://rust-mapmaker.com/v4/maps/custom/saved-configs/my-wipe" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/json" -d @settings.json

curl -X POST "https://rust-mapmaker.com/v4/maps/custom/saved-config" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/json" \
  -d '{"mapParameters":{"size":4500,"seed":1337},"configName":"my-wipe"}'

Saved configs are private to your account. GET the collection to list them, DELETE one by name.

Getting told when it is done

Rather than polling, have the finished map posted to you:

"webhook": { "enabled": true, "url": "https://example.com/hook", "secret": "…" }

We POST the completed map object to that URL, signed with X-RMM-Signature: sha256=<hex> — an HMAC of the exact body, using your secret, so you can prove the call came from us. Supply your own secret, or omit it and one is generated and returned once as data.rmmWebhookSecret on the queued response. The URL must be publicly resolvable; private and loopback addresses are refused. Delivery is retried, and it fires on failure too, so a generation that dies still tells your system rather than leaving it waiting.

6Finding a map

Search the catalogue on world size, biome mix, monument counts and terrain features. Everything is optional — send only what you care about:

curl -X POST "https://rust-mapmaker.com/v4/maps/search?page=0" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/json" \
  -d '{
    "searchQuery": {
      "size":     { "min": 4000, "max": 4500 },
      "islands":  { "min": 2, "max": 6 },
      "biomes":   [ { "type": "Snow", "settings": { "min": 20, "max": 60 } } ],
      "largeMonuments": [ { "type": "Launch Site", "selectionStatus": "Wanted" } ]
    }
  }'

Filterable: world size, monument count, the five biome percentages, land percentage, islands, caves, rivers, lakes, canyons, oases, mountains, ice lakes, water wells, swamps, gas stations, supermarkets, warehouses and lighthouses — plus any large monument as Wanted or NotWanted.

Results are paged 50 at a time; meta carries page, totalItems and lastPage. Each hit is a mapId, seed, size and a link — fetch the full object with GET /v4/maps/{mapId}.

Search returns worlds you can actually generate. Every result is a real seed and world size that reproduces that exact map — so anything you find here, you can hand straight to a server.

If a filter cannot be evaluated for part of the catalogue, that is stated in meta.errors on an otherwise successful response, and those maps are left out rather than included on the assumption they match. A filter that silently does nothing is worse than one that tells you its limits.

Saved searches

curl -X POST "https://rust-mapmaker.com/v4/maps/search/save" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/json" \
  -d '{"name":"big snowy","searchQuery":{ … }}'
# → { "data": { "filterId": "…" } }

curl "https://rust-mapmaker.com/v4/maps/filter/$FILTER_ID?page=0" -H "X-API-Key: $RMM_KEY"

By seed, or by a URL you already have

curl "https://rust-mapmaker.com/v4/maps/4500/1337" -H "X-API-Key: $RMM_KEY"
curl "https://rust-mapmaker.com/v4/maps/url?url=$ENCODED_URL" -H "X-API-Key: $RMM_KEY"

7Uploading your own map

Multipart, exactly as you would expect:

curl -X POST "https://rust-mapmaker.com/v4/maps/upload" \
  -H "X-API-Key: $RMM_KEY" \
  -F "map=@my-world.map" \
  -F "note=wipe 2026-08-14"

Or, if multipart is awkward from your language, post the file as the raw body — this site accepts both:

curl -X POST "https://rust-mapmaker.com/v4/maps/upload?note=wipe" \
  -H "X-API-Key: $RMM_KEY" -H "content-type: application/octet-stream" \
  --data-binary @my-world.map

You get back an id, a thumbnail URL and a permanent downloadUrl you can drop straight into a server's level URL. Re-uploading the same file returns the map you already have rather than making a second copy. How many you may hold at once comes from your plan.

8What this API adds

Everything above is the shared interface. These are additions — same key, same envelope, same base URL.

EndpointWhat it gives you
GET /v4/capabilitiesThe full support matrix in machine-readable form — every custom setting, whether it is honoured, and the exact behaviours that differ. Free, and never counts against your credits.
GET /v4/maps/{id}/monumentsThe monument list with category, keycard tier, landmark flag, world height and rotation — more than the standard shape carries.
GET /v4/maps/{id}/imageA rendered map image with the in-game coordinate grid, labels, monument filters and road/rail/powerline overlays — every option in The export endpoint applies.
GET /v4/maps/{id}/terrain.glbThe terrain as a 3D glTF mesh.
GET /v4/maps/{id}/heightmapThe raw heightmap grid.
GET /v4/maps/{id}/preview
GET /v4/maps/{id}/thumb
The rendered images, authenticated by API key — so these work for your own private maps, which the cookie-based site URLs cannot serve to a machine.
GET /v4/maps/{id}/statusPoll a generation directly instead of reading it off a 409.
POST /v4/maps/{id}/cancelStop a generation you no longer want.

Generator features with no equivalent in the shared interface

These live under an rmm key inside customMapSettings, namespaced so they can never collide with a standard field:

SettingWhat it does
rmm.placements[]Pin a monument to a point on the map — { stem, nx, nz, r } in normalised coordinates. Add flatten to terraform a pad so it seats where the seed has no room for it, and rot to force its facing (snapped to 15°).
rmm.forceMonuments[]Guarantee a monument spawns, even on a world below its normal minimum size.
rmm.oceanLevelRaise or lower sea level.
rmm.terrainRelief1-5 — smoother terrain. Lowers the hills and mountains toward the waterline while the world is generated, so slopes are gentler and more of the map is buildable. The coastline is left where the seed put it; 1 takes the edge off, 5 is near-flat plains.
rmm.excludeMonuments[]Blacklist by prefab name directly, for anything the typed groups do not cover.
rmm.snapshotsLive generation preview — watch terrain and monuments appear as the world is built.

The placeable monument list is in GET /v4/capabilities under forceableMonuments.

9Behaviour worth knowing

Nothing here is a surprise if you read meta.errors, but these are the things integrators ask about.

canDownload: false is not an error

A map object can come back with "canDownload": false and a null download URL. The map is not lost and nothing has gone wrong.

POST the same seed and size again — you get the same id and the same world back, downloadable. A seed and a world size reproduce a map exactly, so this is always safe to do.

Fields we will not guess

A field we cannot determine for a map is left out, not sent as zero. A zero is an answer a filter acts on; a missing field is honest. Same reason monuments comes back null rather than [] when a map's monument data is not available — an empty array would claim the world has no monuments.

Ids

Map ids are 32-character hex, stable for the life of the map, and the id you are handed when you request a generation is the id the finished map keeps. Every per-map route also accepts this site's own numeric map id, if that is what you already have from a /map/<id> link.

Errors

CodeMeans
400Bad request — seed or size out of range, or settings that cannot be honoured. The specifics are in meta.errors.
401No key, or a key that is not valid.
402Your plan carries no API credits. This is an upgrade, not a wait.
403Plan limit — usually world size, or a Premium-only feature.
404No such map.
409Already generating, or the exact settings you asked for are already owned privately by someone else.
429Rate limit, or your monthly generation quota. Retry-After tells you which.
503The generator is at capacity. Retry shortly.

GET /v4/maps/limits shows your concurrent and monthly generation allowance and what you have used. It is free and keeps answering even when your credits are spent — which is exactly when you need it to tell you why everything else stopped.

10The export endpoint

GET https://rust-mapmaker.com/api/maps/<id>/export

Every parameter is optional, and every overlay is opt-in. With no parameters at all you get a clean 2048px PNG of the map — no grid, no markers, no roads. Add the layers you want.

ParameterValuesDefaultWhat it does
w512 · 1024 · 1600 · 2048 · 3072 · 4096 · 81922048Output width in pixels, square. Allowlisted — any other value is rejected.
formatpng · webp · jpegpngPNG keeps the terrain crisp; JPEG is ~20× smaller if you are posting it somewhere.
grid1offDraw the Rust coordinate grid, 146.3m cells.
gridlabels1offLabel every cell A0, B0, C0 … the way the in-game map does. Turns the grid on for you.
gridcolor#rrggbb#ffffffGrid line colour.
gridlabelcolor#rrggbb#ffffffCell label colour.
gridopacity0 – 10.22Grid line opacity. Labels keep their own dark halo so they stay readable over snow.
monumentsall · none · landmarks · large · safe · keyednoneWhich monuments get a marker. Off by default like every other overlay. See section 4.
categorieslarge · small · tiny · caves · unique · ice_lake · underwater · customNarrows the preset to these categories.
tierssafe · red · blueNarrows the preset to these keycard / safe-zone tiers.
includenames, comma separatedForce these on, whatever the filters above decided.
excludenames, comma separatedForce these off. Always wins.
labels1offDraw monument NAMES instead of icons, in Rust's in-game map style.
roads1offOverlay the road network.
rails1offOverlay the rail network.
powerlines1offOverlay powerlines.

The response carries X-Monuments-Drawn, the number of markers actually drawn. It is the quickest way to confirm a filter did what you meant.

11Choosing which monuments appear

Filters apply in a fixed order, so combinations are predictable:

  1. monuments= picks the starting set
  2. categories= narrows it
  3. tiers= narrows it further
  4. include= adds those back, whatever steps 1–3 decided
  5. exclude= removes those — always wins

Names match case-insensitively on any part of the name, so include=oil rig catches both the small and the large rig.

PresetWhat you get
noneNo markers. The default — also the base for an explicit include= list.
allEvery monument on the map.
landmarksThe monuments that draw as a full icon — Launch Site, Airfield, Outpost, the rigs. Not the roadside clutter.
largeWhat players mean by "large monuments". A curated list, deliberately not the generator's size category, which also sweeps in ranches and cabins.
safeSafe zones: Outpost, Bandit Camp, Fishing Villages, Apartment Complex.
keyedAnything behind a keycard puzzle, green tier included.

Worked examples

GoalQuery
Clean map, no markers?w=2048 (the default)
Just the grid?grid=1&gridlabels=1
Only the big stuff?monuments=landmarks
Every monument?monuments=all
Everything except caves and wells?monuments=all&exclude=Cave,Water Well
Just three, named?include=Launch Site,Airfield,Oil Rig
Safe zones and red-card monuments?monuments=all&tiers=safe,red
Large monuments, minus the one you dislike?monuments=large&exclude=Junkyard

12Track a live server

GET https://rust-mapmaker.com/api/maps/../servers/<ip>:<port>

Give it a Rust server's connect address and it returns that server's current map. When the server wipes, the map id changes by itself, so polling this is how an embedded map or 3D scene stays in sync with a wipe cycle without anyone touching it.

curl -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/servers/104.143.2.86:28015"
{
  "server": { "name": "Scrapland x10000000 ...", "addr": "104.143.2.86:28015",
              "players": 257, "last_seen": "2026-08-07 02:46:01" },
  "map": {
    "id": 9639, "seed": 75902336, "size": 3750,
    "kind": "procedural",
    "renders_available": true, "file_available": true,
    "endpoints": { "export": "...", "markers": "...", "terrain_glb": "..." }
  },
  "status": "ok"
}

This call is free and carries an ETag. Poll it with If-None-Match and an unchanged map answers 304 — so watching for a wipe costs nothing at any interval you like.

Also accepts
?ip=&port=Query form, if that suits your client better.
/api/servers/<ip>Port omitted. Works when that IP has exactly one tracked server; if it hosts several you get 409 with the candidates rather than a neighbour's map.

What you get back when it is not simple

statusMeans
okMap available; use the endpoints.
map_pendingWe track the server but have not finished ingesting its current map. Keep the old one on screen and poll again.
map_unavailableThe server's current map is not publicly viewable.
404 server_not_trackedNo map is published for that address. Either it is not in our index (coverage is live servers carrying real population, so a brand-new or tiny server may not appear), or we index it but cannot verify its world well enough to publish a map. Ask us to start tracking an address with POST https://rust-metrics.com/api/servers/request.
409 ambiguous_addressSeveral servers on that IP — include the port.

map.kind is custom when the world is not a plain seed+size generation, which tells you it cannot be reproduced from the seed alone. file_available:false means the .map bytes were released from storage — images and 3D still work, only the level file is gone.

13Listing what is on a map

GET https://rust-mapmaker.com/api/maps/<id>/markers

Returns JSON describing every monument on the map plus the buckets it belongs to, so you can build a filter without guessing at names. Same privacy rules as the export.

{
  "map_id": 9595,
  "size": 3000,
  "grid": { "cell_metres": 146.3, "columns": 21 },
  "counts": {
    "total": 55,
    "by_category": { "large": 13, "small": 10, "tiny": 22, ... },
    "by_tier": { "none": 39, "red": 8, "blue": 4, "safe": 4 },
    "landmarks": 22
  },
  "monuments": [
    { "name": "Launch Site", "category": "large", "tier": "red",
      "icon": "rocket", "landmark": true, "large": true, "keyed": true,
      "x": -412.5, "z": 233.1, "y": 12.4, "rot": 252.9 }
  ]
}

x and z are world metres, origin at the map centre, z increasing north.

yWorld height of the prefab root in metres, 0 = sea level. It is the origin, not the ground under it — dug-in monuments are negative (Military Tunnel −23.2, Missile Silo −29.7), so treat it as a grounding hint and read the terrain for the surface.
rotThe monument’s facing: Unity Y-euler yaw in degrees, 0–360. Worldgen rotates every monument, so position alone puts a building in the right place pointing the wrong way. See Monument models for the exact two lines that place a mesh correctly. Custom RustEdit markers report 0 — a marker has no facing.

143D terrain export

GET https://rust-mapmaker.com/api/maps/<id>/terrain.glb?res=513

Returns the map's terrain as a binary glTF — a real 3D mesh at true world scale, with the map render baked on as the texture. It is a single self-contained file: it opens in Blender, Unity, Godot, Windows 3D Viewer, Xcode and three.js, offline, with nothing calling back to us after the download.

resMesh resolution per side: 129 · 257 · 513 · 1025 (default 513, ~263k vertices, ~14MB). Capped at the map's own heightmap grid, so asking for more than the source holds costs nothing and gives you the real ceiling.
exaggerationVertical scale, 1–4 (default 1.5). Rust elevation is gentle next to a map's width — a 3000m map might span only 70m vertically — so at true 1:1 it reads as a flat sheet. 1.5 is what the 3D viewer on this site uses. Pass 1 for true-to-game geometry.
flattenOn by default. Rust levels the ground under each big monument and our heightmap loses that pad in the downsample, so a model you place ends up with terrain poking through it. The export reinstates the pads from the real prefab footprints. Each pad levels to the median of the ground it already covers — so it flattens without raising or lowering the terrain — and the surrounding ground eases into it over 45m. Pass flatten=0 for the raw heightmap.
ScaleReal metres. A 3000m map is 3000 units across, Y up, 0 = sea level.
Cost25 credits per pull, whatever the resolution.

Monument positions come from the markers endpoint in the same coordinate space, so you can place your own markers or models in the scene. The mesh itself is terrain only — the monument buildings you see on the demo are not inside the .glb.

Monument models

You can still render the buildings: we serve the model library from our side. One call returns signed URLs for a whole library, which you drop straight into a GLTFLoader.

curl -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/assets/monuments-tex"

{
  "library": "monuments-tex",
  "expires_in": 86400,
  "models": {
    "Launch Site": { "file": "launch_site.glb",
                     "url": "/monuments-tex/launch_site.glb?exp=…&t=…&sig=…" }
  }
}
monuments-texTextured — start here. ~28 MB, 40 monuments. Carries the game’s own baked albedo, so a monument arrives looking like it does in Rust instead of as a grey block. 33 are textured; the 7 the game builds no low-detail LOD for have no baked texture to extract and are included untextured so nothing is missing from your map.
monumentsUntextured, ~12 MB, same 40 keys and the same geometry. Still served and still supported — use it if you want to shade the buildings yourself, or if you are already integrated against it.
monuments-detailThe high-detail tier. All 40 monuments rebuilt from their individual prefab parts — not the game’s low-poly shell — with per-part textures. Same keys and same coordinates as the sets above, so switching is a one-word change. ~3.75 MB per monument (a full map is roughly 140 MB), so fetch only the monuments your map has — see Taking one monument. Requires a meshopt decoder, see below. The live example has a Detail control that cycles through the tiers on a real map, which is the honest way to decide whether the extra weight is worth it for what you are building.
monuments-hiFull prefab detail, much larger, untextured.
cliffsCliff and large-rock formations.
Cost2 credits per library, per call.
LifetimeURLs last 24 hours and are bound to the IP that requested them.

The two monument libraries are interchangeable — same keys, same geometry, same coordinate convention — so switching is a one-word change to the URL and nothing else in your scene code moves.

Call this from your server, not the browser — the URLs are tied to the requesting IP, and your API key must never ship to a page. Fetch the manifest when you build or cache the page, then serve those URLs to visitors.

Taking one monument, or a few

A whole library is more than most pages need — a map has twenty-odd monuments, and the detailed set is several megabytes each. Ask for the ones your map actually has, by the same names the markers endpoint gives you:

# just the ones on this map
GET https://rust-mapmaker.com/api/assets/monuments-detail?names=Launch%20Site,Airfield,Outpost

# or a single monument
GET https://rust-mapmaker.com/api/assets/monuments-detail/Launch%20Site
  -> { "library": "...", "expires_in": 86400,
       "model": { "name": "Launch Site", "file": "launch_site.glb", "url": "…" } }

Filenames work too (launch_site.glb), and matching ignores case. Anything you ask for that this library does not carry comes back in an unknown array rather than being quietly dropped, so a typo does not turn into a missing building you debug in your renderer.

Selecting costs the same as taking everything — 2 credits either way. It exists so you can transfer less, not so we can charge per monument; fetching twenty monuments one at a time would otherwise cost ten times a single bulk call, which would be a daft thing to charge for doing the tidy thing.

The models are served from us and are not redistributable: they are derived from Facepunch's game files, so we can show them to you but cannot hand them over. Everything else in the API — the terrain mesh, the images, the map data — is yours to host.

Placing one correctly

A monument needs three things, and only the first is obvious. /markers gives you position and rot, the monument's facing — worldgen rotates every one of them, so position alone puts the building in the right spot pointing the wrong way. The models are also exported X-negated out of Unity, so they arrive mirrored until you undo it. This is exactly what the viewer on this site does:

// monument came from GET /api/maps/<id>/markers
const gltf = await new GLTFLoader().loadAsync(signedUrl);   // /api/assets/monuments-tex
const obj = gltf.scene;

// 1. Un-mirror. Our GLBs are exported X-negated out of Unity.
obj.scale.x = -1;

// 2. Face it. rot is Unity Y-euler degrees; +Ry in three matches it once (1) is done.
obj.rotation.y = monument.rot * Math.PI / 180;

// 3. Ground it. monument.y is the prefab ROOT, which for a dug-in monument sits
//    far below the surface — sample your terrain instead and clamp offshore
//    platforms (oil rigs) up to sea level.
obj.position.set(monument.x, groundHeightAt(monument.x, monument.z), monument.z);
scene.add(obj);

// Do NOT assign your own material to a monuments-tex mesh — the texture is
// already on it. Overwriting o.material is what makes these render grey.

Both corrections or neither: undoing the mirror without applying rot leaves every building facing north, and applying rot without undoing the mirror reflects the rotation instead of reproducing it. Symmetric monuments look fine either way, which is what makes this easy to miss — check against the harbour or launch site, not the airfield.

Using monuments-detail: two requirements

The detailed models are compressed with EXT_meshopt_compression and KHR_mesh_quantization. That is what makes the tier shippable at all — it takes the geometry of a monument from about 6.8 MB to 1 MB with no visible loss. It also means two things have to be true on your side, and both fail silently: the file downloads with a 200, and then simply never appears.

1. Give your loader a meshopt decoder. In three.js:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';

const loader = new GLTFLoader().setMeshoptDecoder(MeshoptDecoder);

Quantization needs nothing — every current loader handles it. Meshopt does not. Without the decoder, glTF parsing fails for that file and nothing renders.

2. Allow WebAssembly in your CSP. The decoder is a wasm module, and a policy of script-src 'self' refuses to compile any wasm. Add 'wasm-unsafe-eval' — it permits WebAssembly and does not permit eval() of JavaScript, so it is the narrow directive rather than 'unsafe-eval':

Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; …

We tripped over this one ourselves. Every model returned 200, and every single one failed to place, with a single CompileError in the browser console and nothing whatsoever server-side. If your detailed monuments are missing while the network tab looks perfect, check the console for that error before you check anything else. monuments-tex and monuments are not compressed and need neither of these.

If the monuments look too dark

Rust bakes shadow and ambient occlusion into the textures — measured across the library, mean luminance is 78–99 out of 255 — and PBR lighting then multiplies that already-dark texture down again, so a monument reads as a black smudge from any angle but the sunlit one. Raising your lights does not fix it: that is a multiply either way, so it blows out the lit faces and does nothing for the shadowed ones.

Feed the albedo back through emissive instead. That adds a floor which does not depend on light direction, so every face keeps its own colour:

// once per loaded model — clones share materials, so do it on the template
obj.traverse((o) => {
  if (!o.isMesh || !o.material.map) return;
  o.material.emissiveMap = o.material.map;          // NOT a bare emissive colour
  o.material.emissive = new THREE.Color(0.85, 0.85, 0.85);
  o.material.needsUpdate = true;
});

emissive multiplies emissiveMap, which is the whole point — an emissive factor with no map emits flat grey and washes the monument out to near-white, losing exactly the colour this is meant to rescue. We ship the textures faithful to the game rather than pre-brightened, so this stays your choice; 0.85 is what our own viewer uses.

If a monuments-tex monument renders grey, it is one of two things. Either your scene code assigns its own material over the loaded mesh (that overwrites the texture — shade only meshes with no map), or your page's CSP is missing blob:. A .glb carries its texture embedded and every glTF loader extracts it to a blob: URL, so a policy without blob: on img-src and connect-src blocks the image while the mesh still loads — it looks like a bad export and is a blocked request. Browser console will say “Refused to load blob:…”.

See a live example → — a real map from this site, rendered from the file the API returns, with the code it uses. It has a Textured toggle that swaps between the two libraries live, so you can see exactly what the difference is before you pick one.

Putting it on your own site

Pull the file once, host it yourself, and serve it from your own site. Nothing here calls back to us — after the download the map is a static asset like any image, so it costs you 25 credits total no matter how many people view it.

The example below is complete and working. Save the .glb next to it as map.glb:

<!doctype html>
<meta charset="utf-8">
<style>html,body{margin:0;height:100%;background:#0d0b0a}
#map3d{width:100%;height:100vh;display:block}</style>
<canvas id="map3d"></canvas>

<script type="importmap">
{"imports":{"three":"/three/build/three.module.js","three/addons/":"/three/examples/jsm/"}}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const canvas = document.getElementById('map3d');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0b0a);
scene.add(new THREE.HemisphereLight(0xffffff, 0x33404d, 1.1));
const sun = new THREE.DirectionalLight(0xffffff, 1.6);
sun.position.set(1, 2.2, 1.4);
scene.add(sun);

const camera = new THREE.PerspectiveCamera(45, 1, 1, 100000);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

const gltf = await new GLTFLoader().loadAsync('map.glb');
scene.add(gltf.scene);

// The mesh is in real metres, so its bounding box IS the map size.
const box = new THREE.Box3().setFromObject(gltf.scene);
const size = box.getSize(new THREE.Vector3());
const centre = box.getCenter(new THREE.Vector3());
const span = Math.max(size.x, size.z);
camera.position.set(centre.x + span * 0.7, centre.y + span * 0.55, centre.z + span * 0.7);
controls.target.copy(centre);
controls.update();

function resize() {
  const w = canvas.clientWidth, h = canvas.clientHeight;
  if (canvas.width !== w || canvas.height !== h) {
    renderer.setSize(w, h, false);
    camera.aspect = w / h; camera.updateProjectionMatrix();
  }
}
renderer.setAnimationLoop(() => {
  resize(); controls.update(); renderer.render(scene, camera);
});
</script>
CoordinatesRaw Rust world metres, no axis flip: a position (x, z) from /markers is (x, height, z) in the mesh. Y is up and 0 is sea level, so monument coordinates drop straight in unchanged.
FramingThe mesh is in real metres, so Box3.setFromObject() gives you the true map size — that is what the example uses to place the camera, and it works for any world size without tuning.
Sea levelY = 0. Anything below is underwater; a flat plane at Y = 0 makes a convincing ocean.

If your site sends a Content-Security-Policy, allow blob: on connect-src and img-src. A .glb carries its texture embedded, and every glTF loader extracts it to a blob: URL — blocked, the mesh still loads but renders untextured, which looks like a broken export rather than a blocked request. You also need worker-src blob: if your loader decodes off the main thread.

Adding a marker at a monument is then three lines:

// monument came from GET /api/maps/<id>/markers — x/z are used as-is
const pin = new THREE.Mesh(
  new THREE.SphereGeometry(25),
  new THREE.MeshBasicMaterial({ color: 0xd65a32 }),
);
pin.position.set(monument.x, 150, monument.z);   // same x/z the API gave you
scene.add(pin);

What a full 3D viewer costs

Two calls, once — then it is static files on your own server.

CallWhenCredits
/terrain.glbonce per wipe25
/markersonce per wipe, for monument positions1
/api/servers/<ip>:<port>poll to notice the wipefree

26 credits per wipe, whatever your traffic — the files are yours and you serve them. On the Supporter plan that is 192 map-wipes' worth a month; on Premium, 1,923.

Customising it is your code, not our API: the mesh is a normal glTF, so lighting, sky, camera and controls are whatever you write. The example above is a starting point, not a fixed viewer.

Also available, if you would rather build your own geometry from raw data:

EndpointWhat it isCredits
/api/maps/<id>/heightmapRaw elevation grid, binary.5
/api/maps/<id>/cliffsCliff placements.2

15Live examples

Every image below is a real render from map 9595 (seed 1337, 3000m), produced by the same renderer the API uses — so if an export ever breaks, it breaks here first.

The bare endpointNo parameters. Clean map, nothing drawn on it.
The bare endpoint
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export"
Grid with cell labelsCoordinates that match what players read in game.
Grid with cell labels
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?grid=1&gridlabels=1"
Grid and landmarksWhat you want for a wipe announcement.
Grid and landmarks
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?grid=1&gridlabels=1&monuments=landmarks"
Every monumentThe full set, icons colour-coded by tier.
Every monument
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?monuments=all"
Names instead of iconsRust's in-game map styling, tier-coloured.
Names instead of icons
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?labels=1&monuments=large"
Roads and railsRoute planning.
Roads and rails
curl -o map.png -H "Authorization: Bearer $RMM_KEY" \
  "https://rust-mapmaker.com/api/maps/9595/export?roads=1&rails=1"

16Limits, caching and errors

Rate limit120 requests per minute per key. Over that returns 429 rate_limited with Retry-After.
Render timeRoughly 0.3–0.9s for an image, ~0.7s for a 3D mesh. Nothing is queued — the response is the file.
CachingPublic maps are sent immutable, so a repeated identical URL is served from cache rather than re-rendered. Vary a parameter and it renders again — and costs credits again.
KeysUp to 5 active keys per account. Revoking is immediate. A key is shown in full once, at creation — we only store its hash, so a lost key is replaced, not recovered.
Free callsGET /api/me (key check) and GET /api/maps/by-seed/<size>/<seed> (id lookup) cost nothing and are never credit-gated.

Errors are JSON with a machine-readable code:

StatusCodeMeans
400bad_width · bad_format · bad_monuments · bad_color · bad_seed_sizeA parameter was outside its allowed set. The message lists what is allowed.
401no_api_key · invalid_api_key · revoked_api_keyMissing, unrecognised, or revoked key.
402plan_requiredYour plan carries no API credits. Upgrading is the fix; waiting is not.
404not foundNo such map, or it is private and not yours. The two are deliberately indistinguishable.
404map_not_generatedWell-formed seed+size, but we have not generated that world yet.
409meta unavailableThe map has not been parsed yet, so monuments and the grid cannot be placed.
410The map's file has been released from storage. Ask for a fresh link on the map's page.
429rate_limited · credits_exhaustedToo fast, or out of monthly credits. The body says which.

17Notes

  • The grid pitch is 146.3m, which is what the Rust client uses to letter the map. Cell labels therefore match what players read in game.
  • The grid covers the playable world. Offshore monuments — the oil rigs — legitimately sit outside it, because the rendered image extends past the world edge so they are not clipped.
  • Marker colours match this site's map viewer: red for red-card monuments, blue for blue-card, green for safe zones.
  • Live-server maps mirrored from the tracker can be exported like any other public map, but their .map file is not downloadable.

Something missing, or a parameter you need? Open a ticket.