Developer Tooling & API

The Metabase API - A Developer's Reference Guide

The Metabase API is a REST API that exposes nearly every administrative and analytical operation available in the Metabase UI — creating dashboards, m...

📅
📖9 min read

The Metabase API: A Developer's Reference Guide

The Metabase API is a REST API that exposes nearly every administrative and analytical operation available in the Metabase UI — creating dashboards, managing users, executing queries, configuring permissions, and more — as HTTP endpoints that can be automated, scripted, and integrated into existing developer workflows. It is the foundation for treating Metabase as code: provisioning instances programmatically, managing configuration through CI/CD pipelines, and building integrations with other tools in your stack.

The API is available on all Metabase deployments — open source, Pro, and Enterprise — and is documented interactively at /api/docs on any running instance.

---

Authentication

Session Token Authentication

The Metabase API uses session tokens for authentication. Obtain a token by POSTing credentials to /api/session:

bash

curl -X POST \ -H "Content-Type: application/json" \ -d '{"username": "admin@yourcompany.com", "password": "your-password"}' \ https://your-metabase.com/api/session

Response:

json

{ "id": "session-token-string-here" }

Include the token in subsequent requests as the X-Metabase-Session header:

bash

curl -H "X-Metabase-Session: session-token-string-here" \ https://your-metabase.com/api/dashboard

API Key Authentication (Recommended for Automation)

For automated scripts and CI/CD pipelines, use API keys instead of session tokens. API keys don't expire and are tied to a specific user account.

Create an API key in Admin → Settings → Authentication → API Keys, then use it as the x-api-key header:

bash

curl -H "x-api-key: mb_your_api_key_here" \ https://your-metabase.com/api/dashboard

API keys are available in Metabase Pro and Enterprise. For open-source deployments, use session tokens.

Important: API Authentication in Automation

Never hardcode credentials in scripts. Use environment variables:

bash

export METABASE_API_KEY="mb_your_api_key_here" export METABASE_SITE_URL="https://your-metabase.com"

curl -H "x-api-key: $METABASE_API_KEY" \ "$METABASE_SITE_URL/api/dashboard"

---

API Structure and Conventions

The Metabase API follows REST conventions with a few quirks worth knowing:

Base URL

All API endpoints are under /api/:

https://your-metabase.com/api/{resource}

HTTP Methods

MethodUsage
GETRead resources
POSTCreate resources or perform actions
PUTUpdate resources (full replacement)
DELETEDelete resources

Response Format

All responses are JSON. Successful responses return the resource directly (not wrapped in a data key). Error responses include an errors or message field:

json

// Success { "id": 42, "name": "Sales Dashboard", ... }

// Error { "errors": { "name": "value must be a non-blank string" } }

Pagination

List endpoints return arrays. Some endpoints support limit and offset query parameters for pagination. Check individual endpoint documentation for pagination support.

---

Core API Endpoints

Databases

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all connected databases</h1> GET /api/database

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get a specific database</h1> GET /api/database/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Add a new database connection</h1> POST /api/database

<h1 class="text-4xl font-bold mb-6 text-slate-900">Update database connection settings</h1> PUT /api/database/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Trigger a manual schema sync</h1> POST /api/database/:id/sync_schema

<h1 class="text-4xl font-bold mb-6 text-slate-900">Trigger a manual field values rescan</h1> POST /api/database/:id/rescan_values

Add a database connection:

bash

curl -X POST \ -H "x-api-key: $METABASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Production DB", "engine": "postgres", "details": { "host": "your-db-host", "port": 5432, "dbname": "your_database", "user": "metabase_reader", "password": "your-password", "ssl": true } }' \ "$METABASE_SITE_URL/api/database"

Dashboards

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all dashboards</h1> GET /api/dashboard

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get a specific dashboard (with all cards)</h1> GET /api/dashboard/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Create a dashboard</h1> POST /api/dashboard

<h1 class="text-4xl font-bold mb-6 text-slate-900">Update a dashboard</h1> PUT /api/dashboard/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Delete a dashboard</h1> DELETE /api/dashboard/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Add a question card to a dashboard</h1> POST /api/dashboard/:id/cards

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get dashboard cards</h1> GET /api/dashboard/:id/cards

Create a dashboard:

bash

curl -X POST \ -H "x-api-key: $METABASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Customer Analytics", "description": "Key metrics for customer success", "collection_id": 5 }' \ "$METABASE_SITE_URL/api/dashboard"

Questions (Cards)

In Metabase's API, questions are called "cards":

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all questions</h1> GET /api/card

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get a specific question</h1> GET /api/card/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Create a question</h1> POST /api/card

<h1 class="text-4xl font-bold mb-6 text-slate-900">Update a question</h1> PUT /api/card/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Delete a question</h1> DELETE /api/card/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Execute a question and return results</h1> POST /api/card/:id/query

Execute a question and get results:

bash

curl -X POST \ -H "x-api-key: $METABASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"parameters": []}' \ "$METABASE_SITE_URL/api/card/42/query"

Response includes the query results as a dataset:

json

{ "data": { "rows": [[1, "Acme Corp", 15000], [2, "Globex", 9500]], "cols": [ {"name": "id", "display_name": "ID", "base_type": "type/Integer"}, {"name": "name", "display_name": "Name", "base_type": "type/Text"}, {"name": "revenue", "display_name": "Revenue", "base_type": "type/Integer"} ] }, "row_count": 2 }

Running Ad-Hoc Queries

Execute a SQL query against a connected database without saving it as a question:

bash

POST /api/dataset

bash

curl -X POST \ -H "x-api-key: $METABASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "database": 2, "native": { "query": "SELECT COUNT(*) as total_orders FROM orders WHERE created_at > now() - interval '"'"'7 days'"'"'" }, "type": "native" }' \ "$METABASE_SITE_URL/api/dataset"

Collections

Collections are folders for organizing questions and dashboards:

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all collections</h1> GET /api/collection

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get items in a collection</h1> GET /api/collection/:id/items

<h1 class="text-4xl font-bold mb-6 text-slate-900">Create a collection</h1> POST /api/collection

<h1 class="text-4xl font-bold mb-6 text-slate-900">Move an item to a collection</h1> PUT /api/card/:id # update collection_id field PUT /api/dashboard/:id # update collection_id field

Users and Groups

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all users</h1> GET /api/user

<h1 class="text-4xl font-bold mb-6 text-slate-900">Create a user</h1> POST /api/user

<h1 class="text-4xl font-bold mb-6 text-slate-900">Update a user</h1> PUT /api/user/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">Deactivate a user</h1> DELETE /api/user/:id

<h1 class="text-4xl font-bold mb-6 text-slate-900">List all groups</h1> GET /api/permissions/group

<h1 class="text-4xl font-bold mb-6 text-slate-900">Create a group</h1> POST /api/permissions/group

<h1 class="text-4xl font-bold mb-6 text-slate-900">Add a user to a group</h1> POST /api/permissions/group/:group_id/membership

Create a user:

bash

curl -X POST \ -H "x-api-key: $METABASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jane", "last_name": "Smith", "email": "jane@yourcompany.com", "password": "temporary-password-123" }' \ "$METABASE_SITE_URL/api/user"

Permissions

bash

<h1 class="text-4xl font-bold mb-6 text-slate-900">Get the permissions graph (all permissions for all groups)</h1> GET /api/permissions/graph

<h1 class="text-4xl font-bold mb-6 text-slate-900">Update the permissions graph</h1> PUT /api/permissions/graph

The permissions graph is a nested JSON object representing all group permissions across all databases. Updating it requires sending the complete graph — partial updates are not supported.

---

Common Automation Patterns

Pattern 1: Provisioning a New Tenant

When a new customer signs up for your SaaS product, automatically provision their Metabase access:

javascript

async function provisionTenantAccess(tenant) { const baseUrl = process.env.METABASE_SITE_URL; const headers = { "x-api-key": process.env.METABASE_API_KEY, "Content-Type": "application/json", };

// 1. Create a group for this tenant const groupRes = await fetch(${baseUrl}/api/permissions/group, { method: "POST", headers, body: JSON.stringify({ name: Tenant: ${tenant.name} }), }); const group = await groupRes.json();

// 2. Create an admin user for the tenant const userRes = await fetch(${baseUrl}/api/user, { method: "POST", headers, body: JSON.stringify({ first_name: tenant.adminFirstName, last_name: tenant.adminLastName, email: tenant.adminEmail, password: generateTempPassword(), }), }); const user = await userRes.json();

// 3. Add the user to the tenant's group await fetch(${baseUrl}/api/permissions/group/${group.id}/membership, { method: "POST", headers, body: JSON.stringify({ user_id: user.id }), });

return { groupId: group.id, userId: user.id }; }

Pattern 2: Programmatic Dashboard Creation

Build a script that creates a standardized dashboard for each new product or team:

javascript

async function createStandardDashboard(name, collectionId, databaseId) { const baseUrl = process.env.METABASE_SITE_URL; const headers = { "x-api-key": process.env.METABASE_API_KEY, "Content-Type": "application/json", };

// 1. Create the dashboard const dashRes = await fetch(${baseUrl}/api/dashboard, { method: "POST", headers, body: JSON.stringify({ name, collection_id: collectionId, }), }); const dashboard = await dashRes.json();

// 2. Create a question const cardRes = await fetch(${baseUrl}/api/card, { method: "POST", headers, body: JSON.stringify({ name: ${name} - Orders Over Time, display: "line", dataset_query: { database: databaseId, type: "query", query: { "source-table": TABLE_ID, aggregation: [["count"]], breakout: [["field", DATE_FIELD_ID, { "temporal-unit": "week" }]], }, }, visualization_settings: {}, collection_id: collectionId, }), }); const card = await cardRes.json();

// 3. Add the question to the dashboard await fetch(${baseUrl}/api/dashboard/${dashboard.id}/cards, { method: "POST", headers, body: JSON.stringify({ cardId: card.id, row: 0, col: 0, size_x: 12, size_y: 6, }), });

return dashboard.id; }

Pattern 3: Extracting Query Results for External Use

Use the API to pull data from Metabase questions into another system (a data export, a Slack alert, a custom report):

javascript

async function getQuestionData(questionId, parameters = []) { const res = await fetch( ${process.env.METABASE_SITE_URL}/api/card/${questionId}/query, { method: "POST", headers: { "x-api-key": process.env.METABASE_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ parameters }), } );

const result = await res.json(); const { rows, cols } = result.data;

// Convert to array of objects return rows.map((row) => Object.fromEntries(cols.map((col, i) => [col.name, row[i]])) ); }

// Usage const orders = await getQuestionData(42, [ { type: "date/range", value: "2024-01-01~2024-12-31", target: ["variable", ["template-tag", "date_range"]] } ]); // orders = [{ order_id: 1, customer: "Acme", amount: 500 }, ...]

---

Rate Limits and Error Handling

The Metabase API does not publish formal rate limits, but it's good practice to:

  • Add delays between bulk operations (e.g., creating many users)
  • Implement exponential backoff on 429 or 503 responses
  • Avoid running many concurrent API requests against a small instance
  • javascript
    

    async function apiRequest(url, options, retries = 3) { for (let attempt = 0; attempt < retries; attempt++) { const res = await fetch(url, options);

    if (res.ok) return res.json();

    if (res.status === 429 || res.status === 503) { const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s await new Promise((r) => setTimeout(r, delay)); continue; }

    const error = await res.json(); throw new Error(API error ${res.status}: ${JSON.stringify(error)}); }

    throw new Error(Failed after ${retries} retries); }

    ---

    Exploring the API

    Every Metabase instance exposes interactive API documentation at:

    https://your-metabase.com/api/docs

    This is the most accurate reference for the version you're running — it reflects the exact endpoints and request shapes available on your instance. The API surface can change between Metabase versions, so always reference /api/docs on your specific deployment rather than relying on third-party documentation.

    You can also discover the API surface by watching network traffic in your browser's DevTools while using the Metabase UI — every UI action maps to one or more API calls.

    ---

    Summary

    The Metabase REST API covers all major operations: database management, dashboard and question CRUD, query execution, user and group management, and permissions. Authentication is via API key (recommended for automation) or session token. Common use cases include provisioning new tenants, creating standardized dashboards programmatically, and extracting query results into external systems. The API is available on all Metabase plans and is self-documented at /api/docs on every running instance.