← Carto Desk / API
Token panel

Drive Carto Desk from your own code

Everything the web app does is one HTTP API away. Base URL: https://api.skillsafe.ai/v1/app-api, scoped to this app by the token you send. Every response is wrapped in an envelope: success is {"ok":true,"data":{…}}, failure is {"ok":false,"error":{"code":"…","message":"…","details":{…}}}.

Error codes you will actually meet

CodeHTTPWhat it meansWhat to do
UNAUTHORIZED401Missing, malformed or expired token.Mint a new one from the token panel.
FORBIDDEN403The token belongs to another app.Use a token minted for carto-desk.
INSUFFICIENT_CREDITS402Balance is under min_credits.Compare /me against /estimate before running.
VALIDATION_ERROR400The input object is the wrong shape.Check error.details; task and style are required.
RATE_LIMITED429Too many requests.Back off; do not tight-loop a poll.
JOB_FAILED200The job reached a terminal failed status.Read job.error; retry with the same idempotency key is safe.

The task field comes first

This app has four lanes over one work object. task selects the lane and is the field to get right before any other: it decides the checks, the extra block and the price. An unrecognised value is answered by the closest lane, which names itself in lane — so always read lane back rather than assuming.

taskLaneAnswersExtra blockSource skill
auditAuditDoes this style parse, resolve and ship?blocking[]@mapbox/mapbox-style-quality
cartoCartographyWill a reader find what they came for?palette[]@mapbox/mapbox-cartography
datavizData layersAre the data layers encoding the data honestly?recipes[]@mapbox/mapbox-data-visualization-patterns
perfRender costWhat does this style cost to draw?budget{} + wins[]@mapbox/mapbox-web-performance-patterns

One worked example per lane

task: "audit" — Audit

Request body:

{"task":"audit","style":"…the style.json…","usage":"dispatcher wall display","audience":"field","emphasis":"general","carryover":"","prescan_facts":{"stats":{"layer_count":15,"symbol_layers":5,"sources":3},"flags":[{"id":"SC-001","title":"Mapbox secret token embedded in the style","severity":"critical","layer":"","detail":"…"}]}}

The reply is the shared envelope plus this lane's block:

{ …envelope…, "blocking": ["Rotate the sk. token and remove it from the document", "Declare a glyphs URL or no label renders"] }

task: "carto" — Cartography

Request body:

{"task":"carto","style":"…the style.json…","usage":"driver phone app","audience":"field","emphasis":"legibility","carryover":"Previous lane: Audit. Verdict: rework…","prescan_facts":{"stats":{},"flags":[]}}

The reply is the shared envelope plus this lane's block:

{ …envelope…, "palette": [{"swatch":"#12161c","role":"background","verdict":"keep","note":"the darkest ground, and the only value the labels are read against"}] }

task: "dataviz" — Data layers

Request body:

{"task":"dataviz","style":"…the style.json…","usage":"public air-quality map","audience":"general","emphasis":"data","carryover":"","prescan_facts":{"stats":{},"flags":[]}}

The reply is the shared envelope plus this lane's block:

{ …envelope…, "recipes": [{"layer":"tract-choropleth","technique":"choropleth","expression":"[\"interpolate\",[\"linear\"],[\"get\",\"pm25_total\"],0,\"#f7fbff\",50,\"#08306b\"]","why":"a sequential ramp for a magnitude, not a diverging one"}] }

task: "perf" — Render cost

Request body:

{"task":"perf","style":"…the style.json…","usage":"mid-range Android, GL JS 3.x, map.on('load') hydrates the driver source","audience":"field","emphasis":"speed","carryover":"","prescan_facts":{"stats":{"layer_count":15,"symbol_layers":5,"sources":3},"flags":[]}}

The reply is the shared envelope plus this lane's block:

{ …envelope…, "budget": {"layer_count":15,"symbol_layers":5,"sources":3,"verdict":"tight","note":"…"}, "wins": [{"action":"Gate the label layers to z12+","impact":"high","effort":"S"}] }

The input contract

These are the exact fields the web app submits — taken from its run path, not from intent.

FieldTypeRequiredNotes
taskstringyesaudit, carto, dataviz or perf.
stylestringyesThe Mapbox GL style JSON as text. The app clips at 60 000 characters, from the middle, keeping both ends and marking the cut in-band.
usagestringnoWhat the map is for. The perf lane also reads GL JS init code pasted here.
audiencestringnogeneral, analyst, field, print, unknown.
emphasisstringnogeneral, legibility, accessibility, data, speed. Orders findings; never hides them.
carryoverstringnoA digest of a previous lane's review. The model acknowledges it and does not re-litigate it.
prescan_factsobjectno{stats, flags[]} from a client-side scan. Every flags[].id must come back in coverage_check.
retry_notestringnoSent only on a reformat retry. A correction directive about the model's own previous reply — never user content, and never anything about the style. The web app sets it when a reply fails to parse and runs one extra attempt under a new idempotency key.

retry_note is the only field that is not about the user's style. It exists because a reply that is not one JSON object is unusable, and one named retry is cheaper than making a person press the button again. If you drive this API yourself you can leave it out entirely; if you do send it, send what was wrong with the previous reply and nothing else.

The output contract

The model replies with one JSON object as the job's output.output string. The envelope is identical in every lane, so one parser handles all four:

{
  "lane": "audit",
  "title": "…",
  "style_name": "…",
  "verdict": "ship | fix-first | rework",
  "headline": "…",
  "summary": "…",
  "checks":  [{"name": "…", "status": "pass | warn | fail | unknown", "note": "…"}],
  "findings": [{"id": "CD-001", "title": "…", "severity": "critical | high | medium | low",
                "area": "spec | accessibility | palette | typography | expressions | performance | tokens | data",
                "layer": "an exact layer id from the style, or \"\"",
                "detail": "…", "fix": "…", "snippet": "…"}],
  "coverage_check": [{"prescan_id": "SC-001", "status": "confirmed | set-aside", "note": "…"}],
  "next_steps": ["…"]
}

checks carries that lane's named checks in the lane's order — ten for audit, eight for each of the others. The app renders the canonical list and marks any the model dropped as not reported rather than silently shortening the table, so a client should do the same.

Step by step

1. Get a token

Every call needs Authorization: Bearer <token>. Two ways to get one:

Keep the token out of your source. Read it from an environment variable at runtime and never commit it.

2. Check the session and the balance

GET /me tells you which subject the token belongs to and how many credits it holds. Do this before a run: comparing the balance against the estimate's hold_credits is how you avoid a 402 after submitting.

3. Estimate the lane — free, no job

POST /estimate costs nothing and creates no job. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled.

Estimate the lane you are about to run. The hold differs per lane, because the prompt sections and output caps differ — an estimate for audit does not price perf. The app re-estimates on every lane switch for exactly this reason.

4. Run it, then poll the job

POST /run returns {"job_id"} immediately; poll GET /jobs/{job_id} until status is terminal. Always send an Idempotency-Key header: a retried request with the same key returns the original job instead of billing a second run. Make the key a hash of the lane plus the input — two lanes over the same style are two distinct runs and must not collide on one key.

5. Or stream it

POST /run-stream is the same call over server-sent events. Events are job (the job id, as soon as it exists), delta (text fragments as they are produced) and done (the terminal job, including charged_credits and truncated). The app uses this lane so the staged progress card can advance on the section markers as they arrive.

Send the same Idempotency-Key you would send to /run. If the stream dies mid-flight, keep what arrived: the app closes the truncated JSON and renders whatever sections parsed rather than throwing the run away.

Rate limits and good manners

Attribution

Carto Desk is a derived work built on @mapbox/mapbox-style-quality, @mapbox/mapbox-cartography, @mapbox/mapbox-data-visualization-patterns and @mapbox/mapbox-web-performance-patterns. Mapbox and Mapbox GL JS are trademarks of Mapbox, Inc.; this app is not affiliated with or endorsed by Mapbox.