An API key runs a dashboard without the browser: a nightly job, a cron entry, an Airflow task, or an AI agent through the MCP server (see the MCP page). It is the same run the app makes, on any of the three targets — HTML, Apache Superset, Power BI — and it downloads the same file.
1. Get a key, and what it costs
Account → API keys → name it → Make a key. The key (dg_…) is shown once; only its SHA-256 is stored, so a lost key is revoked and replaced, never looked up.
- Pro and Max plans only. Every run through a key pays in Coins, and a daily-count plan has none. The plan is checked on every request, so a lapsed subscription stops the job too (
403 api-key-plan). - At most five keys at once (
429 api-key-limit). - Revoking takes effect on the next request (
401 api-key-invalid).
Every run through a key is paid, when it starts. A first dashboard, a new version of one — another design, another theme, another chart type, a newer data file or another instruction — an Apache Superset bundle or a Power BI project: each is charged at its target's list price the moment it starts. The free changes the app gives every day, and any promotional free runs, belong to the app window; a key neither uses nor spends them, so a night of scripted runs leaves your free changes in the app untouched.
| Run through a key | Price with the default model |
|---|---|
| HTML dashboard | 0.2 Coins |
| A new version of an HTML dashboard (any change) | 0.2 Coins |
| Apache Superset bundle | 0.25 Coins |
| Power BI project | 0.5 Coins |
| One delivery to a situational blueprint | 0.02 Coins |
A description longer than 2,000 characters doubles the price, and so does an HTML file above the standard size; a large Power BI upload adds 0.5 Coins. GET /api/v1/quota returns every price and your balance, so read it rather than hard-coding these numbers.
A run that fails is refunded — the first time its status is read as FAILED. Reading, looking and downloading cost nothing: the quota, a workbook's sheets, a job's status, a finished file (a run already paid for it), and the libraries of designs, themes and templates.
2. Run a dashboard
Send the key as X-Api-Key. Everything else is the app's own endpoint.
KEY=dg_your_key_here
BASE=https://dashgen.de/api/v1
# 1. Start the run. biTool is HTML, SUPERSET or POWER_BI; userPrompt is optional.
# Idempotency-Key is optional: repeat the call with the same value and you get the same job,
# charged once.
JOB=$(curl -s -H "X-Api-Key: $KEY" -H "Idempotency-Key: $(uuidgen)" \
-F "file=@sales.csv" -F "dashboardName=Sales, nightly" \
-F "userPrompt=Revenue by region and the trend by month" \
"$BASE/dashboards/generate/async?biTool=HTML" | jq -r .jobId)
# 2. Poll until it is COMPLETED (or FAILED, with errorMessage).
until [ "$(curl -s -H "X-Api-Key: $KEY" "$BASE/dashboards/jobs/$JOB" | jq -r .status)" \
= COMPLETED ]; do sleep 10; done
# 3. Download the finished file.
curl -s -H "X-Api-Key: $KEY" -o sales.html "$BASE/dashboards/jobs/$JOB/download"
The same call builds the other targets:
curl -s -H "X-Api-Key: $KEY" -F "file=@sales.csv" -F "dashboardName=Sales" \
"$BASE/dashboards/generate/async?biTool=SUPERSET"
curl -s -H "X-Api-Key: $KEY" -F "file=@sales.xlsx" -F "dashboardName=Sales" \
"$BASE/dashboards/generate/async?biTool=POWER_BI&outputFormat=PBIP"
A report on your own Power BI model. Upload a .pbit (File → Export → Power BI template) or a zip of the .pbip project folder instead of a data file, with biTool=POWER_BI: the report is built on your tables and measures, and the download is a project with your model folder byte-for-byte and a new report beside it — open the .pbip, press Refresh. Three parameters apply to that upload only (400 option-not-for-target on any other): reportOnly=true leaves the model folder out, for a report you place beside the folder you already have; modelConnection=semanticmodelid=… binds the report to a published model instead of a folder; addMeasures=true lets the design agent add measures, delivered as definition/tables/_Measures.tmdl beside the project. Charged as a Power BI run. The job's stages then read ["CHARTS","DASHBOARD"] (plus "MEASURES" with addMeasures) — that is how a model run is told from a file run. POST /datasets/inspect on such an upload answers the model's tables in the sheets slot with "model": true. Details: the upload-formats page, §4c.
See it, change it, then download it. A finished report on a model has a preview: GET /jobs/{id}/design answers its pages and visuals with rows invented from your model (every number is made up; previewData.source is INVENTED_FROM_MODEL). Two changes are free and run no AI: POST /jobs/{id}/charts/{chartId} with {"type":"PIE"} gives one visual another type (a type the report could not draw with that visual's fields is refused 422 with the reason; a library template is refused — a Power BI visual takes a type), and PUT /jobs/{id}/look with {"style":…,"design":…} another look. A change the AI has to make — POST /jobs/{id}/revisions with {"userPrompt":"Split the revenue page by country"} — is a new version: the model run again with every instruction so far, a new job to poll, charged as one Power BI run; without an instruction it is refused 400 revision-needs-prompt. GET /jobs/{id}/revisions lists the versions and POST /jobs/{id}/revisions/{n}/restore brings one back, free. GET /jobs/{id}/download?outputFormat=PBIT (for a .pbit upload) or PBIP builds the file from what the preview shows now; the header X-Artifact-Stale: true says the file is the last one built, because the model it would need is no longer kept.
curl -s -H "X-Api-Key: $KEY" -F "file=@Sales.pbit" -F "dashboardName=Sales" \
-F "userPrompt=Revenue by region and month, top customers" \
"$BASE/dashboards/generate/async?biTool=POWER_BI&reportOnly=true"
A poll answers the job's state and, once it is over, what it cost you:
{"jobId": "…", "status": "COMPLETED", "chargedCenticoins": 20, …}
chargedCenticoins is in hundredths of a Coin: null while the job runs, 0 for a run that was refunded.
A workbook takes sheet and headerRow on the start call, as in the app (Upload formats §3; POST /api/v1/dashboards/datasets/inspect shows the sheets first). style=<theme> picks the colours and design=<design> the look (HTML dashboard §5a; GET /api/v1/dashboards/designs and GET /api/v1/dashboards/styles list them); an unknown design is 404 design-not-found, a theme without colour tokens under a design 422 theme-not-combinable, and colours without a stylesheet on a run without a design 422 theme-not-classic. POST /dashboards/generate does all three steps in one call and answers with the file, for a caller that would rather hold the connection than poll.
A new version. On a finished HTML dashboard, POST /api/v1/dashboards/jobs/{id}/revisions with {"design": "brutal"} (or "none", or "style", "chartEdits", "userPrompt", a new "name", a "layout" — the grid whole, tabs down to cells — or per-chart "title", "columns", "sortColumn"+"sortAscending", "showTotals", "remove", or a "logo" — a raster image as a data:image/…;base64,… URI, at most 96 KB, "" to take it off) builds another version; the same path as multipart with a file part rebuilds it from a newer file. A chart edit may also change what the chart computes — "metric" and "additionalMetrics" by measure name (count, sum__amount, avg__amount, min__…, max__…, count_distinct__customer_no, a calculated calc__…), "dimension", "temporalColumn", "columnDimension", "clearDimension", "clearTemporalColumn" — together with a "type"; "addCharts" adds widgets ({"clientId":"new-1","type":"KPI","metric":"count"}, placed by their clientId in a "layout" or at the end of "activeTab"), and "calculated" adds, changes or removes calculated measures ({"clientId":"m1","label":"Cost per hour","formula":"{sum__cost} / {sum__hours}"}; 422 formula-invalid with position and token when it cannot be used). A measure or a field the dashboard's data does not offer, or an edit that would leave the chart undrawable, is 422 chart-edit-rejected with the reason. The editors' dry-run (POST …/jobs/{id}/edit-preview) and the design read behind them are the signed-in window's, not a key's. A hand edit does not combine with a userPrompt — the model designs its own layout, so the two are two versions. Each costs one HTML price through a key, takes an Idempotency-Key like a start, and is polled and downloaded like any job. GET …/jobs/{id}/revisions lists the versions, GET …/jobs/{id}/price says what a download would take.
Sharing. POST /api/v1/dashboards/jobs/{id}/share makes a public link (https://dashgen.de/d/…) to a finished HTML dashboard, GET shows it, DELETE stops it. Anyone with the link can open the dashboard.
Repeating a call. An Idempotency-Key header (any string up to 128 characters, a UUID per run is the usual choice) on a start or a new version is remembered for 24 hours: the same key again answers the job it already started and charges nothing.
Reference columns. If your file has an IP address or a German postal code in it, the dashboard gains country, city or Bundesland columns without being asked — see the reference tables page. Send enrich=none to switch that off for a run, or enrich=plz-de (a comma-separated list of names from GET /api/v1/references) to name the tables yourself. An unknown name answers 400 unknown-reference and lists what this deployment has.
Situational dashboards. A blueprint published from a finished HTML dashboard takes rows by a contract and answers a link that the people its owner named open after signing in: PUT /api/v1/situational/blueprints/{id}/instances/{ref} with {"rows": [ … ]}. Publishing a blueprint and naming its viewers are the app's, not a key's. ?format=html answers the file itself, with its record in X-Situational-* headers, and GET …/instances/{ref}?format=html fetches it again for free — 304 on a matching If-None-Match. On a blueprint that belongs to a company, a key delivers and does not read unless a company administrator switched that on for the blueprint: otherwise ?format=html is refused before the charge (403 api-key-read-refused), the drawn file is not found, and the contract always comes without its example rows. See the situational dashboards page and the companies page.
Quota. GET /api/v1/quota answers the plan, coins.balanceCenticoins, the price of each target and the reset date. Its htmlChanges counter is the app window's free allowance and means nothing to a key.
3. What a key reaches
| Endpoint | |
|---|---|
POST /api/v1/dashboards/generate, /generate/async | any target; paid at start |
POST /api/v1/dashboards/jobs/{id}/revisions (JSON or multipart) | a new version; paid at start |
GET /api/v1/dashboards/jobs/{id}, /jobs/{id}/download | the job and its file |
GET /api/v1/dashboards/jobs/{id}/revisions, /jobs/{id}/price | the versions, the download price |
GET, POST, DELETE /api/v1/dashboards/jobs/{id}/share | the public link |
POST /api/v1/dashboards/datasets/inspect | the sheets of a workbook before a run |
GET /api/v1/quota | Coins left and every price |
GET /api/v1/dashboards/designs[/{name}[/preview]], /dashboards/styles[/{name}] | designs and colour themes |
GET /api/v1/chart-templates[/{name}] | chart templates |
GET /api/v1/powerbi/themes[/{name}], /powerbi/visual-templates[/{name}], /powerbi/visual-types | Power BI templates |
GET /api/v1/references | reference tables |
PUT, GET, DELETE /api/v1/situational/blueprints/{id}/instances/{ref}, POST and GET …/instances, GET …/{id}/contract | a situational blueprint's deliveries and its contract; a delivery is paid, fetching its file again (GET …?format=html) is free |
GET /api/v1/situational/instances/{id}/html | a drawn situational dashboard — of a personal blueprint, or a company blueprint its administrator opened to the key; otherwise 404 to a key |
Anything else answers 403 api-key-scope before the key is looked up: making, changing or deleting a template or a design, publishing a situational blueprint or naming its viewers or members, everything under /api/v1/organizations (a company and who may read its dashboards), /api/v1/api-keys (so a leaked key cannot make the keys that would outlive its revocation), the job list, restoring a version, the step-by-step Power BI draft, billing and /api/v1/admin/**. A key carries no role, so an owner's key is not an owner.
It is meant for a server or an agent, not a web page: CORS does not allow the X-Api-Key header, so a browser on another origin cannot send it.
4. How it works
studio security/ApiKeyAuthenticationFilter runs before the bearer-token filter. A valid key becomes the same JwtAuthenticationToken a Keycloak login produces — the owner's subject and e-mail, plus ROLE_API_KEY and the key's prefix — so every controller, the provisioning filter, the plan gates and the coin ledger work unchanged. Where money is decided, security/ApiKeys tells a key from a login and takes the paid path: HtmlSettlementService.consumePaidRun charges one HTML price without touching the daily free-change counter, a download through a key ignores the free-run mark, and the promo period is skipped. The ledger row of a key's run names the key's prefix. The unsigned token it carries names the key's id, never the key, and is what studio forwards to the engines as Bearer, which they ignore (root invariant 2). Keys live in api_key (studio V029), idempotency keys in generation_idempotency (V037); last_used_at is written at most every five minutes, so a polling loop does not write a row per poll.
DASH:Gen