PHPackages                             foundry-co/tableau-api - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. foundry-co/tableau-api

ActiveLibrary[HTTP &amp; Networking](/categories/http)

foundry-co/tableau-api
======================

A framework-agnostic PHP client for the Tableau Server / Tableau Cloud REST API, built on Guzzle.

v0.1(today)03↑2900%MITPHPPHP ^8.4

Since Aug 9Pushed todayCompare

[ Source](https://github.com/foundry-co/php-tableau-client)[ Packagist](https://packagist.org/packages/foundry-co/tableau-api)[ RSS](/packages/foundry-co-tableau-api/feed)WikiDiscussions master Synced today

READMEChangelogDependencies (6)Versions (2)Used By (0)

foundry-co/tableau-api
======================

[](#foundry-cotableau-api)

A modern, framework-agnostic PHP client for the [Tableau Server / Tableau Cloud REST API](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api.htm).

- **Framework agnostic.** usable in any PHP 8.4+ project.
- **Built on Guzzle.** Uses `guzzlehttp/guzzle` directly for HTTP — the only hard dependency.
- **Optional Laravel integration.** Ships a service provider, facade, and publishable config file for Laravel apps (requires `illuminate/support` + `illuminate/contracts` when used).
- **Workflow helpers.** Common multi-call Tableau operations (publish-then-refresh, provision-a-user, clone-permissions, refresh-and-wait) are wrapped in single method calls.

Requirements
------------

[](#requirements)

- PHP 8.4+
- `guzzlehttp/guzzle` ^7.8

Installation
------------

[](#installation)

```
composer require foundry-co/tableau-api
```

Quick start (framework agnostic)
--------------------------------

[](#quick-start-framework-agnostic)

```
use FoundryCo\TableauApi\TableauClient;

$tableau = new TableauClient([
    'server_url' => 'https://tableau.example.com',
    'site_content_url' => 'marketing',   // '' for the Default site
    'token_name' => 'my-personal-access-token-name',
    'token_secret' => 'my-personal-access-token-secret',
]);

$tableau->signIn();

foreach ($tableau->projects()->list() as $project) {
    echo $project['name'], PHP_EOL;
}

$tableau->signOut();
```

A [Connected App](#obtaining-your-tableau-credentials) is also supported, and preferred for server-to-server integrations:

```
$tableau = new TableauClient([
    'server_url' => 'https://tableau.example.com',
    'connected_app_client_id' => 'client-id',
    'connected_app_secret_id' => 'secret-id',
    'connected_app_secret_value' => 'secret-value',
    'connected_app_user' => 'service-account@example.com',
    'connected_app_scopes' => ['tableau:content:read', 'tableau:content:write'],
]);
```

Username/password sign-in is also supported (Tableau is deprecating this for most deployments, but it's still available where enabled):

```
$tableau = new TableauClient([
    'server_url' => 'https://tableau.example.com',
    'username' => 'alice',
    'password' => 'secret',
]);
```

You can also build the client from an explicit `TableauConfig` value object, or inject your own `GuzzleHttp\ClientInterface` (useful for testing with a mock handler, or to add your own middleware/logging):

```
use FoundryCo\TableauApi\Config\TableauConfig;
use FoundryCo\TableauApi\TableauClient;
use GuzzleHttp\Client as GuzzleClient;

$config = new TableauConfig(
    serverUrl: 'https://tableau.example.com',
    apiVersion: '3.24',
    siteContentUrl: 'marketing',
    tokenName: 'name',
    tokenSecret: 'secret',
);

$tableau = new TableauClient($config, new GuzzleClient());
```

If you don't pass a Guzzle client, `TableauClient` builds one for you from the `TableauConfig` (base URL, timeout, SSL verification, user agent).

Obtaining your Tableau credentials
----------------------------------

[](#obtaining-your-tableau-credentials)

**Server URL** — the base address of your Tableau pod, with no trailing path. For Tableau Cloud, this is `https://.online.tableau.com` — the `` is visible in your browser's address bar after logging in (e.g. `10ay`, `us-east-1`, `prod-uk-a`). For Tableau Server, it's whatever internal hostname your admin gave you, e.g. `https://tableau.internal.example.com`.

**Site** — the `contentUrl` segment identifying your site, visible in the browser URL as `.../#/site//...`. If you're on the server's Default site, this is an empty string.

**Connected App (Direct Trust)** — the recommended option for a server-to-server integration like this one, since it doesn't tie authentication to one person's account and doesn't expire from inactivity the way a PAT does:

1. As a site or server admin, go to **Settings → Connected Apps** in Tableau.
2. Click **New Connected App** → choose **Direct Trust**, give it a name, and save. Tableau generates a **Client ID** — copy it into `TABLEAU_CONNECTED_APP_CLIENT_ID`.
3. On the same Connected App, click **Generate new secret**. Tableau shows a **Secret ID** and **Secret Value** exactly once — copy them into `TABLEAU_CONNECTED_APP_SECRET_ID` and `TABLEAU_CONNECTED_APP_SECRET_VALUE`.
4. Connected Apps authenticate *as* a specific user (there's no separate "service account" identity) — set `TABLEAU_CONNECTED_APP_USER` to the value described below, for the Tableau user whose permissions the integration should run under. Skip this if you'll supply the user dynamically at runtime instead (see [Impersonating a dynamic user](#impersonating-a-dynamic-user) below).
5. Under the Connected App's **Access** settings, restrict which domains/projects it can reach if desired, and confirm it's **Enabled**.
6. The client builds and signs the required JWT itself via [`firebase/php-jwt`](https://github.com/firebase/php-jwt) (HS256, `kid` = Secret ID, `sub` = the user).

**What value goes in `connected_app_user` / the `sub` claim** — it must be the Tableau **username** exactly as Tableau has it stored for that user, *not* their display name:

- **Tableau Cloud**: this is the user's email address.
- **Tableau Server**: it's the login name from your identity provider — for local auth that's whatever username was set when the account was created; for AD/SAML it's typically `DOMAIN\username` or the SAML NameID, depending on how the server's authentication is configured.
- To check the exact value for a given user: as an admin, go to **Users** on the site, and look at the **Name** column (not "Full Name") in the users table — that's the string Tableau expects.
- A mismatch here doesn't error on JWT signing — it fails at sign-in with an authentication error, since Tableau simply can't find a matching user.

**Personal Access Token** (simpler alternative, tied to one user's account):

1. Sign in to Tableau in your browser.
2. Open your account menu (top right) → **My Account Settings**.
3. Scroll to **Personal Access Tokens** and enter a name for the new token, then click **Create new token**.
4. Tableau shows the token's secret exactly once — copy it immediately into `TABLEAU_PAT_SECRET`; the token's *name* (what you typed in step 3) goes in `TABLEAU_PAT_NAME`.
5. PATs expire after 15 consecutive days of inactivity (Tableau Cloud) or per your server's configured policy — if requests start failing with an authentication error, generate a new one.

If neither is available, fall back to `username`/`password` — but note Tableau is phasing this out for Tableau Cloud and it requires the site to allow it.

If more than one of these is configured at once, the client prefers Connected App over PAT over username/password.

**API version** — Tableau's REST API is versioned independently from the product version. Check the [REST API and Resources Versions](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_versions.htm) page for the version matching your server, or query `GET https:///api/metadata/graphql`'s sibling `GET https:///api/-/serverInfo` (no auth required) which returns the server's supported `restApiVersion` directly. `3.24` (the default here) works against any reasonably current Tableau Server/Cloud.

Impersonating a dynamic user
----------------------------

[](#impersonating-a-dynamic-user)

With a Connected App, the server-level credentials (client id, secret id, secret value) are fixed and belong in config/env — but *who* you're impersonating often isn't known until runtime, e.g. it should be whichever user is logged into your own application. Configure everything except `connected_app_user`/`TABLEAU_CONNECTED_APP_USER`, then call `signInAs()` with the user per-request:

```
$tableau = new TableauClient([
    'server_url' => 'https://tableau.example.com',
    'connected_app_client_id' => 'client-id',
    'connected_app_secret_id' => 'secret-id',
    'connected_app_secret_value' => 'secret-value',
    // no connected_app_user — supplied dynamically below
]);

$tableau->signInAs($currentUser->tableau_username);

foreach ($tableau->workbooks()->list() as $workbook) { ... }
```

`signInAs()` always re-authenticates (it doesn't reuse a previous session), since a different call may need to impersonate a different user than the last one. Pass `scopes` as a second argument to override `connected_app_scopes` per call if needed.

Session memoization
-------------------

[](#session-memoization)

`signIn()` is memoized: once authenticated, calling it again is a no-op as long as the session hasn't expired, so it's safe to call at the start of every method/request without worrying about extra round trips.

```
$tableau->signIn(); // hits the network, authenticates
$tableau->signIn(); // no-op — session still valid
$tableau->signIn(force: true); // always re-authenticates
```

Expiry is tracked from Tableau's `estimatedTimeToExpiration` (falling back to Tableau's default 240-minute session timeout), with a 60-second safety buffer. This memoization is in-memory only and scoped to a single `TableauClient` instance — in a non-persistent runtime (e.g. a typical PHP-FPM/Laravel request), a fresh process still re-authenticates once per request.

Laravel integration (optional)
------------------------------

[](#laravel-integration-optional)

The package auto-discovers `FoundryCo\TableauApi\Laravel\TableauServiceProvider`. Publish the config file:

```
php artisan vendor:publish --tag=tableau-config
```

Set the relevant `TABLEAU_*` environment variables (see `config/tableau.php`):

```
TABLEAU_SERVER_URL=https://tableau.example.com
TABLEAU_SITE=marketing

# Connected App (preferred) — or use TABLEAU_PAT_NAME/TABLEAU_PAT_SECRET instead
TABLEAU_CONNECTED_APP_CLIENT_ID=client-id
TABLEAU_CONNECTED_APP_SECRET_ID=secret-id
TABLEAU_CONNECTED_APP_SECRET_VALUE=secret-value
TABLEAU_CONNECTED_APP_USER=service-account@example.com

```

Then resolve the client via the container, dependency injection, or the `Tableau` facade:

```
use FoundryCo\TableauApi\Laravel\Tableau;
use FoundryCo\TableauApi\TableauClient;

// Facade
Tableau::signIn();
$workbooks = Tableau::workbooks()->list();

// Container / constructor injection
class ReportSyncController
{
    public function __construct(private TableauClient $tableau) {}
}
```

The bound `TableauClient` is a singleton per application lifecycle, so calling `signIn()` repeatedly (e.g. at the top of every request) is cheap — see [Session memoization](#session-memoization) below.

Resources
---------

[](#resources)

Each resource class is accessed from `TableauClient` and mirrors a section of the REST API reference:

MethodResourceDocs`sites()``Resources\Sites`[Sites](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_sites.htm)`projects()``Resources\Projects`[Projects](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_projects.htm)`users()``Resources\Users`[Users and Groups](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_users_and_groups.htm)`groups()``Resources\Groups`[Users and Groups](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_users_and_groups.htm)`workbooks()``Resources\Workbooks`[Workbooks and Views](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm)`datasources()``Resources\Datasources`[Data Sources](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_data_sources.htm)`views()``Resources\Views`[Workbooks and Views](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm)`jobs()``Resources\Jobs`[Jobs, Tasks, and Schedules](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm)`schedules()``Resources\Schedules`[Jobs, Tasks, and Schedules](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_jobs_tasks_and_schedules.htm)`permissions()``Resources\Permissions`[Permissions](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_permissions.htm)`flows()``Resources\Flows`[Flows](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_flows.htm)`subscriptions()``Resources\Subscriptions`[Subscriptions](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_subscriptions.htm)`favorites()``Resources\Favorites`[Favorites](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_favorites.htm)List endpoints return PHP `Generator`s that transparently walk every page:

```
foreach ($tableau->workbooks()->list(filterExpression: 'name:eq:Sales Overview') as $workbook) {
    // one HTTP request per page of 100, fetched lazily as you iterate
}
```

Every resource method returns either the decoded array Tableau sent back, or a `FoundryCo\TableauApi\Http\Response` (a small read-only DTO with `json()`, `body()`, `status()`, `header()`, `successful()`/`failed()`) when you might want status codes/headers — check each method's return type.

Workflow helpers
----------------

[](#workflow-helpers)

Several Tableau operations require multiple, ordered REST calls. The client wraps the common ones so you don't have to hand-roll them:

### Publish a workbook/datasource and wait for its extract refresh

[](#publish-a-workbookdatasource-and-wait-for-its-extract-refresh)

```
$result = $tableau->publishAndRefresh()->workbook(
    filename: 'sales.twbx',
    contents: file_get_contents('sales.twbx'),
    projectId: $projectId,
    overwrite: true,
);

// $result['workbook'] — the published workbook payload
// $result['job']      — the finished refresh job payload
```

### Refresh and wait for completion

[](#refresh-and-wait-for-completion)

```
$job = $tableau->refreshAndAwait()->workbook($workbookId, pollSeconds: 5, timeoutSeconds: 900);
// or ->datasource($datasourceId) / ->flow($flowId) / ->await($existingJobId)
```

### Provision a user (create + add to groups + grant project permissions)

[](#provision-a-user-create--add-to-groups--grant-project-permissions)

```
$user = $tableau->provisionUser()->run(
    name: 'alice@example.com',
    siteRole: 'Explorer',
    groupIds: [$analystsGroupId],
    projectPermissions: [
        $marketingProjectId => ['Read', 'Filter', 'ViewComments'],
    ],
);
```

### Clone permissions from one content item to another

[](#clone-permissions-from-one-content-item-to-another)

```
$tableau->clonePermissions()->run(
    contentType: 'projects',
    sourceId: $templateProjectId,
    targetId: $newProjectId,
);
```

Error handling
--------------

[](#error-handling)

Failed API calls throw `FoundryCo\TableauApi\Exceptions\TableauApiException`, which exposes the Tableau error code/detail and the underlying HTTP response:

```
use FoundryCo\TableauApi\Exceptions\TableauApiException;

try {
    $tableau->projects()->create('Marketing');
} catch (TableauApiException $e) {
    report($e->tableauErrorCode);  // e.g. "429006"
    report($e->tableauDetail);
    report($e->httpStatus);
}
```

Configuration/usage errors (e.g. missing credentials) throw `FoundryCo\TableauApi\Exceptions\TableauException`.

Testing your own code against this client
-----------------------------------------

[](#testing-your-own-code-against-this-client)

Because the client is built directly on Guzzle, you can inject a client using Guzzle's `MockHandler` in tests:

```
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response as PsrResponse;

$mock = new MockHandler([
    new PsrResponse(200, [], json_encode(['credentials' => [
        'token' => 'fake-token',
        'site' => ['id' => 'site-id', 'contentUrl' => ''],
        'user' => ['id' => 'user-id'],
    ]])),
    new PsrResponse(200, [], json_encode(['projects' => ['project' => []], 'pagination' => ['totalAvailable' => 0]])),
]);

$http = new GuzzleClient(['handler' => HandlerStack::create($mock)]);

$tableau = new TableauClient(['server_url' => 'https://tableau.example.com'], $http);
$tableau->signIn();
```

License
-------

[](#license)

MIT

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/107836?v=4)[Josh Butts](/maintainers/jimbojsb)[@jimbojsb](https://github.com/jimbojsb)

---

Top Contributors

[![jimbojsb](https://avatars.githubusercontent.com/u/107836?v=4)](https://github.com/jimbojsb "jimbojsb (4 commits)")

---

Tags

laravelsdkGuzzleREST APItableau

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/foundry-co-tableau-api/health.svg)

```
[![Health](https://phpackages.com/badges/foundry-co-tableau-api/health.svg)](https://phpackages.com/packages/foundry-co-tableau-api)
```

###  Alternatives

[laravel/socialite

Laravel wrapper around OAuth 1 &amp; OAuth 2 libraries.

5.7k113.1M983](/packages/laravel-socialite)[xeroapi/xero-php-oauth2

Xero official PHP SDK for oAuth2 generated with OpenAPI spec 3

1074.9M20](/packages/xeroapi-xero-php-oauth2)[ellaisys/aws-cognito

Laravel Authentication using AWS Cognito (Web and API)

121269.8k1](/packages/ellaisys-aws-cognito)[civicrm/civicrm-core

Open source constituent relationship management for non-profits, NGOs and advocacy organizations.

762297.9k51](/packages/civicrm-civicrm-core)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

353.6k](/packages/eslazarev-wildberries-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
