Pulling a spreadsheet or API export into a report used to mean pasting secrets into a one-off script, or asking an admin to put keys in an environment nobody else could see. When that key rotated, every copy had to change.
Integrations is an organization credential vault for those secrets. Owners create named entries under Admin → Integrations. Editors bind a credential to a data source when an integration script needs it. Secrets are encrypted at rest and never shown again after save.
#Key Concepts
Integration — A named vault entry (for example “Warehouse Sheets”) that holds one credential. The name is what editors see when binding a data source; the secret itself is write-only after create.
Credential type — A Cursor API key (Cursor Admin and Analytics APIs), or Google Service Account JSON (full service-account key file for Drive and similar Google APIs).
Bound data source — A data source that points at an Integration. While any data source is still bound, Threaded blocks deleting that Integration (HTTP 409) so scripts do not lose auth mid-flight. Unbind first, then delete.
#Who can use it
- Owners create, rotate (update), and delete Integrations. The Admin → Integrations page is owner-only.
- Editors can list Integrations by name (no secrets) and bind or unbind them on a data source’s detail page.
- The Integrations feature must be enabled for your organization (standalone Integrations access). Contact Threaded if you need it turned on.
#How It Works
#Opening Integrations
Go to Admin → Integrations. You will see each Integration’s name, description, type, how many data sources are bound, who created it, and when.
#Creating an Integration
- Click Create integration.
- Enter a Name and optional Description.
- Choose the Type (Cursor API key or Google Service Account JSON).
- Paste the Credential plaintext. Threaded encrypts it on save and does not display it again.
- Click Create.
#Rotating a credential
Open Edit on the row, paste the new secret, and save. The same Integration UUID stays bound to any data sources, so you do not need to rebind them.
#Binding to a data source
On Reporting → Data sources, open a non-managed data source. In the Integration credential card at the bottom of the page, choose an Integration from the dropdown (or None to unbind). Editors can do this; the picker only appears when Integrations is enabled for the org.
When an integration script runs, Threaded decrypts the bound credential and passes it to the isolate as the INTEGRATION_CREDENTIAL environment variable. See Manufacturing Report Data Sources for scripts, Run integration, and schedules.
#Fetching a Google Sheet with a service account
Hosted JavaScript can sign a Google service-account JWT with crypto.subtle and call the external API with fetch. Use require('threaded/helpers') for base64url, httpJson, and die. A script may only reach the HTTPS origins Threaded staff have listed for that credential type, so ask support if an integration needs a new origin. This example downloads a Sheet as CSV to /tmp/sheet.csv:
const fs = require('fs')
const { base64url, die, httpJson } = require('threaded/helpers')
const serviceAccount = JSON.parse(process.env.INTEGRATION_CREDENTIAL)
const keyBytes = Buffer.from(serviceAccount.private_key.replace(/-----[^-]+-----/g, '').replace(/\s/g, ''), 'base64')
const issuedAt = Math.floor(Date.now() / 1000)
const unsignedJwt =
base64url({ alg: 'RS256', typ: 'JWT' }) +
'.' +
base64url({
iss: serviceAccount.client_email,
scope: 'https://www.googleapis.com/auth/drive.readonly',
aud: serviceAccount.token_uri,
iat: issuedAt,
exp: issuedAt + 3600
})
crypto.subtle
.importKey('pkcs8', keyBytes, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign'])
.then(key => crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, Buffer.from(unsignedJwt)))
.then(signature => unsignedJwt + '.' + base64url(Buffer.from(signature)))
.then(assertion =>
httpJson(serviceAccount.token_uri, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body:
'grant_type=' +
encodeURIComponent('urn:ietf:params:oauth:grant-type:jwt-bearer') +
'&assertion=' +
encodeURIComponent(assertion)
})
)
.then(token =>
fetch('https://www.googleapis.com/drive/v3/files/YOUR_SHEET_ID/export?mimeType=text%2Fcsv', {
headers: { authorization: 'Bearer ' + token.access_token }
})
)
.then(response => response.text())
.then(csv => fs.writeFileSync('/tmp/sheet.csv', csv))
.catch(error => die(error.message))
Replace YOUR_SHEET_ID and share the Sheet with the service account’s client_email. For large grids, keep the CSV path shown here instead of building a SheetJS worksheet with aoa_to_sheet. A shell integration can then parse the CSV, load it with hosted sqlite3, and upload the .sqlite result.
#Calling the Cursor Admin API
Create an Integration with type Cursor API key and paste the key from the Cursor dashboard (API Keys). Bind it to the data source. Cursor expects HTTP Basic auth with the key as the username and an empty password. Admin and Analytics both use https://api.cursor.com:
const fs = require('fs')
const { die, httpJson } = require('threaded/helpers')
const apiKey = process.env.INTEGRATION_CREDENTIAL
const authorization = 'Basic ' + Buffer.from(apiKey + ':').toString('base64')
httpJson('https://api.cursor.com/teams/members', {
headers: { authorization }
})
.then(body => fs.writeFileSync('/tmp/cursor-members.json', JSON.stringify(body)))
.catch(error => die(error.message))
#Deleting an Integration
Use Delete on the row and confirm. If any data sources are still bound, Threaded keeps the Integration and shows an error asking you to unbind them first — follow the Data sources link in that message, clear the Integration picker on each bound source, then delete again.
#Why This Matters
Credentials stay in one vault instead of in scripts, tickets, or personal password managers. Binding is explicit per data source, rotation does not break bindings, and delete-while-bound is blocked so a refresh job cannot suddenly lose auth.