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:
expmust 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.jtimust be unique per token. Reuse is treated as a replay and rejected.audis the literal stringtableau.scpmust 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 patterntableau:<resource>:<action>(for exampletableau: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
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 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.