{"openapi":"3.1.0","info":{"title":"Ultimaps Public API","version":"1.0.0-beta.1","description":"Render vector maps of countries, states, counties, and other administrative\nregions with a single HTTP call. Region colors or raw data values in,\nPNG/SVG out.\n\n## Quick start: no key, no signup\n\n```bash\ncurl https://api.ultimaps.com/v1/renders \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mapId\": \"united-states\", \"regions\": {\"US-CA\": \"#1D4ED8\", \"US-TX\": \"#F59E0B\"}}' \\\n  -o map.png\n```\n\nThat works right now, anonymously. You get a PNG up to 1600 px wide with an\n\"ultimaps.com\" attribution, 30 renders per hour per IP. Add a key for\nbigger canvases, higher quotas and, on Pro, SVG and clean output:\n\n```bash\ncurl https://api.ultimaps.com/v1/renders \\\n  -H \"Authorization: Bearer $ULTIMAPS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mapId\": \"united-states\",\n       \"choropleth\": {\"values\": {\"California\": 39.5, \"Texas\": 30.5, \"Florida\": 22.6}},\n       \"title\": {\"text\": \"Population by state (M)\"}}' \\\n  -o map.png\n```\n\n## Get an API key\n\nCreate a key in Studio, under Workspace → API:\nhttps://studio.ultimaps.com/account/workspace/api. Free and Pro accounts\nboth mint keys there. The key is shown once, so copy it when you create it.\n\nSend it as `Authorization: Bearer um_live_…`. `X-API-Key: um_live_…` is an\naccepted alias for tools that cannot set the Authorization header. Keep\nkeys server-side. You can rotate a key from the same screen, and the old\none keeps working for 72 hours.\n\n## Tiers\n\n| - | Keyless | Free key | Pro key |\n|---|---|---|---|\n| Auth | none | `Authorization: Bearer um_live_…` | same |\n| Formats | PNG | PNG | PNG, SVG |\n| Attribution | full watermark | full watermark | none |\n| Canvas | ≤ 1600×1600 px, scale 1 | ≤ 1600×1600 px, scale ≤ 2 | ≤ 4000×4000 px, scale ≤ 4 |\n| Rate limit | 30/hour per IP (IPv6 /64), burst 5/min | 10/min | 30/min |\n| Daily cap | none | 50 | 1,000 |\n| Monthly renders | none | 500 | 5,000 |\n| Concurrency | 1 | 2 | 2 |\n\nSVG output is Pro-only. PNG dimensions are exactly\n`round(width×scale) × round(height×scale)`. A request that carries an\ninvalid key is always `401`. It never falls back to the keyless tier. The\ndocumented rate limits are the ones you hit in practice, so a busy client\ngets a predictable `429 rate_limit_exceeded` rather than\n`concurrency_limit_reached`. There is no sandbox environment. Use\n`dryRun: true` to test any request for free, without spending quota.\n\n## The flow\n\n1. `GET /v1/maps`: pick a `mapId` (\"united-states\", \"europe\",\n   \"california-counties\"). Keyless, cached an hour.\n2. `GET /v1/maps/{mapId}`: the region keys and titles you will color\n   (`q` narrows by name, code or alias). Keyless, cached an hour.\n3. `POST /v1/renders` with `dryRun: true`: a free pre-flight showing what\n   matched, what did not, the class breaks and the legend.\n4. `POST /v1/renders`: the image.\n\nSteps 1 to 3 are optional once you know a map. Region keys are matched\nloosely enough that `{\"California\": 39.5}` just works.\n\n## No key required\n\nBoth discovery endpoints and both render endpoints work anonymously.\n`GET /v1/renders?spec=…` is a plain image URL. Drop it in an `<img>` tag,\na README, a Notion page or a dashboard tile. It is cached publicly for 24\nhours, so repeat views cost nothing against the 30/hour allowance.\n\n## Region matching\n\nRegion keys are matched case-insensitively against region `key`s\n(ISO/FIPS/… codes), titles, known aliases, and normalized names (diacritics\nstripped, \"St.\" becomes \"Saint\"). Unambiguous typos are auto-corrected\n(\"Calfornia\" to California) and every correction is reported: inline in\nJSON responses, and through the `X-Ultimaps-Corrected`,\n`X-Ultimaps-Unmatched` and `X-Ultimaps-Matching` headers on image\nresponses. Set `onUnmatched: \"error\"` in production pipelines to fail\ninstead of rendering with holes. Use `dryRun: true` for a free pre-flight\nthat returns the full matching report and computed class breaks without\nrendering.\n\n## What can it draw?\n\nThe request-body examples on `POST /v1/renders` cover the range: a\nminimal blank map, a numeric choropleth with an auto-generated legend,\ncategorical coloring, direct region colors with a manual legend, themes\nwith region labels and custom borders, and labelled pins. A prose cookbook\nwith the same validated recipes lives at\nhttps://api.ultimaps.com/llms-full.txt.\n\n## Errors\n\nAll errors are RFC 9457 `application/problem+json` with a stable `code`\nfield to branch on. `402 monthly_quota_exceeded` is a billing state, so\nnever auto-retry it. `429` responses carry `Retry-After` and are retryable.\n\n## Versioning and stability\n\nThe API is in **beta**. The contract below is what we intend to freeze at\nGA, we do not expect breaking changes, and any that prove unavoidable will\nbe announced on the changelog first. From GA on, the API is versioned in\nthe URL path (`/v1`) and evolves additively: new fields and enum values may\nappear, existing ones are never removed or repurposed within v1. A breaking\nchange would ship as `/v2`. `/v1` would then keep working for **at least 12\nmonths**, announce the sunset date on every response through the `Sunset`\nand `Deprecation` headers, and the change would be announced in the API\ndocs at https://ultimaps.com/docs/api/. Nothing is sunset today.\n\n**What \"stable\" covers:** the request schema, the response shapes, the\n`code` taxonomy, the HTTP statuses and the headers. The **pixels are not\npart of the contract**, because rendering improves. Legend layout and\nspacing, fonts, label placement, the automatic colors assigned to unpinned\ncategories, break-label formatting and the ordering of `suggestions[]` can\nall change without notice. If you need byte-stable output, pin what you\ncontrol (`categories.colors`, `regions`, `style`, `output`) and cache the\nimage you rendered.\n\n## Machine-readable docs\n\n- `GET /v1/openapi.json` / `GET /v1/openapi.yaml`: this contract\n- `GET /v1/schemas/render-request.json`: the render request as standalone\n  JSON Schema 2020-12\n- `GET /v1/tools/render_map.json`: a tool definition for LLM function\n  calling / MCP\n- `https://api.ultimaps.com/llms-full.txt`: the full prose guide\n\nAttribution on keyless and Free-key output must stay visible. See the\ncontent license at https://ultimaps.com/license/.\n","contact":{"name":"Ultimaps","url":"https://ultimaps.com"},"termsOfService":"https://ultimaps.com/terms/"},"externalDocs":{"description":"Guides, cookbook and the LLM-facing reference","url":"https://ultimaps.com/docs/api/"},"servers":[{"url":"https://api.ultimaps.com","description":"Production. There is no sandbox. Use `dryRun` for free tests."}],"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"tags":[{"name":"Renders","description":"Create map images (and dry-run them)."},{"name":"Catalog","description":"Discover maps and their regions. Keyless, cached."},{"name":"Account","description":"Usage and service state."}],"paths":{"/v1/renders":{"post":{"operationId":"createRender","summary":"Render a map","tags":["Renders"],"description":"Renders the map and returns the finished image in the response body.\nRenders are **synchronous**: no job ids, no polling, no webhooks. A\nrender is normally well under a second, and is abandoned at 30 s with\n`504 render_timeout`. Responses are `image/png` (default),\n`image/svg+xml` (`output.format: \"svg\"`, Pro), or `application/json`\nwhen `dryRun: true`. Every response carries\n`Content-Disposition: inline; filename=\"<mapId>.<ext>\"` and\n`Cache-Control: no-store`. CORS is open (`*`, no cookies), so this is\ncallable straight from a browser. The body must be sent as\n`Content-Type: application/json`.\n\nWorks without a key: the keyless tier renders PNG up to 1600 px with\na visible attribution, 30 renders/hour per IP. Send a key for larger\ncanvases, SVG, clean output and your plan's quotas. An **invalid** key\nis always `401`. It never falls back to keyless.\n\n## Coloring a map: three layers, two data modes\n\nA render paints in this order, each layer winning over the one before:\n\n1. **One data mode, `choropleth` OR `categories`, never both** (a map\n   has one scale, so sending both is `400`).\n   - **`choropleth`: numeric data.** `choropleth.values` maps region\n     keys to numbers, and that alone is a complete request. The\n     visualization `type` (gradient / steps / groups), `classes`,\n     `method` and `palette` are all suggested from the data\n     distribution unless pinned explicitly (`dryRun` and the\n     `X-Ultimaps-Choropleth` header report what was picked). `format`\n     (a preset or an options object) sets how numbers print on every\n     surface: class labels, gradient ticks, tooltips. Regions absent\n     from `values` get `noDataColor`. If *none* of the keys match, the\n     request fails with `400` even in `warn` mode, since there is\n     nothing to build a scale from.\n   - **`categories`: categorical coloring.** `categories.values` maps\n     region keys to category names (\"West\", \"Swing state\"). Pin the\n     colors with `categories.colors` (exact, case-sensitive names).\n     Anything unpinned gets a color from a built-in categorical\n     palette, assigned in order of first appearance, so pin them if\n     you need stable output. The legend is generated from the\n     categories actually used, and a color for a category no region\n     uses is ignored. Unmatched keys simply go uncolored.\n2. **`regions`: direct hex overrides.** Composes with either mode, and\n   wins over it. On its own it is the simplest possible render, and the\n   only mode where you may supply `legend.items` by hand. With\n   `choropleth` or `categories` the legend is generated and\n   `legend.items` is a `400`. Steer it with `legend.position` and\n   `choropleth.format` instead.\n3. **Everything else** gets `choropleth.noDataColor` (choropleth mode),\n   else `style.defaultRegionColor`, else the theme's region color.\n\n`choropleth` and `categories` are also the only fields that decide\nwhich mode you are in. There is no `mode` field, and no default mode:\na request with neither is a valid blank map of the region outlines.\n\nA legend is drawn automatically whenever there is one to draw, taking\na column on the left or right (180 px, capped at 30% of the canvas on\nnarrow renders), or a horizontal band between the title and the map\nwith `legend.position: top | bottom` (60 px for item\nlegends, 70 px for gradient/steps bars, which are horizontal-only and\ndefault to the bottom band). Set `legend.show: false` to suppress it\nand give the whole canvas to the map.\n\n## Sizing\n\nThe output is exactly `width × height` pixels. The title band and the\nlegend column take their space from the map area, never by growing the\ncanvas. When you omit `height` it is derived as a snug fit: the height\nat which the map sits at its natural aspect ratio once the title and\nlegend take their cut. A derived-height render has no letterboxing.\n\n## Before you spend a render\n\n`dryRun: true` returns the full pre-flight as JSON (which keys\nmatched, which were auto-corrected, which failed with suggestions,\nthe class breaks and the exact legend) without rendering and\n**without consuming quota** (it is rate-limited only). Use it in\neditors, before batch jobs, and whenever an LLM assembled the request.\nIn production pipelines pair it with `onUnmatched: \"error\"` so a bad\nkey fails loudly instead of rendering a map with holes.\n\n## Metering\n\nOnly delivered images are charged. A failed render refunds its\nreservation, and `dryRun` never charges. `429` (per-minute or per-hour\nrate, daily cap, concurrency) is retryable, so honor `Retry-After`.\n`402 monthly_quota_exceeded` is a billing state and must never be\nretried. `403 plan_upgrade_required` means the request exceeded your\ntier's format or size limits, and it costs no quota. Check\n`GET /v1/usage` for the monthly picture.\n","security":[{"bearerAuth":[]},{"apiKeyHeader":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"},"examples":{"minimal":{"summary":"The smallest valid request: a blank themed map","value":{"mapId":"united-states"}},"choropleth":{"summary":"Choropleth from raw data values","value":{"mapId":"united-states","choropleth":{"values":{"California":39.5,"Texas":30.5,"Florida":22.6},"palette":"blues","classes":5,"method":"quantile","noDataColor":"#EEEEEE","format":"compact"},"legend":{"position":"left"},"title":{"text":"Population by State (M)"},"style":{"labels":{"show":true,"content":"value"}}}},"categories":{"summary":"Categorical coloring","value":{"mapId":"united-states","categories":{"values":{"California":"West","Oregon":"West","Texas":"South"},"colors":{"West":"#1D4ED8"}}}},"regionColors":{"summary":"Direct region colors with a manual legend","value":{"mapId":"europe","regions":{"DE":"#1D4ED8","FR":"#F59E0B"},"legend":{"items":[{"label":"Visited","color":"#1D4ED8"},{"label":"Next up","color":"#F59E0B"}]}}},"styledWithLabels":{"summary":"Theme, region labels, custom borders","value":{"mapId":"europe","regions":{"DE":"#1D4ED8"},"style":{"theme":"newspaper","labels":{"show":true,"color":"#111827"},"regionBorders":{"color":"#94A3B8","width":0.75},"mapBorder":{"width":2}}}},"pins":{"summary":"Labelled pins on a plain map","value":{"mapId":"united-states","locations":[{"title":"HQ","lat":37.77,"lon":-122.42,"color":"#DC2626"},{"title":"Austin","lat":30.27,"lon":-97.74,"labelPosition":"right"}]}},"dryRun":{"summary":"Free pre-flight (no render, no quota)","value":{"mapId":"united-states","choropleth":{"values":{"Calfornia":39.5,"Atlantis":1}},"dryRun":true}}}}}},"responses":{"200":{"description":"The rendered image (or the dry-run report when `dryRun: true`).\n`Content-Type` is `image/png` by default, `image/svg+xml` when\n`output.format` is `svg`, and `application/json` for dry runs.\nAlso carries `Content-Disposition: inline; filename=\"<mapId>.<ext>\"`\nand `Cache-Control: no-store`.\n","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Requests allowed in the current window: per minute with a key, per hour keyless.\n"},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Epoch seconds when the window resets."},"X-Ultimaps-Render-Id":{"schema":{"type":"string"},"description":"Unique id of this render, logged server-side. Quote it in support requests. Present on error responses too.\n"},"X-Ultimaps-Corrected":{"schema":{"type":"integer"},"description":"Number of region keys that were auto-corrected."},"X-Ultimaps-Unmatched":{"schema":{"type":"integer"},"description":"Number of region keys that did not match any region."},"X-Ultimaps-Matching":{"schema":{"type":"string"},"description":"JSON summary of corrections and unmatched keys (first 5 of each). Present only when there is something to report.\n"},"X-Ultimaps-Choropleth":{"schema":{"type":"string"},"description":"The resolved visualization plan as JSON `{type, method, classes, palette, format}`, where `format` is the FormatSpec after preset expansion and auto inference. Present on image responses in choropleth mode. Dry runs carry it in the body instead.\n"},"X-Ultimaps-Warnings":{"schema":{"type":"string"},"description":"JSON array of `{code, message}` non-fatal request adjustments: `legend_position_coerced` (gradient/steps legend moved to the bottom band), `percent_values_look_scaled` (`style: percent` multiplies by 100, but the data max exceeds 1.5, so the values look pre-scaled), `legend_overflow` (estimated legend wider than the canvas), `layer_unavailable` (a requested layer this map doesn't ship was skipped), `labels_unavailable` (`style.labels.show` on a map with no curated labels, so nothing was drawn). Present only when there is something to report.\n"}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/DryRunResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Monthly quota exhausted. A billing state, NOT retryable. Carries quota_resets_at (calendar month, UTC) and upgrade_url.\n","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"$ref":"#/components/responses/PlanUpgradeRequired"},"404":{"$ref":"#/components/responses/UnknownMap"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/RenderFailed"},"504":{"$ref":"#/components/responses/RenderTimeout"}}},"get":{"operationId":"createRenderKeyless","summary":"Render a map (keyless, cacheable URL)","tags":["Renders"],"description":"The keyless twin of `POST /v1/renders`, for `<img>` tags, Markdown and\nno-code tools. Same request object, URL-encoded as JSON in `spec`.\nAlways the keyless tier (PNG, full attribution, ≤ 1600 px wide) and\npublicly cacheable for 24 hours (`Cache-Control: public, max-age=86400`),\nso repeat loads of an embedded map do not count against the limit.\nBecause the cache key is the URL, a *changed* spec is a new URL. For\ndashboards that re-render on fresh data that is exactly what you want.\n\nThis endpoint never accepts a key. Sending one returns `400`. Use POST\nwith your key instead.\n\nAn oversized, missing, or malformed `spec` is always\n`400 validation_error` (never `413`).\n\n```\nhttps://api.ultimaps.com/v1/renders?spec=%7B%22mapId%22%3A%22united-states%22%2C%22regions%22%3A%7B%22US-CA%22%3A%22%231D4ED8%22%7D%7D\n```\n","security":[],"parameters":[{"name":"spec","in":"query","required":true,"description":"The render request (see `RenderRequest`) as URL-encoded JSON. At most 6144 **bytes** of UTF-8 measured after URL-decoding, so non-ASCII text counts more than one byte per character. Larger specs must use POST. Overflow is `400 validation_error`, not `413`.\n","schema":{"type":"string","maxLength":6144},"example":"{\"mapId\":\"united-states\",\"regions\":{\"US-CA\":\"#1D4ED8\"}}"}],"responses":{"200":{"description":"The rendered PNG (or the dry-run report when `spec` sets `dryRun: true`). Carries `Content-Disposition: inline; filename=\"<mapId>.png\"`.","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"`public, max-age=86400` for images, `no-store` for dry runs."},"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Keyless renders allowed per hour for your IP."},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Epoch seconds when the hourly window resets."},"X-Ultimaps-Render-Id":{"schema":{"type":"string"}},"X-Ultimaps-Corrected":{"schema":{"type":"integer"}},"X-Ultimaps-Unmatched":{"schema":{"type":"integer"}},"X-Ultimaps-Matching":{"schema":{"type":"string"},"description":"JSON summary of corrections and unmatched keys (first 5 of each). Present only when there is something to report.\n"},"X-Ultimaps-Choropleth":{"schema":{"type":"string"},"description":"The resolved visualization plan as JSON `{type, method, classes, palette}` (choropleth mode only). Cached with the image for 24h, like every header on this endpoint.\n"},"X-Ultimaps-Warnings":{"schema":{"type":"string"},"description":"JSON array of `{code, message}` non-fatal adjustments. Present only when there is something to report. Cached with the image.\n"}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"$ref":"#/components/schemas/DryRunResponse"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"403":{"$ref":"#/components/responses/PlanUpgradeRequired"},"404":{"$ref":"#/components/responses/UnknownMap"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/RenderFailed"},"504":{"$ref":"#/components/responses/RenderTimeout"}}}},"/v1/maps":{"get":{"operationId":"listMaps","summary":"List available maps","tags":["Catalog"],"description":"The full public map catalog: every map you can render, with its id, title, what a \"region\" means on it, extra layers, and region count. This is the discovery entry point, since every render needs a `mapId` from here. Map ids are stable slugs (`united-states`, `europe`, `world-equal-earth`) that always point at the current edition of a map, and they match the map's ultimaps.com URL. `q` filters the catalog by id, title, region type or category (case- and diacritic- insensitive substring, and it also matches ids you may have seen in Studio URLs). No authentication. Cached for one hour (`Cache-Control: public, max-age=3600`). Not paginated: `data` is every matching map, and `meta` reports `matched` against `total` so you can see what a filter removed. Private workspace maps are never listed here, even with a key. They stay renderable by id with a key from their workspace.\n","security":[],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":100},"example":"counties","description":"Case- and diacritic-insensitive substring filter over id, title, region type and category.\n"}],"responses":{"200":{"description":"Catalog envelope.","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"`public, max-age=3600`."}},"content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CatalogMap"}},"meta":{"type":"object","required":["matched","total"],"properties":{"matched":{"type":"integer","description":"Maps in `data` (after `q`).","examples":[3]},"total":{"type":"integer","description":"Maps in the whole catalog, regardless of `q`.","examples":[250]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/maps/{mapId}":{"get":{"operationId":"getMap","summary":"Get map detail with regions","tags":["Catalog"],"description":"Full detail for one map: its layers and every region as `{key, title}`. `key` is the region's code in the map's primary scheme (ISO 3166-2, FIPS, ZCTA, GSS, …), or its title where the scheme publishes no code for it. It is the recommended key for `choropleth.values`, `categories.values` and `regions`. The render endpoint also accepts titles, common aliases (USPS abbreviations, alternate names) and typos, so you rarely need this call before rendering. `q` filters regions by key, title or any accepted alias, so a search for \"CA\" finds California even though only `US-CA` is emitted. `offset` and `limit` page through large maps (US counties has 3,143 regions). Without them you get every region, and nothing is truncated silently. No authentication. Cached for one hour (`Cache-Control: public, max-age=3600`). Private workspace maps are not exposed here.\n","security":[],"parameters":[{"name":"mapId","in":"path","required":true,"schema":{"type":"string"},"example":"united-states","description":"The map's id (slug) from `GET /v1/maps`."},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":100},"example":"new york","description":"Case- and diacritic-insensitive substring filter over each region's key, title and accepted aliases. Matching is wide, but the response still prints only `key` and `title`.\n"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"Skip this many matching regions."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":5000},"description":"Return at most this many regions. Omit for all of them."}],"responses":{"200":{"description":"Map detail.","headers":{"Cache-Control":{"schema":{"type":"string"},"description":"`public, max-age=3600`."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapDetail"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"404":{"$ref":"#/components/responses/UnknownMap"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/usage":{"get":{"operationId":"getUsage","summary":"Current plan and quota state","tags":["Account"],"description":"The key's plan, this month's renders and today's renders, so you never need a 402 to learn where you stand. The monthly counter is per **workspace** (shared across all of that workspace's keys) and resets on the calendar month (UTC), independent of subscription renewal anniversaries. The daily counter is per **key** (the key making this request) and resets at midnight UTC. Hitting the daily cap surfaces as `429 rate_limit_exceeded` on renders. Keyless renders are not counted anywhere.\n","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"Usage summary.","content":{"application/json":{"schema":{"type":"object","required":["plan","renders"],"properties":{"plan":{"type":"string","enum":["free","pro"]},"renders":{"type":"object","required":["used","quota","resetsAt","today"],"properties":{"used":{"type":"integer","description":"Renders used this month, workspace-wide."},"quota":{"type":"integer"},"resetsAt":{"type":"string","format":"date-time"},"today":{"type":"object","required":["used","cap"],"properties":{"used":{"type":"integer","description":"Renders used today by this key."},"cap":{"type":"integer"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/health":{"get":{"operationId":"getHealth","summary":"Service health","tags":["Account"],"description":"Liveness plus render-engine state. `degraded` means the engine is busy and work is queueing behind it. Renders still succeed, they just wait. Unauthenticated, uncached. Poll at most once a minute.\n","security":[],"responses":{"200":{"description":"Health status including render-engine state.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"engine":{"type":"object","properties":{"availableWorkers":{"type":"integer"},"queueDepth":{"type":"integer"}}},"timestamp":{"type":"string","format":"date-time"}}}}}}}}},"/v1/openapi.json":{"get":{"operationId":"getOpenApiJson","summary":"This contract as JSON","tags":["Account"],"description":"The OpenAPI 3.1 document, as JSON. Cached 5 minutes.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/v1/openapi.yaml":{"get":{"operationId":"getOpenApiYaml","summary":"This contract as YAML","tags":["Account"],"description":"The OpenAPI 3.1 document, as YAML. Cached 5 minutes.","security":[],"responses":{"200":{"description":"The OpenAPI document.","content":{"application/yaml":{"schema":{"type":"string"}}}}}}},"/v1/schemas/render-request.json":{"get":{"operationId":"getRenderRequestSchema","summary":"Render request as standalone JSON Schema","tags":["Account"],"description":"`RenderRequest` with every `$ref` inlined, as JSON Schema 2020-12. The exact shape the server validates. Cached 5 minutes.\n","security":[],"responses":{"200":{"description":"The JSON Schema document.","content":{"application/schema+json":{"schema":{"type":"object"}}}}}}},"/v1/tools/render_map.json":{"get":{"operationId":"getRenderMapTool","summary":"Tool definition for LLM function calling","tags":["Account"],"description":"A function-calling / MCP tool definition for the render endpoint. `input_schema` is the same inlined render-request schema. Cached 5 minutes.\n","security":[],"responses":{"200":{"description":"The tool definition.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"The canonical form: Authorization: Bearer <api key>. Create a key in Studio under Workspace → API."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Accepted alias for low-code tools that cannot set the Authorization header."}},"schemas":{"HexColor":{"type":"string","pattern":"^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$","examples":["#1D4ED8"]},"FormatSpec":{"type":"object","additionalProperties":false,"description":"Structured number format (Intl-backed, en-US). Field exclusivity: `preset` and `d3` are each mutually exclusive with every other field, `decimals` and `significantDigits` are mutually exclusive, and `currency` requires `style: \"currency\"`. Examples: `{\"decimals\": 0, \"suffix\": \" km²\"}` gives \"1,234 km²\", `{\"compact\": true, \"decimals\": 1, \"trim\": true}` gives \"39.5M\", `{\"style\": \"percent\", \"decimals\": 1}` turns 0.423 into \"42.3%\".\n","properties":{"preset":{"type":"string","enum":["auto","plain","compact","percent","currency"],"description":"Alias for the bare-string preset form."},"style":{"type":"string","enum":["decimal","percent","currency"],"default":"decimal","description":"\"percent\" MULTIPLIES BY 100. The input is a fraction (0.42 prints as \"42%\"). Data already in percent points should use `suffix: \"%\"` instead.\n"},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$","default":"USD","description":"ISO-4217 code, rendered with its Intl symbol (\"€\", \"CA$\"). Only valid with style \"currency\".\n"},"compact":{"type":"boolean","description":"Compact notation: K/M/B/T (never SI \"G\")."},"decimals":{"type":"integer","minimum":0,"maximum":6,"description":"Fraction digits. Min equals max, for uniform labels, unless `trim` is set."},"significantDigits":{"type":"integer","minimum":1,"maximum":6},"trim":{"type":"boolean","description":"Strip trailing zeros."},"grouping":{"type":"boolean","default":true,"description":"Thousands separators."},"sign":{"type":"string","enum":["auto","always","exceptZero"],"default":"auto","description":"Maps 1:1 to Intl signDisplay. \"exceptZero\" is the usual choice for diverging legends.\n"},"prefix":{"type":"string","maxLength":12,"description":"Literal, inserted AFTER any leading sign (\"-€1,234\", never \"€-1,234\").\n"},"suffix":{"type":"string","maxLength":20,"description":"Literal, e.g. \" per km²\"."},"d3":{"type":"string","maxLength":20,"description":"Escape hatch: a raw d3-format spec. Invalid grammar is a 400.\n"}}},"LayerId":{"type":"string","description":"The extra-layer vocabulary, shared by every map. Each map ships a subset, listed by `GET /v1/maps` and `GET /v1/maps/{mapId}`. The enum is additive-only: ids are appended, never renamed or removed.","enum":["admin0","admin0-mesh","admin1-mesh","admin1-mesh-inner","admin2-mesh","lakes","rivers","roads","cities"]},"RenderRequest":{"type":"object","additionalProperties":false,"required":["mapId"],"not":{"required":["choropleth","categories"]},"dependentSchemas":{"choropleth":{"properties":{"legend":{"not":{"required":["items"]}}}},"categories":{"properties":{"legend":{"not":{"required":["items"]}}}}},"properties":{"mapId":{"type":"string","minLength":1,"maxLength":120,"description":"Map id (slug) from GET /v1/maps, e.g. `united-states`, `europe`, `california-counties`. Always resolves to the current edition of that map.\n","examples":["united-states"]},"choropleth":{"type":"object","additionalProperties":false,"required":["values"],"description":"Numeric choropleth mode. Mutually exclusive with `categories` (one scale per map). `regions` composes with it as an override layer. If none of the keys in `values` match, the request fails with 400 even under `onUnmatched: warn`, since there is nothing to build a scale from. `type`, `classes`, `method` and `palette` are all optional: whatever you leave out is suggested from the data distribution (the resolved plan is returned by `dryRun` and in the `X-Ultimaps-Choropleth` header).\n","properties":{"values":{"type":"object","additionalProperties":{"type":"number"},"minProperties":1,"maxProperties":5000,"description":"Region key to numeric value. Keys go through the matching rules described above. Two keys resolving to the same region count as two matched keys, and the later value wins.\n"},"type":{"type":"string","enum":["auto","gradient","steps","groups"],"default":"auto","description":"Visualization type. `auto` picks from the data: ≤7 unique values → `groups` (discrete legend items), skewed distributions → `steps` (contiguous bar with break ticks), else `gradient` (continuous color scale). Explicit values pin the type. `gradient` and `steps` legends are horizontal-only: they render in a top/bottom band (default bottom), and a left/right `legend.position` is coerced to bottom with a warning. That rule depends on the data, so JSON Schema cannot express it and generated SDKs should not try to enforce it.\n"},"palette":{"type":"string","enum":["gnbu","bugn","bupu","orrd","pubu","pubugn","purd","rdpu","ylgn","ylgnbu","ylorbr","ylorrd","blues","greens","greys","oranges","purples","reds","rdbu","brbg","spectral","piyg","prgn","puor","rdgy","rdylbu"],"description":"ColorBrewer palette id (the server matches case-insensitively). Absent = suggested from the data: diverging (rdbu/rdylgn) when the values cross zero, sequential blues otherwise. Sequential palettes suit one-directional data, and diverging palettes (rdbu, brbg, spectral, piyg, prgn, puor, rdgy, rdylbu) suit data around a midpoint.\n"},"classes":{"type":"integer","minimum":2,"maximum":9,"description":"Number of classes. Absent = suggested from the data (5 base, more for skewed distributions). Explicit counts are honored exactly, capped only by the number of unique values. Tied values can still collapse duplicate quantile edges, and a constant dataset collapses to one class. `dryRun` returns the breaks and effective class count that will actually be drawn.\n"},"method":{"type":"string","enum":["quantile","equalInterval","jenks","pretty"],"description":"Break method. Absent = suggested from the data. quantile = equal number of regions per class, equalInterval = equal-width value intervals, jenks = natural breaks (minimal within-class variance), pretty = rounded human-readable edges. Additive-only enum: more methods may be added, none removed.\n"},"noDataColor":{"$ref":"#/components/schemas/HexColor","description":"Fill for regions absent from `values`. Choropleth mode only. Overrides `style.defaultRegionColor` when both are set.\n"},"format":{"description":"Number format for ALL choropleth surfaces: class labels, gradient ticks, and tooltips in interactive contexts. A preset string or a FormatSpec options object. Default \"auto\", which infers from the magnitude. Presets: `auto`, `plain` (39,538,223), `compact` (39.5M), `percent` (the input is a FRACTION, so 0.42 prints as \"42.3%\", and pre-scaled data should use `{\"suffix\": \"%\"}` instead), `currency` ($39,538,223, USD). Raw d3-format strings are no longer accepted. Use `{\"d3\": \"~s\"}` for the old behaviour. The dry run and the `X-Ultimaps-Choropleth` header echo the RESOLVED spec after preset expansion and auto inference.\n","oneOf":[{"type":"string","enum":["auto","plain","compact","percent","currency"]},{"$ref":"#/components/schemas/FormatSpec"}]}}},"categories":{"type":"object","additionalProperties":false,"required":["values"],"description":"Categorical coloring mode. Mutually exclusive with `choropleth`. Unmatched keys simply leave their regions uncolored. The no-data fill is `style.defaultRegionColor`, and `noDataColor` does not apply here.\n","properties":{"values":{"type":"object","additionalProperties":{"type":"string","minLength":1},"minProperties":1,"maxProperties":5000,"description":"Region key to category name."},"colors":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/HexColor"},"description":"Optional category name to color. Names are matched exactly and case-sensitively against the names in `values` (unlike region keys, which are matched loosely). An entry for a category no region uses is silently ignored. Unlisted categories get a color from a built-in categorical palette, assigned in order of first appearance in `values`, so pin the colors if you need stable output.\n"}}},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/HexColor"},"maxProperties":5000,"description":"Override layer. Region key to hex color, applied AFTER choropleth/categories coloring and winning over it. Precedence: choropleth/categories paint first, `regions` overrides win, and everything else gets `choropleth.noDataColor` (choropleth mode), else `style.defaultRegionColor`, else the theme default.\n"},"legend":{"type":"object","additionalProperties":false,"properties":{"position":{"type":"string","enum":["left","right","top","bottom"],"description":"left/right take a column carved from the map area (180 px, capped at 30% of the canvas width on narrow renders), where the legend items stack vertically. top/bottom take a horizontal band between the title and the map: 60 px for item legends (items flow in a single centered row), 70 px for gradient/steps bars. Absent = left for items/groups legends, bottom for gradient/steps. Gradient and steps legends are horizontal-only, so left/right is coerced to bottom with a warning. Legends do not wrap, so a wide legend on a narrow render can overflow the canvas. The `X-Ultimaps-Warnings` header estimates this.\n"},"show":{"type":"boolean","description":"Defaults to true whenever a legend exists (auto-generated for `choropleth`/`categories`, or from `legend.items`). Set false to suppress it and give the whole canvas to the map.\n"},"items":{"type":"array","minItems":1,"maxItems":50,"description":"Manual legend items, for regions-only maps. With `choropleth` or `categories` the legend is auto-generated and `items` is rejected with 400.\n","items":{"type":"object","additionalProperties":false,"required":["label","color"],"properties":{"label":{"type":"string","minLength":1,"maxLength":120},"color":{"$ref":"#/components/schemas/HexColor"}}}}}},"title":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":200},"position":{"type":"string","enum":["top","bottom"],"default":"top"},"color":{"$ref":"#/components/schemas/HexColor"}}},"locations":{"type":"array","maxItems":200,"description":"Pin markers. They compose with every mode.","items":{"type":"object","additionalProperties":false,"required":["title","lat","lon"],"properties":{"title":{"type":"string","minLength":1,"maxLength":120},"lat":{"type":"number","minimum":-90,"maximum":90},"lon":{"type":"number","minimum":-180,"maximum":180},"color":{"$ref":"#/components/schemas/HexColor"},"labelPosition":{"type":"string","enum":["top","bottom","left","right"],"default":"bottom","description":"Where the pin's label sits relative to the pin."},"showLabel":{"type":"boolean","default":true,"description":"Set false to render this pin without its label (the marker stays).\n"}}}},"style":{"type":"object","additionalProperties":false,"properties":{"theme":{"type":"string","default":"paper","enum":["paper","newspaper","dark","earth","vintage","embossed","maritime","neon","outline","wireframe"],"description":"Theme name from Studio's theme gallery (the server matches case-insensitively). Additive-only enum.\n"},"backgroundColor":{"$ref":"#/components/schemas/HexColor"},"defaultRegionColor":{"$ref":"#/components/schemas/HexColor","description":"Fill for regions that get no color from `choropleth`/`categories`/`regions`. In choropleth mode `choropleth.noDataColor` overrides this. In categories mode this IS the no-data fill.\n"},"regionBorders":{"type":"object","additionalProperties":false,"properties":{"color":{"$ref":"#/components/schemas/HexColor"},"width":{"type":"number","minimum":0,"maximum":5}}},"mapBorder":{"type":"object","additionalProperties":false,"properties":{"color":{"$ref":"#/components/schemas/HexColor"},"width":{"type":"number","minimum":0,"maximum":10}}},"labels":{"type":"object","additionalProperties":false,"description":"Region labels, off by default. Each map ships its own curated label set (`labels` in GET /v1/maps). On a map with none, `show: true` renders nothing and a `labels_unavailable` warning is reported.","properties":{"show":{"type":"boolean","default":false},"content":{"type":"string","enum":["name","value"],"default":"name","description":"What each label prints. `name` is the region's name. `value` prints the region's choropleth number instead, formatted by the resolved `choropleth.format` (the same string the legend and tooltips show). Regions without a value get no label. Requires `choropleth`. With `categories`, or with neither, it is a `validation_error`. Labels are not collision-checked: a long number in a small region overflows it, exactly as in Studio."},"color":{"$ref":"#/components/schemas/HexColor"}}}}},"layers":{"type":"object","additionalProperties":false,"description":"Extra geographic layers to draw, keyed by `LayerId`, e.g. {\"cities\": true, \"roads\": true}. Keys outside the enum are a `validation_error`. Each map ships a subset (see `layers` in GET /v1/maps/{mapId}). Turning on a layer this map lacks is not an error: the layer is skipped and a `layer_unavailable` warning is reported. Omitted layers are off, except layers the map source turns on by default (currently `admin0` on ZIP-code maps, the land no ZIP covers). Pass `false` to hide those.","properties":{"admin0":{"type":"boolean","description":"Surrounding land, as a filled area of neighbouring countries or states."},"admin0-mesh":{"type":"boolean","description":"Country borders (mesh)."},"admin1-mesh":{"type":"boolean","description":"First-level subdivision borders (states, provinces, regions)."},"admin1-mesh-inner":{"type":"boolean","description":"State borders drawn over the map's own regions (county and ZIP maps, where `admin1-mesh` covers the surroundings only)."},"admin2-mesh":{"type":"boolean","description":"Second-level subdivision borders (counties, districts)."},"lakes":{"type":"boolean","description":"Major lakes."},"rivers":{"type":"boolean","description":"Major rivers."},"roads":{"type":"boolean","description":"Major roads."},"cities":{"type":"boolean","description":"Major cities as labelled points."}}},"output":{"type":"object","additionalProperties":false,"properties":{"width":{"type":"integer","minimum":100,"maximum":4000,"default":1200},"height":{"type":"integer","minimum":100,"maximum":4000,"description":"Optional. When omitted, derives as a snug fit: the map's aspect ratio applied to the map area that remains after the title band (50 px) and any left/right legend column take their cut. A top/bottom legend adds its band (60 px for item legends, 70 px for gradient/steps bars) on top of that. An An explicit height is delivered exactly, and the title and legend then shrink the map rather than the canvas.\n"},"scale":{"type":"number","minimum":1,"maximum":4,"default":1,"description":"PNG raster multiplier: the PNG is exactly round(width×scale) × round(height×scale) pixels. It has no effect on SVG output, but it is still tier-capped.\n"},"format":{"type":"string","enum":["png","svg"],"default":"png","description":"SVG requires a Pro key."}},"description":"Caps: width×scale ≤ 8192, height×scale ≤ 8192, and (width×scale)×(height×scale) ≤ 4096×4096 = 16.8M pixels. Your tier caps width, height and scale further (see Tiers).\n"},"onUnmatched":{"type":"string","enum":["warn","error"],"default":"warn","description":"warn = render anyway (unmatched regions get the no-data fill) and report. error = 400 with per-key suggestions. Recommended for production pipelines: error. Exception: in choropleth mode, ZERO matched keys is a 400 even under warn.\n"},"dryRun":{"type":"boolean","default":false,"description":"Return the matching report + computed breaks as JSON. No image, no quota."}}},"DryRunResponse":{"type":"object","required":["dryRun","mapId","regionMatching"],"properties":{"dryRun":{"type":"boolean","const":true},"mapId":{"type":"string"},"choropleth":{"type":["object","null"],"description":"The resolved visualization plan. Present in choropleth mode, null otherwise. Under `type: auto` this is what the detection picked. `classes` is the effective count, since tied data collapses duplicate edges even for explicit requests.\n","properties":{"type":{"type":"string","enum":["gradient","steps","groups"]},"method":{"type":"string","enum":["quantile","equalInterval","jenks","pretty"]},"classes":{"type":"integer"},"palette":{"type":"string"},"reason":{"type":"string","description":"Why this type was picked, e.g. \"Highly skewed data - steps show distribution better\"."},"breaks":{"type":"array","items":{"type":"number"},"description":"The class edges actually drawn (`classes + 1` values, fewer if tied data collapsed edges).\n"}}},"legend":{"type":["array","object","null"],"description":"Exactly the legend the render would draw. Its shape follows the resolved type. groups/steps choropleths and categories return an array of `{label, color}` (steps labels are the class boundaries, groups labels are ranges). A gradient returns `{points: [min, mid|null, max], stops: [hex…]}`, the continuous scale's domain and preview colors. Null for regions-only requests.\n"},"warnings":{"type":"array","description":"Non-fatal request adjustments (same objects as the `X-Ultimaps-Warnings` header on image responses).\n","items":{"type":"object","properties":{"code":{"$ref":"#/components/schemas/WarningCode"},"message":{"type":"string"}}}},"regionMatching":{"$ref":"#/components/schemas/RegionMatching"}}},"WarningCode":{"type":"string","description":"Non-fatal request adjustment. Additive-only: codes may be appended, never renamed or removed.","enum":["legend_position_coerced","percent_values_look_scaled","legend_overflow","layer_unavailable","labels_unavailable"]},"RegionMatching":{"type":"object","required":["matchedKeys","corrected","unmatched"],"properties":{"matchedKeys":{"type":"integer","description":"Input keys that resolved, summed across the request's choropleth/categories/regions blocks. A region referenced by two keys counts twice (the later value wins). This counts keys, not distinct regions.\n"},"corrected":{"type":"array","items":{"type":"object","properties":{"input":{"type":"string"},"matchedTo":{"type":"string","description":"The published `key` of the region the input was corrected to.","examples":["US-CA"]},"title":{"type":"string"},"via":{"type":"string"}}}},"unmatched":{"type":"array","items":{"type":"object","properties":{"input":{"type":"string"},"suggestions":{"type":"array","items":{"type":"string"},"description":"Closest regions as `\"Title (key)\"`, or just the title on maps whose key is the title.\n","examples":[["Texas (US-TX)"]]}}}}}},"CatalogMap":{"type":"object","properties":{"id":{"type":"string","description":"Stable slug, the `mapId` for every other endpoint. Points at the current edition of the map and matches its ultimaps.com URL.\n","examples":["united-states"]},"title":{"type":"string","examples":["United States"]},"regionType":{"type":["string","null"],"examples":["State"]},"layers":{"type":"array","items":{"$ref":"#/components/schemas/LayerId"},"description":"Extra layers this map ships, renderable via the `layers` request field."},"regionCount":{"type":"integer"},"labels":{"type":"boolean","description":"Whether this map ships curated region labels, which is what `style.labels` draws. Most county and district maps have none."}}},"MapDetail":{"type":"object","properties":{"id":{"type":"string","description":"The map's slug. The same id `GET /v1/maps` prints, even when the request used an alias.","examples":["united-states"]},"title":{"type":"string"},"regionType":{"type":["string","null"]},"regionTypePlural":{"type":["string","null"]},"layers":{"type":"array","items":{"$ref":"#/components/schemas/LayerId"},"description":"Extra layers this map ships, renderable via the `layers` request field."},"regionCount":{"type":"integer","description":"Regions in the whole map, regardless of `q`/`offset`/`limit`.","examples":[51]},"labels":{"type":"boolean","description":"Whether this map ships curated region labels, which is what `style.labels` draws. Coverage varies by map, and the flag only says whether any exist."},"regions":{"type":"array","description":"The requested page of regions. Aliases are accepted as render input but never emitted here. `key` is the one identifier the API documents and prints.\n","items":{"type":"object","required":["key","title"],"properties":{"key":{"type":"string","description":"The region's code in the map's primary scheme (ISO 3166-2, FIPS, ZCTA, GSS, …), or its title where the scheme publishes no code. Unique within the map. The recommended render key. Never empty.\n","examples":["US-CA"]},"title":{"type":"string","examples":["California"]}}}},"page":{"type":"object","required":["offset","limit","matched"],"properties":{"offset":{"type":"integer","examples":[0]},"limit":{"type":["integer","null"],"description":"The effective `limit`, or `null` when every matching region was returned."},"matched":{"type":"integer","description":"Regions matching `q` (all of them when `q` is absent) before paging.","examples":[51]}}}}},"Problem":{"type":"object","description":"RFC 9457 problem details with a stable `code` to branch on. In `errors[]`, `param` is the dotted path of the offending field for schema violations (e.g. `regions.CA`), and the raw input key for unmatched region keys (e.g. `Texassss`).\n","required":["type","title","status","detail","code"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"code":{"type":"string","enum":["validation_error","invalid_region_id","unknown_map","unauthorized","plan_upgrade_required","monthly_quota_exceeded","rate_limit_exceeded","concurrency_limit_reached","payload_too_large","render_expired","render_failed","render_timeout","not_found","internal_error"]},"errors":{"type":"array","description":"Per-field details on validation errors.","items":{"type":"object","properties":{"param":{"type":"string"},"message":{"type":"string"},"suggestions":{"type":"array","items":{"type":"string"}}}}},"suggestions":{"type":"array","items":{"type":"string"}},"upgrade_url":{"type":"string","format":"uri","description":"Pricing page. Present on plan/quota problems."},"signup_url":{"type":"string","format":"uri","description":"Where to create a free API key. Present on keyless-tier problems."},"quota_resets_at":{"type":"string","format":"date-time","description":"Present on `monthly_quota_exceeded` (first of next month, UTC)."},"regionMatching":{"$ref":"#/components/schemas/RegionMatching","description":"Present on the `onUnmatched: error` 400. The full matching report for the failed request.\n"}}}},"responses":{"PlanUpgradeRequired":{"description":"The request exceeds what the caller's tier allows: SVG without a Pro key, a keyless request over 1600×1600 px at scale 1, or a free-key request over 1600×1600 px at scale 2. It costs no quota. Carries upgrade_url and, for keyless callers, signup_url.\n","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"example":{"type":"https://ultimaps.com/docs/api/errors#plan_upgrade_required","title":"Plan upgrade required","status":403,"code":"plan_upgrade_required","detail":"Keyless renders are capped at 1600×1600px at scale 1. Create a free API key for larger images.","signup_url":"https://studio.ultimaps.com/account/workspace/api"}}}},"ValidationError":{"description":"The request failed validation.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"example":{"type":"https://ultimaps.com/docs/api/errors#validation_error","title":"Request validation failed","status":400,"code":"validation_error","detail":"Request validation failed.","errors":[{"param":"regions.CA","message":"`regions` maps region keys to hex colors. Numbers go in `choropleth.values`."}]}}}},"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"UnknownMap":{"description":"The mapId does not exist (or is not accessible).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"},"example":{"type":"https://ultimaps.com/docs/api/errors#unknown_map","title":"Unknown map","status":404,"code":"unknown_map","detail":"Unknown map \"united-sates\". Use GET /v1/maps to list available maps.","suggestions":["united-states"]}}}},"PayloadTooLarge":{"description":"Request body exceeds the 2MB limit.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"RateLimited":{"description":"Rate or concurrency limit hit. Retryable, so honor Retry-After. It also covers the daily per-key cap, where Retry-After points to midnight UTC.\n","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying."}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"InternalError":{"description":"Unexpected server error. Safe to retry with backoff.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"RenderFailed":{"description":"The render engine failed. Retrying may help.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"RenderTimeout":{"description":"The render exceeded the 30-second budget. Retryable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}