# The Tableau REST API with a Connected App (JWT)

> Authenticate to the Tableau REST API with a Connected App JWT instead of a password or PAT, and the endpoints you can reach once you do.

Canonical: https://ext.tableauops.com/knowledge/rest-api-connected-app
Updated: 2026-09-19
Author: Eric Summers


The Tableau REST API is how you automate Tableau Server and Tableau Cloud from the outside: publish and download workbooks, refresh extracts, pull view data and images, manage users, and read permissions. This page is the practical map: how to sign in with a **Connected App** so you never store a password, and the endpoints you can call once you hold a session token.

A Connected App with **direct trust** lets your service mint a short-lived JSON Web Token (JWT), sign it with a secret, and exchange it for a session. No stored password, no personal access token to rotate by hand, and you can scope exactly what the token is allowed to do.

## What you need first

Create the Connected App in Tableau (Settings, then Connected Apps, then "Direct Trust"). It gives you three values:

| Value | Where it goes |
|---|---|
| **Client ID** (the Connected App id) | the JWT `iss` claim |
| **Secret ID** | the JWT header `kid` |
| **Secret value** | the HMAC key you sign the JWT with (store it like a password) |

The Connected App must be **enabled**, and the user you sign in as (`sub`) must already exist on the site with the rights you expect. Direct trust for the REST API needs Tableau Cloud, or Tableau Server 2022.1 or later.

## Step 1: mint the JWT

Sign an HS256 JWT with the secret value. The header carries the secret id and the client id; the payload carries the user to act as, the audience, an expiry, a unique id, and the scopes.

```
// header
{ "alg": "HS256", "typ": "JWT", "kid": "<SECRET_ID>", "iss": "<CLIENT_ID>" }

// payload
{
  "iss": "<CLIENT_ID>",
  "sub": "user@example.com",        // the Tableau user to act as
  "aud": "tableau",
  "exp": <now + 5 minutes, epoch>,   // keep it short; Tableau rejects long-lived tokens
  "jti": "<a fresh UUID per token>",
  "scp": ["tableau:content:read", "tableau:views:download"]
}
```

Rules that trip people up:

- **`exp` must be short.** Use a few minutes. A token dated far in the future is refused. Keep your server clock in sync, because clock skew shows up here first.
- **`jti` must be unique per token.** Reuse is treated as a replay and rejected.
- **`aud` is the literal string `tableau`.**
- **`scp` must list every scope your calls need.** If a call needs a scope the token does not carry, it fails even though the user has the right in the UI. Scopes follow the pattern `tableau:<resource>:<action>` (for example `tableau:content:read`, `tableau:views:download`, `tableau:workbooks:download`, `tableau:tasks:run`). Check Tableau's Connected Apps scopes reference for the exact string an endpoint expects, and request the narrowest set that works.

## Step 2: exchange the JWT for a session

Post the JWT to the sign-in endpoint. The site is identified by its `contentUrl` (the slug in the site URL; the empty string is the default site on Server).

```
POST https://<your-server>/api/<version>/auth/signin
Content-Type: application/json

{ "credentials": { "jwt": "<JWT>", "site": { "contentUrl": "<site-content-url>" } } }
```

The response carries three things you keep for the rest of the session: the credentials **token**, the **site id**, and the **user id**. Send the token on every later call as the header `X-Tableau-Auth: <token>`. Sessions expire (commonly after a period of inactivity), so sign in again when a call returns 401. Sign out with `POST /api/<version>/auth/signout` when you are done.

Set `<version>` to the REST API version your Tableau supports (for example `3.21`). Add `Accept: application/json` if you want JSON back; the API speaks XML by default.

## The endpoint sheet {#endpoints}

Almost every resource lives under `/api/<version>/sites/<site-id>/...`, using the site id from sign-in. This is the working subset, grouped by what you are trying to do. Each row lists the scope area you will typically need in `scp`.

### Auth and site

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| POST | `/auth/signin` | (the JWT itself) | Exchange the JWT for a session token |
| POST | `/auth/signout` | session | End the session |
| GET | `/sites/<site-id>` | content:read | Read the current site's settings |
| GET | `/sites/<site-id>/projects` | content:read | List projects (the folders content lives in) |

### Workbooks

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| GET | `/sites/<site-id>/workbooks` | content:read | List workbooks (filter by project, owner, name) |
| GET | `/sites/<site-id>/workbooks/<id>` | content:read | One workbook's metadata and its views |
| GET | `/sites/<site-id>/workbooks/<id>/content` | workbooks:download | Download the `.twbx` |
| POST | `/sites/<site-id>/workbooks` | content (publish) | Publish a workbook (multipart upload) |
| PUT | `/sites/<site-id>/workbooks/<id>` | content | Update a workbook (project, owner, tags) |
| DELETE | `/sites/<site-id>/workbooks/<id>` | content | Delete a workbook |
| POST | `/sites/<site-id>/workbooks/<id>/refresh` | tasks:run | Trigger an extract refresh now |

### Views

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| GET | `/sites/<site-id>/views` | content:read | List views across the site |
| GET | `/sites/<site-id>/views/<id>/data` | views:download | The view's summary data as CSV |
| GET | `/sites/<site-id>/views/<id>/image` | views:download | Render the view to PNG (supports `?resolution=high`) |
| GET | `/sites/<site-id>/views/<id>/pdf` | views:download | Render the view to PDF |
| GET | `/sites/<site-id>/views/<id>/crosstab/excel` | views:download | The view as an Excel crosstab |

Filter a rendered view by adding `?vf_<Field Name>=<value>` to the image, pdf, or data call, the same way a URL parameter filters an embedded view.

### Data sources

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| GET | `/sites/<site-id>/datasources` | content:read | List published data sources |
| GET | `/sites/<site-id>/datasources/<id>/content` | datasources:download | Download the `.tdsx` |
| POST | `/sites/<site-id>/datasources` | content (publish) | Publish a data source |
| POST | `/sites/<site-id>/datasources/<id>/refresh` | tasks:run | Refresh its extract now |

### Jobs, schedules, and tasks

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| GET | `/sites/<site-id>/jobs` | tasks | List background jobs (refreshes, subscriptions) |
| GET | `/sites/<site-id>/jobs/<id>` | tasks | Poll one job's status (queued, running, success, failed) |
| GET | `/sites/<site-id>/tasks/extractRefreshes` | tasks | List scheduled extract-refresh tasks |
| POST | `/sites/<site-id>/tasks/extractRefreshes/<id>/runNow` | tasks:run | Run a scheduled refresh task now |

An extract refresh returns a **job id**. Poll `GET /jobs/<id>` until it finishes rather than assuming it is done.

### Users, groups, and permissions

| Method | Path | Scope area | Purpose |
|---|---|---|---|
| GET | `/sites/<site-id>/users` | users:read | List users on the site |
| POST | `/sites/<site-id>/users` | users | Add a user to the site |
| GET | `/sites/<site-id>/groups` | groups:read | List groups |
| GET | `/sites/<site-id>/workbooks/<id>/permissions` | permissions | Read a workbook's permission rules |

### Metadata (GraphQL)

For "where is this field used" and lineage questions, the Metadata API is a single GraphQL endpoint rather than REST:

| Method | Path | Purpose |
|---|---|---|
| POST | `/api/metadata/graphql` | Query lineage: which workbooks use a data source, which fields a sheet reads, downstream impact |

The same `X-Tableau-Auth` token authorizes it.

## A worked run

Sign in, list workbooks, and save a view as a PNG:

```
# 1. mint the JWT (in your language of choice), then:
TOKEN=$(curl -s -X POST https://SERVER/api/3.21/auth/signin \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"credentials":{"jwt":"'"$JWT"'","site":{"contentUrl":"SITE"}}}' \
  | jq -r .credentials.token)
SITE=$(...)   # site id from the same response

# 2. list workbooks
curl -s -H "X-Tableau-Auth: $TOKEN" -H "Accept: application/json" \
  https://SERVER/api/3.21/sites/$SITE/workbooks

# 3. render a view to PNG
curl -s -H "X-Tableau-Auth: $TOKEN" \
  https://SERVER/api/3.21/sites/$SITE/views/VIEW_ID/image?resolution=high \
  -o view.png
```

## Doing it the TableauOps way

TableauOps Scout already speaks the REST API for you: it signs in with a Connected App, watches extract refreshes and health, and surfaces failures, so you do not hand-roll the polling. If you want an AI agent to drive Tableau, the [MCP connector](/mcp) is the same idea one level up: connect it and the agent calls these operations through a tool, with the auth handled for it.

## FAQ

### Why use a Connected App instead of a personal access token?
A Connected App JWT is minted fresh, expires in minutes, and carries only the scopes you grant, so a leaked token is low-value and short-lived. A PAT is a long-lived secret tied to one user that you have to store and rotate. For a service, the Connected App is the safer default.

### My JWT sign-in returns 401. What is wrong?
Work through the usual four: the Connected App is disabled, the `exp` is too far out (or your clock is skewed), the `jti` was reused, or the `sub` user does not exist on that site. If sign-in works but a later call 403s, the token is missing that call's scope in `scp`.

### Which API version should I put in the path?
Match it to your Tableau version. A newer server accepts older versions, so pin a version your code was tested against (for example `3.21`) rather than tracking the latest.

### Can I act as different users with one Connected App?
Yes. The `sub` claim names the user the session acts as, so one Connected App can mint a token for any user on the site, subject to that user's own permissions. That is what makes it useful for a service that automates on behalf of many people.


---
Try it live: Automate it through the MCP connector — https://ext.tableauops.com/mcp
