# Tableau

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 2–3 hours
- Last updated: July 2026

## TL;DR

Bubble connects to Tableau via the API Connector using a two-step Personal Access Token (PAT) authentication flow. You first exchange your PAT for a short-lived session token, store it in App State, and then pass it as a dynamic header on every subsequent workbook query or refresh call. An optional iframe embed lets you display Tableau Public views inside Bubble pages without any API calls.

## Why connecting Bubble to Tableau requires a two-step auth flow

Most API integrations use a static API key: paste the key once as a Private header and every call just works. Tableau is different. Its REST API uses a Personal Access Token (PAT) only as a one-time credential to obtain a short-lived session token — similar to how OAuth exchanges a code for an access token. The session token expires in 4 hours, which means Bubble apps left open overnight will silently fail on the next API call unless you add a re-authentication workflow.

The payoff for this extra complexity is a powerful use case: a Bubble admin panel that gives your operations team a live view of Tableau workbooks and the ability to trigger on-demand data refreshes without needing a Tableau login. Combined with an optional iframe embed for display-only stakeholder views, Tableau's analytics power becomes accessible inside the browser-based tool your team already uses. Bubble's API Connector is perfectly suited here because all calls run server-side — your PAT and session token never touch the browser.

## Before you start

- A Tableau Cloud account (or Tableau Server with REST API access enabled) on a plan that includes REST API access
- A Tableau Personal Access Token (PAT) — created in your Tableau Cloud account settings under My Account → Personal Access Tokens
- Your Tableau Cloud pod URL (e.g., https://10az.online.tableau.com) and site name (e.g., 'mycompany') — both visible in the address bar when logged in
- A Bubble app on any plan for read-only workbook listing and embed; a paid Bubble plan if you want to use API Workflows to schedule re-authentication
- The API Connector plugin installed in your Bubble app (free, by Bubble)

## Step-by-step guide

### 1. Create a Tableau Personal Access Token

In Tableau Cloud or Tableau Server, click your avatar in the top-right corner and select My Account Settings. Scroll to the Personal Access Tokens section and click Create new token. Give the token a meaningful name such as 'Bubble Integration' and copy both the token name and the token secret that appear — the secret is shown only once. Store them temporarily in a text file. You will use the token name and secret as the body of your sign-in API call, not as a header. Do not confuse the PAT secret with the session token you will receive in response — the PAT is the long-lived credential you keep safe; the session token is ephemeral and will be stored in Bubble App State.

**Expected result:** You have a PAT name string (e.g., 'BubbleIntegration') and a PAT secret string (a long alphanumeric value). Both are stored safely and will be used in the next step.

### 2. Add the Tableau API Connector group with a shared Accept header

In your Bubble app, open the Plugins tab in the left sidebar and click Add plugins. Search for 'API Connector' (by Bubble), install it, then close the plugin dialog. Open the API Connector plugin settings. Click Add another API. Name it 'Tableau'. In the Shared headers for this API section, add one header: key is 'Accept', value is 'application/json'. Do NOT mark this header Private — it is not a secret. This Accept header is critical because Tableau's REST API defaults to returning XML. Without it, every response will be XML and Bubble will be unable to parse the fields automatically during initialization. Leave the Authentication field set to None — authentication is handled by the X-Tableau-Auth header added dynamically on each call after sign-in.

```
{
  "api_group": "Tableau",
  "shared_headers": {
    "Accept": "application/json"
  },
  "authentication": "None"
}
```

**Expected result:** The 'Tableau' API group appears in the API Connector plugin with a shared Accept: application/json header and no authentication scheme.

### 3. Add the Sign In call and store the session token in App State

Inside the Tableau API group, click Add another call. Name it 'Sign In'. Set Method to POST and the URL to 'https://{your-pod}.online.tableau.com/api/3.19/auth/signin' — replace {your-pod} with your actual Tableau Cloud pod identifier (e.g., '10az'). Under Use as, select Action (not Data) — this call will be triggered from a Bubble workflow, not bound to a data source. In the Body section, select JSON and paste the sign-in payload shown below. Replace the placeholder values with your actual PAT name and secret for the purpose of the Initialize call — you can make them dynamic later. Click Initialize call. Bubble will make a real POST to Tableau and parse the JSON response. The response contains 'credentials.token' (the session token) and 'credentials.site.id' (the site UUID). After initialization, go to your Bubble app's Data tab and create two App State variables: 'tableau_session_token' (text) and 'tableau_site_id' (text). Then build a workflow — for example triggered on Page is Loaded — that runs the Sign In action and saves the returned 'credentials token' value to 'tableau_session_token' state and 'credentials site id' to 'tableau_site_id' state. Note: the site ID UUID from the sign-in response differs from the site name you see in the URL — always use the UUID in subsequent API paths.

```
{
  "method": "POST",
  "url": "https://10az.online.tableau.com/api/3.19/auth/signin",
  "headers": {
    "Content-Type": "application/json",
    "Accept": "application/json"
  },
  "body": {
    "credentials": {
      "personalAccessTokenName": "BubbleIntegration",
      "personalAccessTokenSecret": "<your-pat-secret>",
      "site": {
        "contentUrl": "mycompany"
      }
    }
  }
}
```

**Expected result:** The Initialize call succeeds and Bubble detects fields including 'credentials token' (the session token string) and 'credentials site id' (a UUID). A page-load workflow saves both to App State, making them available to all subsequent calls.

### 4. Add the List Workbooks call with a dynamic session token header

Back in the Tableau API group, click Add another call. Name it 'List Workbooks'. Set Method to GET and URL to 'https://{your-pod}.online.tableau.com/api/3.19/sites/<site_id>/workbooks' — note the '<site_id>' placeholder. Set Use as to Data. In the Parameters section, add a parameter named 'site_id' — mark it as a path parameter, not a query parameter. This will be supplied dynamically from App State when the call is made. Now add a header at the call level (not the group level): key is 'X-Tableau-Auth', value is your hardcoded test session token from the sign-in step above. Mark this header Private (click the checkbox). This allows you to initialize the call with a real session token. After initialization succeeds, edit the X-Tableau-Auth header value to be a dynamic value from App State ('tableau_session_token'). Bubble's API Connector supports dynamic headers on calls by switching from static text to a dynamic data reference. The response will nest workbooks under 'workbooks workbook' — Bubble's field detector will map this as a list. Set 'Type of content' to List to enable use in a Repeating Group.

```
{
  "method": "GET",
  "url": "https://10az.online.tableau.com/api/3.19/sites/<site_id>/workbooks",
  "headers": {
    "Accept": "application/json",
    "X-Tableau-Auth": "<private: dynamic from App State>"
  },
  "parameters": {
    "site_id": "<dynamic: tableau_site_id from App State>"
  }
}
```

**Expected result:** The List Workbooks call initializes successfully. Bubble detects workbook fields including name, id, contentUrl, and updatedAt. A Repeating Group on your page bound to this call displays your Tableau workbooks by name.

### 5. Add the Refresh Workbook call and wire it to a button

Add a third call in the Tableau API group. Name it 'Refresh Workbook'. Set Method to POST and URL to 'https://{your-pod}.online.tableau.com/api/3.19/sites/<site_id>/workbooks/<workbook_id>/refresh'. Add path parameters 'site_id' and 'workbook_id' — both will be supplied dynamically. Add the X-Tableau-Auth header at the call level, marked Private, set dynamically from App State. Set Use as to Action. Leave the body empty — Tableau's refresh endpoint does not require a request body. Click Initialize call using a real site_id and a real workbook_id from your List Workbooks response. The response returns a job object with a job ID — Bubble's workflow will see it as completed. Important: the Tableau workbook refresh is asynchronous. The POST does not wait for the refresh to finish; it returns immediately with a job ID. You cannot poll for completion inside a single Bubble workflow due to Bubble's 30-second workflow timeout. Design your UI to show 'Refresh requested' after the button click rather than waiting for completion. In your Bubble editor, add a button in the Repeating Group of workbooks. In the button's workflow, add step 1: 'Plugins → Tableau - Refresh Workbook', supplying the current row's workbook id and tableau_site_id from App State. Add step 2: 'Element Actions → Set state' to show a 'Refresh requested' text element for that row.

```
{
  "method": "POST",
  "url": "https://10az.online.tableau.com/api/3.19/sites/<site_id>/workbooks/<workbook_id>/refresh",
  "headers": {
    "Accept": "application/json",
    "X-Tableau-Auth": "<private: dynamic from App State>"
  },
  "parameters": {
    "site_id": "<dynamic: tableau_site_id from App State>",
    "workbook_id": "<dynamic: current cell's workbook id>"
  },
  "body": {}
}
```

**Expected result:** Clicking the Refresh button in the workbook Repeating Group triggers the refresh call, returns a 200 response with a job ID, and the UI updates to show 'Refresh requested' for that row. The actual workbook data refresh completes in Tableau in the background.

### 6. Optionally embed a Tableau view in a Bubble HTML element

For display-only use cases — particularly sharing a Tableau Public view or an internally shared Tableau Cloud view with non-technical stakeholders in your Bubble app — you can embed the view without any API calls. In Tableau, open the view you want to share and click Share (top toolbar) → Embed Code. Copy the iframe src URL, which will look like 'https://public.tableau.com/views/{workbook-name}/{view-name}'. In your Bubble app, add an HTML element to the page (from the Visual elements panel in the editor). Set the element's height to a fixed value like 600 or 700 pixels — Bubble's responsive engine does not auto-size iframes. Paste the following HTML, replacing the src with your embed URL. The '?:embed=y&:showVizHome=no' parameters are required to suppress Tableau's navigation bar inside the iframe. Set the element's width to 100% of its container using Bubble's layout settings. Note: private Tableau Cloud reports (not Tableau Public) will show a Google sign-in prompt inside the iframe unless the viewer is already authenticated with the correct Google account in that browser session. This is a Tableau security feature, not a Bubble setting — the only solution is to share the report publicly or use Tableau's Trusted Authentication (advanced, server-level configuration).

```
<iframe
  src="https://public.tableau.com/views/YourWorkbookName/YourViewName?:embed=y&:showVizHome=no"
  width="100%"
  height="600"
  frameborder="0"
  allowfullscreen>
</iframe>
```

**Expected result:** A live Tableau view renders inside your Bubble page within an HTML element. The visualization responds to Tableau's built-in interactivity (filter dropdowns, tooltip hovers) without any additional Bubble workflow setup.

## Best practices

- Always mark the Tableau PAT secret as a Private parameter in your API Connector Sign In call — even though you exchange it immediately for a session token, the PAT itself should never be visible in the browser network tab.
- Store both the session token AND the site ID UUID in App State after sign-in. The site ID is needed in every subsequent API path and is easy to forget to persist.
- Add a re-authentication workflow on page load that runs every time a user navigates to a Tableau-connected Bubble page. This ensures the session token is always fresh and eliminates silent 4-hour expiry failures.
- Limit who can trigger Tableau workbook refreshes using Bubble's Condition system on the Refresh button — show it only to users with an 'Admin' or 'Analyst' role. Frequent refresh calls may trigger Tableau Server throttles and consume Bubble Workload Units unnecessarily.
- Apply Bubble Privacy Rules to any data type that stores Tableau workbook metadata. If your Bubble app is multi-tenant, users in tenant A should not be able to query workbooks belonging to tenant B.
- Use the Logs tab in Bubble (Settings → Logs → Workflow logs) to debug API call failures. The log shows the full request URL, headers (Private values are redacted), and the raw response — invaluable when troubleshooting 404s caused by wrong site IDs.
- For display-only use cases, prefer the Tableau iframe embed over REST API calls. The embed requires no authentication setup for Tableau Public views and consumes zero Bubble Workload Units, keeping your monthly WU cost lower.
- When the Refresh Workbook job is complete in Tableau, re-run the List Workbooks call to update the 'updatedAt' timestamps in your Bubble Repeating Group. Do not rely on polling — trigger the refresh manually after a reasonable delay.

## Use cases

### On-demand workbook refresh panel

Build a Bubble admin page that lists all Tableau workbooks from a specific site and provides a 'Refresh now' button for each. Operations staff can trigger a data refresh directly from Bubble without navigating to Tableau, while the refresh job status ('requested') appears inline. This is especially useful when Tableau workbooks pull from a database that Bubble also writes to — staff can refresh immediately after a bulk data import.

### Executive dashboard with embedded Tableau view

Embed a Tableau Public or internally shared Tableau Cloud view inside a Bubble page using an HTML element iframe. Executives see live BI visualizations alongside editable Repeating Groups of the same underlying data — a combined BI and operations layer in a single Bubble screen, with no separate Tableau login required for the display portion.

### Workbook directory for non-technical stakeholders

Pull the list of all published workbooks on a Tableau site via the API and display them in a searchable, filterable Bubble Repeating Group. Each card shows the workbook name, last-updated timestamp, and a direct link to the Tableau Cloud view. Non-technical stakeholders access workbooks through a familiar Bubble interface instead of navigating Tableau's own project browser.

## Troubleshooting

### Sign In call returns 401 Unauthorized during API Connector initialization

Cause: The PAT name or secret is incorrect, the PAT has been revoked, or the 'contentUrl' site name in the request body does not match your Tableau site identifier exactly.

Solution: Double-check the PAT name (case-sensitive) and secret in your Tableau account settings under My Account → Personal Access Tokens. Confirm the contentUrl matches the site name shown in your Tableau Cloud URL after '/site/' — the Default site uses an empty string "". Regenerate the PAT if necessary.

### List Workbooks call returns 404 after sign-in succeeds

Cause: The site_id in the workbooks endpoint URL is using the site name (URL string) rather than the UUID returned by the sign-in response. Tableau requires the UUID in REST API paths.

Solution: In your page-load workflow, save the 'credentials site id' value (the UUID, e.g., 'a1b2c3d4-...') to App State, not the site name. The UUID appears in the sign-in response under 'credentials → site → id'. Verify the saved value in Bubble's debugger by inspecting App State after the sign-in workflow runs.

### All Tableau API calls start returning 401 after the app has been open for several hours

Cause: Tableau session tokens expire after 4 hours. Once expired, every subsequent call using the stored token fails silently or returns 401.

Solution: Add a page-load workflow that re-runs the Sign In call and refreshes the App State token every time a user loads any page. For apps used over long sessions, add a scheduled client-side event (using Bubble's built-in recurring event or a JavaScript setInterval) that calls the sign-in workflow every 3.5 hours to pre-empt expiry.

### API Connector returns 'There was an issue setting up your call' during the List Workbooks initialization

Cause: The X-Tableau-Auth header is missing or contains an expired/invalid session token during the Initialize call step. Bubble needs a real, successful API response to detect field shapes.

Solution: During setup, temporarily hardcode a fresh session token (from a just-completed sign-in call) directly in the X-Tableau-Auth header value field — do not use a dynamic reference yet. Complete the initialization, then switch the header value to dynamic (App State reference) once Bubble has saved the field structure.

### Tableau embed iframe shows a blank white box or a sign-in prompt inside the Bubble HTML element

Cause: Either the Tableau Cloud report sharing is set to 'Restricted' (not 'Anyone with the link'), or the viewer's browser is blocking cross-origin iframes via Content Security Policy.

Solution: In Tableau, open the report and go to Share → change access to 'Anyone with the link can view'. If the iframe still shows blank in production, open the browser developer console (F12) and check for CSP errors in the Console tab. Test in an incognito window to rule out browser extension interference.

## Frequently asked questions

### Does Bubble expose my Tableau Personal Access Token in the browser?

No. The API Connector sign-in call runs from Bubble's servers. Your PAT is sent as part of the POST body, but because Bubble processes the call server-side, the PAT is never included in a browser network request that a user could inspect. Mark the PAT secret parameter as Private in the API Connector for an additional layer of protection.

### Can I connect Bubble to Tableau Server (on-premises) instead of Tableau Cloud?

Yes. Replace the base URL in every API call with your Tableau Server's hostname (e.g., https://tableau.yourcompany.com). The REST API paths and authentication flow are identical. Ensure your Tableau Server's REST API is enabled by your administrator and that Bubble's servers can reach the on-premises URL — some organizations place Tableau Server behind a VPN that blocks external traffic.

### Can I embed a private Tableau Cloud report (not Tableau Public) without a sign-in prompt?

Only if the report's sharing setting is 'Anyone with the link can view'. Reports shared with specific users or groups will always show a Tableau/Google sign-in prompt inside the iframe for viewers who are not already authenticated. Tableau's Trusted Authentication feature (server-level configuration) can bypass this, but it requires server-side token generation outside of Bubble's API Connector and is an advanced configuration not covered here.

### The workbook refresh POST returns immediately — how do I know when the refresh is done?

Tableau's workbook refresh endpoint returns a job ID in the response, not a completion signal. To check job status you would need to poll GET /api/3.19/sites/{site_id}/jobs/{job_id} repeatedly. Bubble's 30-second workflow timeout makes in-workflow polling impractical. The recommended design is to show 'Refresh requested' to the user and ask them to reload the Tableau workbook view after a few minutes, or use Tableau's built-in scheduled refresh feature for automated updates.

### Do I need a paid Bubble plan to build the Tableau integration?

The API Connector calls (sign-in, list workbooks, refresh workbook) work on all Bubble plans. If you want to schedule re-authentication using a Backend Workflow (API Workflow) or schedule periodic workbook list refreshes, that feature requires a paid Bubble plan. For most Bubble apps a page-load sign-in workflow is sufficient and works on the free plan.

### My List Workbooks call returns XML instead of JSON even though I set Accept: application/json. What is wrong?

The Accept header must be set at the API group level (Shared headers for this API), not just at the call level, because Tableau checks it on every request. Confirm the header name is exactly 'Accept' (no typos) and the value is exactly 'application/json' (no extra spaces). Also add 'Content-Type: application/json' as a shared header. If XML still appears, check whether your Tableau Server version pre-dates JSON response support — Tableau Server versions before 9.3 return XML only.

---

Source: https://www.rapidevelopers.com/bubble-integrations/tableau
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/tableau
