Back to Article List

Grafana API: Service accounts and tokens

Grafana API: Service accounts and tokens

Almost everything you can click in Grafana has an HTTP endpoint behind it. Dashboards, data sources, alert rules, folders, users, annotations, all of it. That makes the API the natural way to back things up, mirror a dashboard between environments or wire Grafana into whatever automation you already run.

Two things have changed enough recently that older tutorials will send you wrong. API keys are gone, replaced by service account tokens. And Grafana 13 has started deprecating the legacy /api routes in favour of a new /apis structure. Both are covered below, along with a set of curl recipes and a backup script at the end.

What replaced API keys

Service accounts went generally available in Grafana 9.1 and API keys were deprecated behind them. An API key was a credential with a role stapled to it. A service account is an identity: it can hold multiple tokens, it can be disabled without deleting anything, it takes fine-grained RBAC permissions rather than one of three built-in roles, and it appears in the user list where you can audit it.

The migration docs put it plainly: "Your existing API key, now migrated to a service account token, will continue working as before." Grafana's own announcement of the change set 31 August 2024 as the date new API key creation stopped and 31 January 2025 for automatically migrating anything left over and removing the API key endpoints. If you are running a version from that era or later, you already have service accounts, made for you or not.

Nothing about your scripts needs to change. A service account token goes in the same Authorization: Bearer header an API key did. What changes is where you create it and how tightly you can scope it.

Create a service account and token in the UI

Click Administration in the left-side menu, then Users and access, then Service accounts, then Add service account. Give it a name and a role.

Then open the account you just made and click Add service account token. Enter a name, tick Set expiration date and pick one, then click Generate token. The value appears once. It starts with glsa_ and if you lose it you make another one, since there is no way to read it back.

Name tokens after the thing that uses them. ci-dashboard-sync tells you what breaks if you revoke it. dave-test tells you nothing, and in two years Dave will have left.

Create a service account and token with the API

You can also do the whole thing over HTTP, which is what you want in a bootstrap script. Note that this particular call uses basic auth, since you need an existing credential to create the first one:

curl -s -X POST http://localhost:3000/api/serviceaccounts \
  -u admin:yourpassword \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-backup","role":"Viewer","isDisabled":false}'

That returns the account with an id. Feed it to the token endpoint:

curl -s -X POST http://localhost:3000/api/serviceaccounts/2/tokens \
  -u admin:yourpassword \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-backup-token","secondsToLive":604800}'

The response carries id, name and key, and key is the token. secondsToLive is optional; 604800 is seven days. The rest of the set:

Method and pathWhat it doesPermission
GET /api/serviceaccounts/searchSearch accounts, takes perpage, page and queryserviceaccounts:read
GET /api/serviceaccounts/:idOne account by IDserviceaccounts:read
PATCH /api/serviceaccounts/:idRename, change role or disableserviceaccounts:write
DELETE /api/serviceaccounts/:idDelete the account and its tokensserviceaccounts:delete
GET /api/serviceaccounts/:id/tokensList tokens with names, expiry and statusserviceaccounts:read
DELETE /api/serviceaccounts/:id/tokens/:tokenIdRevoke one tokenserviceaccounts:write

Authenticate with a Bearer token

Every token-authenticated call takes the same header. Export the token once and stop typing it:

export GRAFANA_URL="http://localhost:3000"
export GRAFANA_TOKEN="glsa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

The health endpoint needs no auth at all, which makes it the right first request when you are working out if the URL is even correct:

curl -s "$GRAFANA_URL/api/health"
{
  "commit": "087143285",
  "database": "ok",
  "version": "5.1.3"
}

That database field is the useful one. It says ok when Grafana can reach its own SQLite or Postgres, which separates "Grafana is down" from "Grafana is up and its database is not". Point your uptime check at this rather than at the login page. The version string in the sample above is whatever build the documentation was written against, so read past it; yours reports your own.

Now with the token:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/api/access-control/user/permissions" | jq

That prints every action the token is allowed to perform. When a later call comes back 403, this is where you find out why, and it beats guessing at role names.

Legacy /api routes and the new /apis routes

The new API structure reference states it directly: "Grafana 13 deprecates legacy API endpoints (/api) in favor of a new generation of improved APIs (/apis). Legacy APIs are not being disabled for the moment. Removal of legacy APIs is planned for a future major release, and any breaking changes will be announced well in advance."

The new routes follow a Kubernetes-style path:

/apis/<GROUP>/<VERSION>/namespaces/<NAMESPACE>/<RESOURCE>[/<NAME>]

The namespace segment varies. In OSS and on-premise it is default for organization 1 and org-<ORG_ID> for any other org. In Grafana Cloud it is stacks-<STACK_ID>. So a dashboard read looks like this:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/apis/dashboard.grafana.app/v1/namespaces/default/dashboards/production-overview" | jq

For now: write new automation against /apis where a documented equivalent exists, and keep using /api where it does not, which is still most of the surface. The legacy routes work and are not going anywhere this year. Grafana's own docs still index them under a page titled Legacy Grafana HTTP API, so you can look them up without guessing.

Endpoint recipes worth keeping

Everything below assumes the Authorization: Bearer header from the previous section.

Dashboard endpoints

Search is the discovery route:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/api/search?query=&type=dash-db&limit=5000" | jq -r '.[] | "\(.uid)\t\(.title)"'

Parameters are query, tag, type (dash-db for dashboards, dash-folder for folders), dashboardUIDs, folderUIDs, starred, limit and page. The limit maxes out at 5000 and defaults to 1000, so page through on a large instance.

Reading and writing one dashboard, in the new structure:

BASE="$GRAFANA_URL/apis/dashboard.grafana.app/v1/namespaces/default/dashboards"

# list
curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$BASE"

# one dashboard by UID
curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$BASE/production-overview"

# delete
curl -s -X DELETE -H "Authorization: Bearer $GRAFANA_TOKEN" "$BASE/production-overview"

Creating one is a POST to the collection with a Kubernetes-shaped body: the UID goes in metadata.name, the dashboard JSON goes in spec, and the folder and commit message go in annotations:

{
  "metadata": {
    "name": "production-overview",
    "annotations": {
      "grafana.app/folder": "<folder uid>",
      "grafana.app/message": "imported from git"
    }
  },
  "spec": {
    "title": "Production overview",
    "schemaVersion": 41,
    "panels": []
  }
}

An update is a PUT to $BASE/<uid> with the same shape. Both need dashboards:write, and creation also needs dashboards:create.

Data source endpoints

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/api/datasources" | jq -r '.[] | "\(.uid)\t\(.type)\t\(.name)"'
Method and pathWhat it does
GET /api/datasourcesList all, needs datasources:read at datasources:*
GET /api/datasources/uid/:uidOne by UID
GET /api/datasources/name/:nameOne by name
POST /api/datasourcesCreate, needs datasources:create
PUT /api/datasources/uid/:uidUpdate
DELETE /api/datasources/uid/:uidDelete
GET /api/datasources/uid/:uid/healthThe same check the Save & test button runs
POST /api/ds/queryRun a query through Grafana against a data source

Creating a Prometheus data source:

curl -s -X POST "$GRAFANA_URL/api/datasources" \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Prometheus",
    "type": "prometheus",
    "url": "http://localhost:9090",
    "access": "proxy",
    "isDefault": true
  }'

Anything secret goes in secureJsonData rather than at the top level, so a basic auth password is {"secureJsonData":{"basicAuthPassword":"..."}}. Grafana encrypts those with the secret key from grafana.ini and never returns them on a GET. That is correct behaviour, and it also means the API cannot recover a password you have forgotten. The walkthrough for connecting Grafana to Prometheus covers what belongs in that URL field once containers are involved, which is the part of this call that gets fixed twice.

Alert rule and notification endpoints

Alerting lives under the provisioning API, which is a slightly confusing name because it works over HTTP like everything else:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/api/v1/provisioning/alert-rules" | jq -r '.[] | "\(.uid)\t\(.title)"'
Method and pathWhat it does
GET /api/v1/provisioning/alert-rulesAll alert rules
GET /api/v1/provisioning/alert-rules/:uidOne rule
POST /api/v1/provisioning/alert-rulesCreate a rule
PUT /api/v1/provisioning/alert-rules/:uidUpdate a rule
DELETE /api/v1/provisioning/alert-rules/:uidDelete a rule
GET /api/v1/provisioning/alert-rules/exportEvery rule as provisioning YAML or JSON
GET /api/v1/provisioning/contact-pointsList contact points
GET /api/v1/provisioning/policiesThe notification policy tree
GET /api/v1/provisioning/mute-timingsList mute timings
GET /api/v1/provisioning/templatesList notification templates

GET /api/v1/provisioning/alert-rules/export hands you every rule in the file format the provisioning system reads, so you can build your alerting in the UI where it is pleasant, export it and commit the result. Contact points, policies and mute timings all have the same export route.

Anything created through this API is marked as provisioned, which makes it read-only in the UI. Send X-Disable-Provenance: true on the request and the resource stays editable in Grafana afterwards. Turning it on makes sense when the UI is where people work and the API is only seeding the initial state.

Silences are missing from that list. They are not covered by the documented provisioning endpoints, and I have not found a supported path for scripting them, so check your instance's own API explorer rather than copying a path out of a forum post. Setting up email alerting is faster through the UI the first time round in any case, and alert rules, contact points and mute timings cover most of what gets automated anyway.

Organizations, users and the admin endpoints

A service account belongs to one organization and holds one organization role. It cannot be a Grafana server administrator, and no amount of permission granting changes that. So the endpoints that operate above the org level do not accept tokens at all. The admin API docs say so outright: "To use the Admin API endpoints you have to use Basic authentication, and the Grafana user must have the Grafana server administrator permission", and separately, "You can't authenticate to the Admin HTTP API with service account tokens."

Token works:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$GRAFANA_URL/api/org"
curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" "$GRAFANA_URL/api/org/users" | jq -r '.[] | "\(.login)\t\(.role)"'

Basic auth as a server admin only:

curl -s -u admin:yourpassword "$GRAFANA_URL/api/orgs"
curl -s -u admin:yourpassword "$GRAFANA_URL/api/admin/stats" | jq
curl -s -u admin:yourpassword "$GRAFANA_URL/api/admin/settings" | jq '.smtp'

That last one is genuinely useful for debugging, because it returns the running configuration as Grafana resolved it after all five layers of config loading. When you have edited grafana.ini and nothing changed, this tells you what the process really believes, and it is faster than reasoning about override precedence. It also means the admin password is a real secret with real reach, which is a thought worth having while you are still on the default admin login. Rotate it before you script anything.

Token expiry and rotation

By default service account tokens never expire. You can force the issue in /etc/grafana/grafana.ini:

[service_accounts]
token_expiration_day_limit = 90

The comment on that key in Grafana's shipped defaults reads "When set, Grafana will not allow the creation of tokens with expiry greater than this setting." It is empty out of the box, which is why so many instances are carrying tokens somebody generated in 2022.

Rotation is create-then-revoke, in that order. Make the new token, deploy it, confirm the thing using it still works, then delete the old one by ID:

curl -s -H "Authorization: Bearer $GRAFANA_TOKEN" \
  "$GRAFANA_URL/api/serviceaccounts/2/tokens" | jq -r '.[] | "\(.id)\t\(.name)\t\(.expiration)"'

curl -s -X DELETE -u admin:yourpassword \
  "$GRAFANA_URL/api/serviceaccounts/2/tokens/7"

One service account can hold several tokens at once, which is exactly what makes zero-downtime rotation possible. Use it. Deleting the old token first and then discovering your CI job runs hourly is a bad afternoon.

Back up every dashboard to JSON on a schedule

The script searches for every dashboard, pulls each one and writes it to a dated directory. The section on dashboards as code in the guide to Grafana dashboards is the other half of this conversation, if you would rather keep them in files from the start:

#!/usr/bin/env bash
set -euo pipefail

GRAFANA_URL="${GRAFANA_URL:?set GRAFANA_URL}"
GRAFANA_TOKEN="${GRAFANA_TOKEN:?set GRAFANA_TOKEN}"
OUT="/var/backups/grafana/$(date +%F)"
AUTH=(-H "Authorization: Bearer ${GRAFANA_TOKEN}")

mkdir -p "$OUT"

curl -sf "${AUTH[@]}" "${GRAFANA_URL}/api/search?type=dash-db&limit=5000" \
  | jq -r '.[].uid' \
  | while read -r uid; do
      curl -sf "${AUTH[@]}" \
        "${GRAFANA_URL}/apis/dashboard.grafana.app/v1/namespaces/default/dashboards/${uid}" \
        > "${OUT}/${uid}.json"
      echo "saved ${uid}"
    done

find /var/backups/grafana -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

Make it executable, drop it at /usr/local/bin/grafana-backup.sh and run it from a systemd timer or cron:

15 3 * * * GRAFANA_URL=http://localhost:3000 GRAFANA_TOKEN=glsa_xxx /usr/local/bin/grafana-backup.sh

Four notes on that script. set -euo pipefail and curl -sf together mean a 401 or a 500 stops the run instead of writing an error page into a file called production-overview.json, which is the failure mode of every backup script that does not do this. The token comes from an environment variable, so the script file is safe to commit. The find at the end keeps thirty days and no more. And check the JSON shape once by hand before trusting it, because the new API wraps the dashboard in metadata and spec rather than returning the bare dashboard object the way the legacy route did, so jq keys on one file tells you what your restore path needs to unwrap.

To keep them in git, point OUT at a checkout, add jq -S . to sort keys so diffs are readable, and commit. Without sorted keys, Grafana's field ordering shifts between saves and every dashboard looks changed on every run.

Provisioning files versus the API

Anything that should be version controlled goes in provisioning files. The API is for the things provisioning cannot do.

Grafana reads YAML from the provisioning/ directory (on a package install, /etc/grafana/provisioning) with subdirectories for datasources, dashboards, plugins and alerting. A data source file looks like this:

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://localhost:9090
    isDefault: true
    editable: false

Everything about that is better than the equivalent API call. It is idempotent, so applying it twice does nothing the second time. It lives in git next to the rest of your infrastructure. It survives a rebuild of the instance without anyone running a script. And editable: false stops someone changing it in the UI and creating a difference between what is deployed and what is in the repository. The same pattern applies inside a container, where the Grafana Docker Compose setup mounts the provisioning directory as a volume. The file itself does not change.

What provisioning cannot do is anything conditional or anything about state rather than configuration. Reading which alerts fired last night. Backing up dashboards that people build in the UI. Creating a folder because a new customer signed up. Rotating its own credentials. Those are API jobs, and trying to force them into YAML produces something worse than either approach.

The line I use is: if a human should review it in a pull request, it is a provisioning file. If a machine needs to read or react to something, it is an API call. Configuration that ends up on the API side of that line drifts quietly, and after a year nobody can say what the deployed state is meant to be.

A third option now sits between those two. Grafana can sync dashboards from a git repository directly, which puts the version control story inside the product. If you are building this from scratch today, look at that before writing a sync script, since it removes the half of this article you would otherwise be maintaining. The HTTP API reference is the place to check which endpoints are current, and the provisioning documentation covers the file formats on the other side of that line.

Your idea deserves better hosting

24/7 support 30-day money-back guarantee Cancel anytime
Ciclo de Facturación

VPS.S1

$5.99 Save  17 %
$4.99 Mensual
  • 2 vCPU AMD EPYC
  • 2 GB RAMMEMORIA
  • 30 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos

VPS.S3

$14.99 Save  33 %
$9.99 Mensual
  • 4 vCPU AMD EPYC
  • 6 GB RAMMEMORIA
  • 70 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos

EPYC VPS.P1

$8.99 Save  22 %
$6.99 Mensual
  • 2 vCPU AMD EPYC
  • 4 GB RAMMEMORIA
  • 40 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

EPYC VPS.P2

$16.99 Save  24 %
$12.99 Mensual
  • 2 vCPU AMD EPYC
  • 8 GB RAMMEMORIA
  • 80 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

EPYC VPS.P4

$29.99 Save  23 %
$22.99 Mensual
  • 4 vCPU AMD EPYC
  • 16 GB RAMMEMORIA
  • 160 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

EPYC VPS.P5

$39.99 Save  25 %
$29.99 Mensual
  • 8 vCPU AMD EPYC
  • 16 GB RAMMEMORIA
  • 180 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

EPYC VPS.P6

$59.99 Save  25 %
$44.99 Mensual
  • 8 vCPU AMD EPYC
  • 32 GB RAMMEMORIA
  • 200 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

EPYC VPS.P7

$69.99 Save  29 %
$49.99 Mensual
  • 16 vCPU AMD EPYC
  • 32 GB RAMMEMORIA
  • 240 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

Genoa VPS.G2

$24.99 Save  20 %
$19.99 Mensual
  • 2 vCPUAMD EPYC Genoa de 4ª generación 9xx4 a 3,25 GHz o similar, en arquitectura Zen 4. AMD EPYC G4
  • 4 GB DDR5MEMORIA
  • 50 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

Genoa VPS.G4

$44.99 Save  22 %
$34.99 Mensual
  • 4 vCPUProcesador AMD EPYC con núcleos vCPU dedicados, en hardware de servidor empresarial. AMD EPYC G4
  • 8 GB DDR5MEMORIA
  • 100 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

Genoa VPS.G6

$89.99 Save  22 %
$69.99 Mensual
  • 8 vCPUProcesador AMD EPYC con núcleos vCPU dedicados, en hardware de servidor empresarial. AMD EPYC G4
  • 16 GB DDR5MEMORIA
  • 200 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

Genoa VPS.G7

$159.99 Save  22 %
$124.99 Mensual
  • 8 vCPUProcesador AMD EPYC con núcleos vCPU dedicados, en hardware de servidor empresarial. AMD EPYC G4
  • 32 GB DDR5MEMORIA
  • 250 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos. incluidos
  • Backup automático gratisIncluye un espacio de backup que puedes configurar para que se ejecute a diario, cada semana o cada mes.

AMD Ryzen VPS.R1

$16.99 Save  18 %
$13.99 Mensual
  • 1 CPU dedicada AMD Ryzen 9 7950X a 4,5 GHz o similar, en arquitectura Zen 4. vCPU
  • 4 GB DDR5MEMORIA
  • 50 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6 incluidos El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos.
  • Backup automático incluido

AMD Ryzen VPS.R2

$29.99 Save  17 %
$24.99 Mensual
  • 2 CPU dedicadas AMD Ryzen 9 7950X a 4,5 GHz o similar, en arquitectura Zen 4. vCPU
  • 8 GB DDR5MEMORIA
  • 100 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6 incluidos El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos.
  • Backup automático incluido

AMD Ryzen VPS.R4

$109.99 Save  18 %
$89.99 Mensual
  • 8 CPU dedicadas AMD Ryzen 9 7950X a 4,5 GHz o similar, en arquitectura Zen 4. vCPU
  • 32 GB DDR5MEMORIA
  • 400 GB NVMeDISCO
  • Ancho de banda sin medir
  • IPv4 & IPv6 incluidos El soporte IPv6 no está disponible actualmente en Francia, Finlandia ni Países Bajos.
  • Backup automático incluido

Frequent questions

Can I use a service account token to create another service account?

Yes, if the token's own permissions include serviceaccounts:create, which an Admin-role service account has and a Viewer one does not. It is a reasonable bootstrap pattern: one long-lived admin-scoped account that exists only to mint short-lived scoped tokens for everything else, kept in a secret store and never used for anything directly. The limit is that it still cannot reach the server-admin endpoints, so cross-organization work needs basic auth as a Grafana server administrator no matter how the token is scoped.