Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rest-api/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
4. [Trusted auth server typescript](./trusted-auth-server-typescript/)
5. [Trusted auth server python](./trusted-auth-server-python/)
6. [Org inactive users cleanup](./ts-org-inactive-users-cleanup/)
7. [Liveboard schedule webhook](./liveboard-schedule-webhook/)

25 changes: 25 additions & 0 deletions rest-api/liveboard-schedule-webhook/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Receiver (npm start)
PORT=3000
# Bearer token ThoughtSpot sends to the receiver (the webhook's authentication)
RECEIVER_TOKEN=
# Google Drive folder ID in a shared drive; leave empty to write files to ./out
DRIVE_FOLDER_ID=
# Service-account key file for Drive
GOOGLE_APPLICATION_CREDENTIALS=
# Storage mode reads S3 with the AWS default credential chain (needs s3:GetObject)

# ThoughtSpot setup (npm run setup)
TS_HOST=https://your-instance.thoughtspot.cloud
TS_TOKEN=
WEBHOOK_URL=https://receiver.example.com/webhooks/thoughtspot
# Optional S3 storage destination
S3_BUCKET=
S3_REGION=
S3_ROLE_ARN=
# AWS-hosted clusters only
S3_EXTERNAL_ID=
S3_PATH_PREFIX=
# route-schedules: set for one Org, leave empty for the whole cluster
ORG_IDENTIFIER=
# validate: the id printed by create-webhook
WEBHOOK_ID=
3 changes: 3 additions & 0 deletions rest-api/liveboard-schedule-webhook/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
out
.env
4 changes: 4 additions & 0 deletions rest-api/liveboard-schedule-webhook/.stackblitzrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"installDependencies": true,
"startCommand": "npm run dev"
}
167 changes: 167 additions & 0 deletions rest-api/liveboard-schedule-webhook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<!-- search-meta
tags: [webhooks, Liveboard-schedule, REST-API, AWS-S3, Google-Drive, TypeScript, NodeJS, Express, rest-api-sdk]
apis: [createWebhookConfiguration, getWebhookStorageConfig, configureCommunicationChannelPreferences, validateCommunicationChannel, LIVEBOARD_SCHEDULE, REST-API-v2]
questions:
- How do I receive a scheduled ThoughtSpot Liveboard through a webhook?
- How do I handle ThoughtSpot webhook deliveries with PDF/CSV/XLSX attachments in Node.js?
- How do I deliver Liveboard schedule files to an AWS S3 bucket with ThoughtSpot webhooks?
- How do I upload ThoughtSpot scheduled Liveboard exports to Google Drive?
- How do I route Liveboard schedules to the webhook communication channel?
-->

# Liveboard schedule webhook (Typescript)

A webhook receiver, built with TypeScript and Express, for ThoughtSpot **Liveboard schedule** events. When a scheduled Liveboard runs, ThoughtSpot exports it (PDF, CSV or XLSX) and delivers it to this receiver, which uploads the files to a Google Drive folder.

ThoughtSpot delivers in one of two ways, depending on whether the webhook has a storage destination:

| | Direct (no storage destination) | Storage (S3 destination) |
|---|---|---|
| Files | Inside the request | Written to your S3 bucket by ThoughtSpot |
| Request | `multipart/form-data`: a `payload` part with the event JSON, one `file` part per attachment | The event JSON plus a `files[]` list with each file's `objectKey` |
| Receiver needs | Nothing extra | Its own `s3:GetObject` access to the bucket |

```mermaid
sequenceDiagram
participant TS as ThoughtSpot
participant S3 as Your S3 bucket
participant Rx as This receiver
participant GD as Google Drive
alt Direct
TS->>Rx: POST multipart (payload + files)
else Storage
TS->>S3: PutObject (assumes your IAM role)
TS->>Rx: POST event JSON + files[]
end
Rx-->>TS: 200 within 5 seconds
opt Storage
Rx->>S3: GetObject(objectKey)
end
Rx->>GD: Upload files
```

## Key Usage

Simplified from [`src/main.ts`](src/main.ts):

```typescript
app.post('/webhooks/thoughtspot', checkBearerToken, express.json(), async (req, res) => {
// Direct: event from the "payload" part, attachments from the "file" parts.
// Storage: event is the JSON body; files[] says where each file is in S3.
const { event, files } = await parseDelivery(req);

// ThoughtSpot retries deliveries that fail or take longer than 5 seconds,
// so skip repeats (data.msgUniqueId) and acknowledge before uploading.
const key = event.data.msgUniqueId ?? event.eventId;
if (seen.has(key)) return reply(res, 200, 'Duplicate delivery; already received');
seen.add(key);
reply(res, 200, 'Webhook received successfully');

queue = queue.then(() => processDelivery(event, files)); // fetch from S3, upload to Drive
});
```

File structure:

```
liveboard-schedule-webhook
├── src
│ ├── main.ts # The receiver: parse, acknowledge, fetch from S3, upload to Drive
│ ├── setup.ts # ThoughtSpot-side setup with @thoughtspot/rest-api-sdk
│ └── demo.ts # npm run dev / npm test: sends sample deliveries to the receiver
├── fixtures # Sample deliveries from the payload documentation
├── .env.example
├── .stackblitzrc
└── package.json
```

## Demo

Open in [StackBlitz](https://stackblitz.com/github/thoughtspot/developer-examples/tree/main/rest-api/liveboard-schedule-webhook)

`npm run dev` needs no ThoughtSpot instance or cloud credentials. It sends the receiver three deliveries: a direct one, a storage one (one file stored, one failed), and a retry of the first. The files are written to `out/` instead of Drive.

## Documentation

- [Webhooks overview](https://developers.thoughtspot.com/docs/webhooks-overview)
- [Webhook for Liveboard schedule events](https://developers.thoughtspot.com/docs/webhooks-lb-schedule)
- [Webhook communication channel](https://developers.thoughtspot.com/docs/webhooks-comm-channel)
- [Webhook payload](https://developers.thoughtspot.com/docs/webhooks-lb-payload)
- [AWS S3 storage for webhooks](https://developers.thoughtspot.com/docs/webhooks-s3-integration)
- [REST API playground: create webhook](https://try-everywhere.thoughtspot.cloud/v2/#/everywhere/api/rest/playgroundV2_0?apiResourceId=http%2Fapi-endpoints%2Fwebhooks%2Fcreate-webhook-configuration)

## Run locally

- Clone the repository

```bash
git clone https://github.com/thoughtspot/developer-examples.git
cd developer-examples/rest-api/liveboard-schedule-webhook
```

- Install dependencies (Node 22 or later) and try the demo

```bash
npm install
npm run dev
```

- Copy `.env.example` to `.env` and set the variables:

| Variable | Description |
| --- | --- |
| `RECEIVER_TOKEN` | Bearer token ThoughtSpot sends to the receiver; the receiver rejects other requests |
| `DRIVE_FOLDER_ID` | Drive folder to upload to. It must be in a shared drive, with the service account as a member. Leave empty to write to `out/` |
| `GOOGLE_APPLICATION_CREDENTIALS` | Service-account key file for Drive |
| `TS_HOST`, `TS_TOKEN` | Your ThoughtSpot instance, and a bearer token for the setup calls |
| `WEBHOOK_URL` | Where ThoughtSpot can reach the receiver (HTTPS) |
| `S3_BUCKET`, `S3_REGION`, `S3_ROLE_ARN`, `S3_EXTERNAL_ID`, `S3_PATH_PREFIX` | Optional S3 storage destination |

- Start the receiver and make it reachable from your ThoughtSpot instance, for example with [ngrok](https://ngrok.com/) or by deploying it. For storage mode, it also needs AWS credentials with `s3:GetObject` on the bucket.

```bash
npm start
```

- Set up ThoughtSpot, in this order:

| Step | Command | API |
| --- | --- | --- |
| 1. S3 only: get ThoughtSpot's AWS account or GCP service account and a trust-policy template | `npm run setup -- storage-config` | `GET /api/rest/2.0/webhooks/storage-config` |
| 2. S3 only: create the bucket and an IAM role with that trust policy, allowed `s3:PutObject` and `s3:PutObjectAcl` | AWS console | [S3 storage docs](https://developers.thoughtspot.com/docs/webhooks-s3-integration) |
| 3. Create the webhook | `npm run setup -- create-webhook` | `POST /api/rest/2.0/webhooks/create` |
| 4. Send Liveboard schedules to it. Until then, they keep going to email | `npm run setup -- route-schedules` | `POST /api/rest/2.0/system/preferences/communication-channels/configure` |
| 5. Send a test delivery (set `WEBHOOK_ID` to the id from step 3) | `npm run setup -- validate` | `POST /api/rest/2.0/system/communication-channels/validate` |
| 6. Schedule a Liveboard | Liveboard **Schedule** menu | |

For step 2, how the trust policy works depends on where your cluster is hosted:
- **AWS-hosted clusters:** it uses an external ID (case-sensitive), passed as `S3_EXTERNAL_ID`.
- **GCP-hosted clusters:** register `accounts.google.com` as an identity provider, and condition the trust on `accounts.google.com:sub` and `:aud`.

### Before you start

- Webhooks are Beta. ThoughtSpot Support enables them, together with the COMS mail agent and template-variable service they need. Without those, schedules fail with `WEBHOOK_PREREQUISITE_NOT_MET`.
- Only Liveboard schedule events use the webhook channel today, and only one Liveboard schedule webhook is allowed per Org.
- Minimum versions:
- 10.14.0.cl: webhook APIs;
- 26.3.0.cl: S3 storage;
- 26.4.0.cl: `validate`;
- 26.7.0.cl: `storage-config`.
- Privileges: `ADMINISTRATION` or `DEVELOPER` (with RBAC, `CAN_MANAGE_WEBHOOKS` also works). Cluster-wide channel preferences need `ADMINISTRATION`.

### Not covered

These can be added on top of `src/main.ts`:
- GCS storage destinations;
- signature verification: the docs don't specify what is signed or how;
- a durable queue and shared dedupe store, needed for more than one receiver instance.

## Technology labels

- Typescript
- NodeJS
- Express
- REST API SDK
- AWS S3
- Google Drive
- Webhook
Binary file not shown.
49 changes: 49 additions & 0 deletions rest-api/liveboard-schedule-webhook/fixtures/event-direct.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"eventId": "n.18bd8bd5-dee3-4d5c-918e-aec3ba1a090f",
"timestamp": "2025-08-29T09:25:32Z",
"eventType": "LIVEBOARD_SCHEDULE",
"schemaVersion": "1.0",
"source": {
"applicationName": "ThoughtSpot",
"applicationUrl": "https://my.thoughtspot.cloud",
"instanceId": "3d85f2fe-8489-11f0-bdf8-5ba90",
"orgId": "2100019165"
},
"actor": { "actorType": "SYSTEM" },
"metadataObject": {
"objectType": "LIVEBOARD",
"id": "c30a0d49-5747-475d-8985-e975b1c2cf6d",
"name": "Sample Liveboard (View: sample view name)",
"url": "https://my.thoughtspot.cloud/#/pinboard/c30a0d49-5747-475d-8985-e975b1c2cf6d?view=a8118b21-4581-4315-8833-39b2aa5be542"
},
"data": {
"scheduleDetails": {
"scheduleId": "cffddca8-d4fc-4575-b8ec-a50e696ccdfc",
"name": "Sample Liveboard",
"creationTime": "2025-08-29T07:52:23Z",
"description": "Daily sales performance report",
"authorId": "59481331-ee53-42be-a548-bd87be6ddd4a",
"viewInfo": { "viewId": "a8118b21-4581-4315-8833-39b2aa5be542", "viewName": "sample view name" },
"userIds": ["59481331-ee53-42be-a548-bd87be6ddd4a"],
"groupIds": [],
"runId": "29001ffd-6a84-45cd-a957-621fce89afc6",
"exportRequest": {
"object_type": "LIVEBOARD",
"pdf_params": { "orientation": "LANDSCAPE", "page_size": "A4" },
"request_type": "SCHEDULE"
},
"fileFormat": "pdf",
"status": "SCHEDULED",
"emailIds": []
},
"recipients": [
{ "type": "USER", "id": "user-123", "name": "John Doe", "email": "john@company.com", "locale": "en_US" }
],
"viewInfo": { "viewId": "a8118b21-4581-4315-8833-39b2aa5be542", "viewName": "sample view name" },
"aiHighlights": "Sales increased by 15% compared to last quarter",
"msgUniqueId": "2f31df6a-2623-4953-a9bb-5af9b2922474",
"channelID": "6dfa4d82-fdc8-4d5b-8294-a5de0dd5ede1",
"channelType": "webhook",
"communicationType": "LiveboardSchedules"
}
}
43 changes: 43 additions & 0 deletions rest-api/liveboard-schedule-webhook/fixtures/event-storage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"eventId": "n.7c1e2a90-4b3d-4f5e-9a8b-1c2d3e4f5a6b",
"timestamp": "2026-02-26T09:25:32Z",
"eventType": "LIVEBOARD_SCHEDULE",
"schemaVersion": "1.0",
"source": {
"applicationName": "ThoughtSpot",
"applicationUrl": "https://your-cluster.thoughtspot.cloud",
"instanceId": "your-instance-id",
"orgId": "0"
},
"actor": { "actorType": "SYSTEM" },
"metadataObject": {
"objectType": "LIVEBOARD",
"id": "c30a0d49-5747-475d-8985-e975b1c2cf6d",
"name": "Sales Dashboard",
"url": "https://your-cluster.thoughtspot.cloud/#/pinboard/c30a0d49-5747-475d-8985-e975b1c2cf6d"
},
"data": {
"scheduleDetails": {
"scheduleId": "cffddca8-d4fc-4575-b8ec-a50e696ccdfc",
"name": "Daily Sales Report",
"creationTime": "2026-02-26T07:52:23Z",
"description": "Daily report",
"authorId": "59481331-ee53-42be-a548-bd87be6ddd4a",
"userIds": ["59481331-ee53-42be-a548-bd87be6ddd4a"],
"groupIds": [],
"runId": "29001ffd-6a84-45cd-a957-621fce89afc6",
"exportRequest": { "object_type": "LIVEBOARD", "request_type": "SCHEDULE" },
"fileFormat": "pdf",
"status": "SCHEDULED",
"emailIds": []
},
"recipients": [
{ "type": "USER", "id": "59481331-ee53-42be-a548-bd87be6ddd4a", "name": "John Doe", "email": "john@company.com", "locale": "en_US" }
],
"aiHighlights": "",
"msgUniqueId": "8a4f0c6e-1d2b-4e3f-a5b6-c7d8e9f0a1b2",
"channelID": "6dfa4d82-fdc8-4d5b-8294-a5de0dd5ede1",
"channelType": "webhook",
"communicationType": "LiveboardSchedules"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
region,revenue
North,120000
South,98000
Binary file not shown.
22 changes: 22 additions & 0 deletions rest-api/liveboard-schedule-webhook/fixtures/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"files": [
{
"filename": "sales_report.pdf",
"contentType": "application/pdf",
"size": 1024000,
"provider": "AWS_S3",
"bucketName": "my-webhook-files",
"region": "us-west-2",
"objectKey": "thoughtspot-webhooks/cluster-abc/user-123/org-0/liveboard/sales_report.pdf",
"uploadStatus": "SUCCESS"
},
{
"filename": "sales_data.csv",
"contentType": "text/csv",
"size": 512000,
"provider": "AWS_S3",
"uploadStatus": "FAILED",
"errorMessage": "Failed to upload to S3: AccessDenied: Access Denied"
}
]
}
Loading