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 page → Developer API → Create 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 InQueue → Generating → Processing → Active. 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.

Making the map viewable by anyone

Maps you generate are private to your account: their url opens for you and nobody else. Add "public": true to the request and the finished map's url opens for anyone, no sign-in needed, which is what you want for a map vote in your Discord:

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, "public": true}'

Public means view: the map page, the render, the monument list and the 3D terrain, and the link unfurls into a card in Discord. It never hands out the .map file or the level URL. A public map is not listed in the Atlas, the map catalogue, sitemaps or search, and it adds nothing to the public counts, but it is not secret either: anyone who tries its address can open it. For a link only the people you send it to can open, use a view link from the map page instead. The map object says which it is in isPublic. It applies to maps your account owns: if the map already exists and is yours it is made public straight away, and if it is still generating it becomes public the moment it is ready. A world another account generated first stays private to its first owner. Change it later with PUT or DELETE /v4/maps/{id}/public (see What this API adds). Works the same on POST /v4/maps/custom and POST /v4/maps/custom/saved-config.

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.
PUT /v4/maps/{id}/public
DELETE /v4/maps/{id}/public
Make one of your maps viewable by anyone at its url, or private again. View only: the page, render, monuments and 3D, never the file or the level URL. Returns { mapId, isPublic, url, pending }; pending: true means the map is still generating and the change applies when it is ready. The same thing as "public": true on a generation request, and the same as the Share card on the map page. Free.

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 } in normalised coordinates, plus rot to force its facing (snapped to 15°). Every pin terraforms a pad at the point and spawns the monument on it (flatten is implied). Nothing is moved silently: the finished map carries rmm.placementReport[], one entry per pin with status (placed within toleranceMetres of the pin, fallback on the map but distMetres away, failed not on the map, unverified the finished map could not be checked), landedAt, duplicates (other copies of it on the map), reasons[] codes (ocean, inland, offshore, edge, too_big, crowded, shifted, seat, early_pass, excluded, no_shore, shore_shift, and from the generator itself anchors_failed, no_prefab, shifted_by_water, shifted_by_monument) and a readable message. Harbors, fishing villages, the ferry terminal and the lighthouse are coastal: they get no pad and go to the nearest spot on the shoreline where they fit, so pin them on the water's edge (shore_shift = the pin was not such a spot and the nearest one was used, no_shore = there was none near the pin). Pinning a lighthouse keeps the map's natural lighthouses; they are listed under duplicates and are never a failure.
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. An entry with requires needs a generator capability (the lighthouse); available: false means the generator does not have it right now, and a request naming that monument is answered 503 instead of queued. Pins on harbors, fishing villages and the desert and arctic bases are refused the same way while that capability is missing; forcing them without a pin still works.

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 · custom—Narrows the preset to these categories.
tierssafe · red · blue—Narrows the preset to these keycard / safe-zone tiers.
includenames, comma separated—Force these on, whatever the filters above decided.
excludenames, comma separated—Force 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). The monument models are in the same prefab-root space, so this is the exact height to put the model’s origin at on true-scale terrain; see Placing one correctly for the formula against our exported terrain.
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

Changed Sep 22 2026, action needed if you integrated before then: every library below — including monuments and monuments-tex, which never needed this — is now built from the game's own prefab parts instead of its baked low-detail shell, and is compressed with EXT_meshopt_compression + KHR_mesh_quantization, with WebP textures where a library carries them. The old plain/PNG format these two used to serve is retired: it is not available at any URL, old or new. If your integration predates this date and only pulled monuments or monuments-tex, add the two requirements under What your loader needs before your next deploy — every model your existing code fetches will otherwise download with a 200 and then simply never appear, which is the standard symptom for a missing meshopt decoder. Nothing else changed: same manifest shape, same 40 keys, same filenames, same coordinate convention, same signed-URL flow. The live example already runs the new format.

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. ~35 MB, 40 monuments, ~0.9 MB each. Every monument rebuilt from its own prefab parts with the game’s real textures AND paint colours (vegetation and shadow-caster proxies left out), not the old baked low-detail shell — noticeably more detailed at close range for about the same weight the old shell-based set carried. All 40 are textured now; none fall back to grey. Requires a meshopt decoder and WebP texture support — new as of Sep 22 2026, see below.
monumentsUntextured, ~13 MB, same 40 keys and the same geometry as monuments-tex — flat concrete grey, for shading the buildings yourself. Also requires a meshopt decoder now (WebP does not apply — there is no texture).
monuments-detailThe high-detail tier. A tighter vertex budget than monuments-tex would waste on a distant monument, spent instead on finer parts up close — same prefab-parts pipeline, same paint colours, larger textures. Same keys and same coordinates as the sets above, so switching is a one-word change. ~2 MB per monument (a full map is roughly 80 MB), so fetch only the monuments your map has — see Taking one monument. 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.
cliffsRock formations and cliffs: the big standalone formations, jutting and coastal cliffs, sea stacks, icebergs and sea ice, hill cliffs and the Lake A/B/C walls. Each type comes three ways: <type> for close range, <type>__mid and <type>__far for distance. Plain glTF (no extensions), with normals, in the prefab root frame and X-negated like the monuments. Placements: /api/maps/<id>/cliffs.
Cost2 credits per library, per call.
LifetimeURLs last 24 hours and are bound to the IP that requested them.

All four monument libraries are interchangeable — same keys, same coordinate convention, same loader requirements as of Sep 22 2026 — so switching is a one-word change to the URL and nothing else in your scene code moves. monuments-tex and monuments share geometry with each other (textured vs. flat grey); monuments-detail and monuments-ultra are the same pipeline at a higher vertex budget and larger textures, not a different model.

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 by the ROOT, kept at its true depth below the surface. The model
//    is in the prefab's own space, so its ground-level parts sit (trueGround - y)
//    above the root: 0 m for most monuments, ~56 m for Military Tunnels, and a
//    rig's deck at sea level with its legs down to the seabed.
//    rendered = the height YOUR terrain has at the centre (water: 0)
//    trueGround = the game's height there. Our heightmap and terrain exports
//    currently store heights at HALF scale (sea level is right), so
//    trueGround = 2 x the decoded value; if you render them unscaled, rendered
//    and trueGround differ by that factor and this line absorbs it.
const rendered = Math.max(0, groundHeightAt(monument.x, monument.z));
const trueGround = rendered * 2;
obj.position.set(monument.x, rendered - (trueGround - monument.y), 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.

What your loader needs (every library, since Sep 22 2026)

Every library — monuments, monuments-tex, monuments-detail, monuments-ultra — is compressed with EXT_meshopt_compression and KHR_mesh_quantization, and every textured one uses WebP (EXT_texture_webp, a required extension — the model will not carry a PNG/JPEG fallback). three.js, Babylon.js and every current browser read WebP with no extra setup; check your loader if you use something else. Meshopt 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 monuments are missing while the network tab looks perfect, check the console for that error before you check anything else — including monuments and monuments-tex now: before Sep 22 2026 they were plain glTF and needed neither of these, but they are built the same way as the detail tiers now and both requirements apply to all four libraries equally.

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
  // scaled by the material colour: monuments-detail paints near-white textures
  // (cranes, containers) through it, and a flat grey emissive would undo that
  o.material.emissive = o.material.color.clone().multiplyScalar(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.
410—The 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.