# Permute Developer Documentation — Complete Context > Full machine-readable context for building trusted integrations with the Permute SDK, MCP server, and REST API. Permute is an enterprise financial intelligence layer. It connects operational, customer, financial, and risk data into a governed model that powers reporting, analytics, AI, and programmatic products. Its public interfaces expose a curated, permission-aware subset of workspace capabilities. REST and SDK data access is scoped to the caller's API-key grants. Hosted MCP access is scoped to the intersection of a published connector's selected sources and the connecting user's current workspace permissions. ## Interface Selection - Prefer the `@permute/sdk` TypeScript SDK for the happy path of a programmatic integration. It provides typed requests and responses and handles multi-step workflows such as uploads and answer-job polling. - Prefer the Permute MCP server for end-user convenience when a person wants to explore or ask questions of trusted workspace data from an MCP-compatible AI client. - Use the REST API as the raw HTTP interface for custom clients, unsupported runtimes, or low-level transport control. - The SDK, MCP server, and REST API share the same curated contract and underlying permission system. REST and SDK requests use API keys. Hosted MCP connectors use OAuth and the connecting user's permissions. - Workspace and dataset mutations are SDK or REST operations unless a capability is explicitly documented as an MCP tool. ## Agent Instructions - Treat workspace, source, dataset, and job IDs as opaque identifiers returned by Permute. Never invent IDs. - Discover sources before answering questions or writing SQL. Inspect a source before selecting exact table or field names. - Prefer answer jobs for interpreted business questions, evidence search for ranked supporting material, and structured queries only for exact read-only rows. - Preserve citations, confidence, and source attribution when presenting Permute results. - Never expose API keys in browser code, generated output, logs, or conversation text. ## REST and SDK Authentication Authenticate with `Authorization: Bearer $PERMUTE_API_KEY`. Workspace-scoped keys infer their workspace. Organization-scoped keys must send `X-Workspace-Id: $PERMUTE_WORKSPACE_ID` for workspace data operations. A valid key never bypasses resource grants. ```http Authorization: Bearer $PERMUTE_API_KEY X-Workspace-Id: $PERMUTE_WORKSPACE_ID Content-Type: application/json ``` ## Hosted MCP Authentication and Scope Documentation: https://permute.ai/docs/mcp Organization admins publish a stable MCP server URL for one workspace and a selected set of datasets or connectors. End users paste that URL into Claude or another compatible client and complete Permute OAuth consent. Hosted MCP does not accept Permute API keys, and users do not enter an OAuth client ID or secret. Effective MCP access is always the intersection of the connector-selected sources and the connecting user's current workspace permissions. MCP tools are read-only: list_sources, get_source, answer, get_answer, search, and query. Mutations remain SDK or REST operations. ## Recommended Analytical Flow 1. List sources to discover datasets and connectors accessible to the key. 2. Inspect a source before selecting exact tables or fields. 3. Start an answer job for interpreted business questions; the SDK waits for the durable result automatically. 4. Use evidence search when the goal is relevant supporting material and citations. 5. Use a structured query only when source IDs and exact table names are already known. ## Integration Guides ### Build and deploy an app Build a static frontend locally, upload its files, save a draft, and publish it at a Permute-hosted URL. Permute versions each complete build; visitors use Permute sign-in by default. Shared-password demos can serve mocked frontend content. Documentation: https://permute.ai/docs/guides/deploy-app Workflow: 1. Prepare your project and API key. Creating an app requires permission to write apps in the workspace; updating an existing app can use a key with read/write access to just that app. Keep the key in your deployment environment, never in browser code. 2. Build the frontend into dist with relative asset URLs. Keep source, package manifests, and the dependency lockfile in your own project. Permute stores the compiled build, and does not run your build tools. 3. Create an app or select an existing app ID. Creation provisions a URL and publishes a starter page. The script waits for hosting to become live before uploading your build. 4. Upload, save, then publish. apps.uploadFiles stores immutable files without changing a draft. apps.saveSite verifies the complete file list and saves a draft revision. revisions.publish makes that draft live; the script below performs all three steps. 5. Open the printed URL and sign in with Permute. Visitors need app read permission. To update the site, rebuild and rerun with the same app ID; to roll back, use an earlier revision ID as shown below. Project setup: ```bash # For a new React project; skip creation if you already have a frontend. bun create vite operations-portal --template react-ts cd operations-portal bun install bun add @permute/sdk@^0.6.0 # Edit src, then build the complete site with relative asset URLs. bun run build --base=./ ``` Preferred SDK example: ```typescript // Save as deploy.ts in your frontend project. Running it publishes the build. import { PermuteClient, type AppUploadFile, type ResourceVersionState } from '@permute/sdk'; import { readdir, readFile } from 'node:fs/promises'; import { join } from 'node:path'; function required(name: string): string { const value = process.env[name]?.trim(); if (!value) throw new Error('Set ' + name + ' before running this script'); return value; } const client = new PermuteClient({ apiKey: required('PERMUTE_API_KEY'), baseUrl: process.env.PERMUTE_API_BASE_URL, // Defaults to https://api.permute.ai. }).withWorkspace(required('PERMUTE_WORKSPACE_ID')); const appId = process.env.PERMUTE_APP_ID; const rollbackRevisionId = process.env.PERMUTE_ROLLBACK_REVISION_ID; if (rollbackRevisionId && !appId) throw new Error('Set PERMUTE_APP_ID when rolling back'); let app = appId ? await client.apps.get(appId) : await client.apps.create({ name: 'Operations portal', key: 'operations-portal', description: '' }); console.log('App ID:', app.id); const deadline = Date.now() + 10 * 60_000; while (app.deployment?.status !== 'live') { if (!app.deployment) throw new Error('This app has no hosting deployment'); if (['failed', 'deleting', 'deleted'].includes(app.deployment.status)) { throw new Error(app.deployment.error ?? 'App cannot be published: ' + app.deployment.status); } if (Date.now() >= deadline) throw new Error('Provisioning timed out; inspect this app before retrying'); await new Promise((resolve) => setTimeout(resolve, 2000)); app = await client.apps.get(app.id); } async function readBuild(directory: string, prefix = ''): Promise { const files: AppUploadFile[] = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const path = prefix + entry.name; if (entry.isDirectory()) files.push(...await readBuild(join(directory, entry.name), path + '/')); else if (entry.isFile()) files.push({ path, data: await readFile(join(directory, entry.name)) }); else throw new Error('Build contains a symbolic link or unsupported entry: ' + path); } return files; } let ready: ResourceVersionState; if (rollbackRevisionId) { const current = await client.revisions.list(app.id); ready = await client.revisions.restore(app.id, rollbackRevisionId, { expectedVersion: current.version }); } else { const current = await client.revisions.list(app.id); if (current.draft?.revisionId !== current.published?.revisionId) { throw new Error('Review the existing unpublished draft before replacing it'); } // 1. Upload bytes. This does not change the draft or the live app. const files = await client.apps.uploadFiles(app.id, await readBuild('./dist')); // 2. Save the COMPLETE file list as a draft, preserving app settings. const saved = await client.apps.saveSite(app.id, { site: { files, spa: true }, expectedDraft: app.draft, }); ready = await client.revisions.list(app.id); if (ready.draft?.revisionId !== saved.draft.revisionId || ready.draft.sequence !== saved.draft.sequence) { throw new Error('Another author changed the draft; review before publishing'); } } // 3. Publish the exact draft we saved or restored. Content updates need no hosting rollout. console.log('Previous live revision:', ready.published?.revisionId); const published = await client.revisions.publish(app.id, { expectedVersion: ready.version }); console.log('Live app:', app.deployment.url); console.log('Published revision:', published.published?.revisionId); ``` #### Run the deployment Save the complete example as deploy.ts and run it from the project root so it can find dist. Supply PERMUTE_API_KEY through your shell or deployment secret store. Set PERMUTE_API_BASE_URL if you are using a different API environment. For a new app, choose its name and a stable creation key in the script. Reuse that key when retrying creation, and keep the printed app ID for later updates. A different new app needs a different creation key. ```bash # PERMUTE_API_KEY must already be set in your shell. export PERMUTE_WORKSPACE_ID=wksp_... # First deployment: creates an app, uploads dist, saves a draft, and publishes. bun run deploy.ts # Later deployments: rebuild and update the same app. export PERMUTE_APP_ID=appl_... bun run build --base=./ bun run deploy.ts ``` #### Directory deployment helper (SDK 0.6.0) SDK 0.5.0 introduced deployApp from @permute/sdk/deploy for Node and Bun. Use SDK 0.6.0 for the grant-based app API shown here. The helper validates the complete build before uploading, saves a draft, and publishes with compare-and-set checks; it does not build or provision the app. By default the draft must already be published. To replace an unpublished draft, review it and pass its exact expectedDraft. Omitted actions preserve the current bindings (an empty array clears them); supplied actions must name exact published revisions. Unchanged files, SPA mode, and supplied bindings return changed: false without uploads or writes; the helper rechecks refs first. A reviewed unpublished draft still publishes its settings changes. The helper stops on concurrent changes and never retries mutations. After a failed or lost response, inspect app history before retrying: publication may have completed, or the saved draft may need review. Use individual upload/save calls when you want to review a draft before publication. ```typescript import { deployApp } from '@permute/sdk/deploy'; // client and app come from the provisioning script above. const deployed = await deployApp(client, { appId: app.id, directory: './dist' }); console.log(deployed.url, deployed.revisionId, deployed.changed); ``` #### What happens to files and versions Uploading the same contents reuses the existing file. Changed contents at the same path are stored separately, and the new draft points that path to the new contents. Previous versions keep their original files. Every save replaces the complete file list (the manifest), so include unchanged files too. A file omitted from the new manifest disappears from the live app when you publish. Uploading or saving alone never changes the live site. Apps use the shared resource versioning system. App-specific save calls validate the files and advance the draft; revisions.list, revisions.get, revisions.restore, and revisions.publish provide history, revision reads, rollback, and publication. To review a draft before release, stop after saving and publish it separately. expectedDraft and expectedVersion reject concurrent changes. On a 409 conflict, read the current draft and reconcile your build before trying again. Replacing an existing unpublished draft is an intentional edit; review it before saving. #### Roll back a published app Use the previous live revision printed by the script, or choose a revision from client.revisions.list(appId). Run the command below with the same API key and workspace. It restores that revision into a new draft and publishes it without uploading files. Restore recovers the complete app definition, including its name, description, and files. Restoring alone leaves the live site unchanged. Previously published assets remain readable under current app permissions so pages already open can finish loading; unpublished drafts are not served. ```bash PERMUTE_APP_ID=appl_... PERMUTE_ROLLBACK_REVISION_ID=rvsn_... bun run deploy.ts ``` #### Pin actions to an app release Use SDK 0.6.0 or later. Create and publish a business action with the business-actions guide first. Add action revision pins to the apps.saveSite request in deploy.ts, as shown below. The files and app variables come from that deployment script; read the action definition to select its published revision. Saving and publishing require invocation permission on each selected action. Omitting actions preserves the current bindings; passing [] clears them. Publish the saved app draft with the same revision check used in deploy.ts. Before publishing, an app admin must approve resource grants on the app detail page or through the grants API. Approval also requires admin permission on each target. Grant Read for query sources and the action’s required invocation level for actions. Existing apps need this approval too; bindings alone no longer authorize app API calls. Each app revision pins its action revisions. Publishing a new action does not silently change an existing app; update the binding and publish the app again to adopt it. Query access follows current app grants and user permissions; source grant changes take effect without republishing. SQL filters in browser code are not a row-level security boundary. ```typescript const actionId = required('PERMUTE_ACTION_ID'); const action = await client.actions.definition(actionId); // Requires action admin. if (!action.published) throw new Error('Publish the action first'); const saved = await client.apps.saveSite(app.id, { site: { files, spa: true }, expectedDraft: app.draft, actions: [{ actionId, revisionId: action.published.revisionId }], }); ``` #### Approve and revoke app access App grants apply to all published revisions, including pages already open. Revoking a grant blocks subsequent requests without publishing a new site. Saving or restoring a revision does not recreate grants. To revoke, send the same PUT request with level: null; app admin permission is sufficient. Each request must fit the signed-in user’s permissions and the app’s current grants. Action calls also require a revision pin in the served app release; queries do not require a source binding. A grant to invoke an action does not grant raw access to its database or connectors. The action retains its separately approved execution scope (the caller only needs invocation permission). ```bash # Use an app and target administrator’s API key outside the browser. curl -X PUT "https://api.permute.ai/v1/apps/$PERMUTE_APP_ID/grants/$PERMUTE_DATABASE_ID" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{"level":"read"}' # Use the action’s required invocation level (write in this example). curl -X PUT "https://api.permute.ai/v1/apps/$PERMUTE_APP_ID/grants/$PERMUTE_ACTION_ID" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{"level":"write"}' ``` #### Read the current user, query and invoke from the frontend Bundle this code into your frontend. PermuteAppClient uses the visitor’s existing Permute sign-in session and the page’s revision-specific base URL. Keep the deployment API key out of browser code. Use a database with an approved app read grant and an action pinned to the app revision. Call app.me() to display the current signed-in user’s id, name and email. It uses the app session, checks current membership and app read access, and returns no organization or workspace directory. Password demos have no user identity and return 403. The HTTP equivalent is GET /v1/me on the app hostname, with its session cookie. Queries require current source read permission for both the signed-in user and the app. Action calls require the action’s declared invocation permission and execute its delegated scope. Reuse the exact requestId and input after an uncertain response; the browser client does not retry writes automatically. A 401 means the visitor needs to sign in again. ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const app = new PermuteAppClient(); const user = await app.me(); document.querySelector('#user-name')!.textContent = user.name; const rows = await app.query({ sourceIds: ['dbas_...'], sql: 'SELECT id, name FROM app.customers', limit: 100, }); console.log(rows.rows); // Retain this request until its outcome is known; retries reuse the same object. const request = { requestId: crypto.randomUUID(), input: { id: crypto.randomUUID(), name: 'Acme' } }; const receipt = await app.actions.invoke('actn_...', request); console.log(receipt.output); ``` #### Recover a write after sign-in or an app update Before sending a write, retain its original document.baseURI, actionId, requestId, and input in storage appropriate to the input’s sensitivity. Never store credentials or session cookies. The SDK does not persist or automatically retry requests. After sign-in or a newer app release, resume as the same user with the original base URL and request (not the new page’s defaults). This preserves the action and app revision selected for the original attempt. Old revisions still enforce current permissions. A later 401, 403, or 409 does not establish that an earlier attempt failed to commit. Keep an uncertain request until its outcome is reconciled; do not clear it, alter its input, or create a replacement ID just because a retry failed. Resolve an earlier uncertain write before submitting edited input as a new operation. ```typescript // pending was retained before the original send, without credentials. const originalApp = new PermuteAppClient({ baseUrl: pending.baseUrl }); const receipt = await originalApp.actions.invoke(pending.actionId, pending.request); ``` #### Choose visitor access Apps default to Permute sign-in. Existing workspace membership, app permissions, user permissions and app grants continue to govern access. For a demo with mocked data, an app admin can select Shared password (demo) under App access, or use the access API below. The password gates every frontend file, including JavaScript and direct asset URLs. Anyone admitted can inspect downloaded code and files from previously published releases. Use a separate demo app if older releases contain sensitive content. Password demos cannot query Permute data or invoke Business Actions, even with existing app grants. Passwords must have 8 to 256 characters. The server stores a salted hash and limits password submissions to 20 per minute per app. Sessions last up to eight hours. Saving access settings applies immediately and signs out existing visitors; it does not create or publish a site revision. Restoring a site does not restore access settings. Password-free public access is unavailable. ```typescript // Requires an SDK build with apps.access/setAccess (not included in 0.6.0). const access = await client.apps.access(appId); await client.apps.setAccess(appId, { mode: 'password', password: required('DEMO_PASSWORD'), expectedVersion: access.version, }); // To require Permute sign-in again, read the latest access version and send // { mode: 'permute', expectedVersion: latest.version }. ``` ```bash # App admin API key; keep both credentials out of browser bundles. curl "https://api.permute.ai/v1/apps/$PERMUTE_APP_ID/access" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" # PUT the same URL with JSON {"mode":"password","password":"...","expectedVersion":0}. # Use the version returned by GET. Never commit the password to source control. ``` #### Set the app favicon In App details → Appearance, choose a square PNG up to 256 KiB, save the draft, then publish. The favicon appears in browser tabs and on both sign-in methods, independently of custom login styling. It overrides icons in the compiled site. Set favicon to null to remove the override; the site’s own icon then applies. The favicon uses the existing app upload API and version history (no separate storage or deployment). Only the current published favicon is public before sign-in. Draft icons require app read access; historical icons are served only to authenticated visitors of that published release. Each app has its own icon; there is no organization default. ```typescript import { readFile } from 'node:fs/promises'; const current = await client.apps.get(appId); const [favicon] = await client.apps.uploadFiles(appId, [ { path: 'branding/favicon.png', data: await readFile('./favicon.png') }, ]); const saved = await client.apps.update(appId, { favicon, expectedDraft: current.draft }); const state = await client.revisions.list(appId); if (state.draft?.revisionId !== saved.draft.revisionId || state.draft.sequence !== saved.draft.sequence) { throw new Error('Draft changed; review before publishing'); } await client.revisions.publish(appId, { expectedVersion: state.version }); ``` #### Build and hosting requirements Upload the complete dist directory, including index.html. The limits are 200 files, 4 MiB per file, and 50 MiB per build. The SDK hashes each file and uploads it through a signed URL; saving verifies the checksum and size before accepting the manifest. Use relative URLs for bundled and public assets. Permute inserts a revision-specific base element into HTML so relative files stay on the same version. Root-relative asset paths bypass that pin. For a runtime data file, fetch(new URL("data/example.json", document.baseURI)) stays on the page’s revision. The example sets spa: true, which serves index.html for extensionless HTML navigation. Missing asset files still return 404. Use spa: false when the build contains separate HTML pages and does not need client-side routing fallback. Hosting supports static HTML, CSS, JavaScript, and assets. Inline scripts, external scripts and API requests, workers, and server-side rendering are unavailable. There is no hosted build service or app source editor; keep source and lockfiles in your own project. With Permute sign-in, the browser SDK supports governed queries and published business actions through the app session. Shared-password demos serve frontend files only; password-free public access is unavailable. #### Custom app addresses Organization admins connect a dedicated namespace, such as apps.example.com, in Organization Settings → App domains. Add the four displayed NS records at your DNS provider for that subdomain (often entered as apps). Keep your main domain’s nameservers unchanged. Permute manages app routing and HTTPS certificate validation in the delegated zone. Keep the NS records while using the namespace; setup is asynchronous, so wait for Ready. Once the namespace is ready, app admins can choose a name under App details → App URL. The SDK equivalent is client.apps.address(appId), followed by client.apps.setAddress(appId, { address: { namespaceId, label: "crm" }, expectedUrl: current.address?.url ?? null }). Use a namespace ID returned by the address read. Set address to null to remove the custom address. These methods require an SDK release containing apps.address and apps.setAddress; the REST routes are GET and PUT /v1/apps/{applicationId}/address. An address assignment takes effect without a content publication or CloudFront rollout. Existing generated Permute URLs remain available. Each hostname has its own sign-in session; app permissions, grants, and password-demo restrictions apply on every address. Removing the organization namespace immediately disconnects all its custom app addresses, then starts cloud cleanup. Remove its NS records from your DNS provider to allow final zone deletion after cached delegation expires. Reconnecting the same namespace reuses its retained zone and nameservers while available, rechecks delegation and provisions HTTPS again. Check the displayed NS records after reconnecting and reassign app addresses; previous assignments are not restored. This release supports one namespace per organization in each environment and one custom address per app. Each app occupies one subdomain level, such as crm.apps.example.com. Individual domain aliases, routing multiple apps under URL paths, and custom domains for the main Permute platform are not included. Namespace setup requires a signed-in organization admin; API keys can assign app addresses but cannot manage organization DNS ownership. ### Define database schemas and update records Save and publish managed table schemas, read filtered rows, and update records with version checks and safe retries. Documentation: https://permute.ai/docs/guides/managed-database Workflow: 1. Create and wait for a database 2. Save and publish a typed schema 3. Insert and filter records 4. Update with __version 5. Handle a conflict Preferred SDK example: ```typescript import { PermuteApiError, PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY! }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); // Creation needs an eligible Enterprise plan and workspace database admin access. let database = await client.databases.create({ name: 'CRM', key: 'crm' }); while (database.status === 'provisioning') { await new Promise((resolve) => setTimeout(resolve, 2000)); database = await client.databases.get(database.id); } if (database.status !== 'ready') throw new Error('Database is not ready'); const current = await client.databases.definition(database.id); const saved = await client.databases.saveDefinition(database.id, { expectedDraft: current.draft ? { revisionId: current.draft.revisionId, sequence: current.draft.sequence } : null, definition: { tables: [ ...(current.draft?.definition.tables ?? current.published?.definition.tables ?? []), { name: 'customers', columns: [ { name: 'id', type: 'text', nullable: false }, { name: 'name', type: 'text', nullable: false }, ], primaryKey: ['id'], }, ], }, }); const history = await client.revisions.list(database.id); if (history.draft?.revisionId !== saved.revisionId) { throw new Error('Schema draft changed; review before publishing'); } await client.revisions.publish(database.id, { expectedVersion: history.version }); console.log('Database ID:', database.id); const insert = { requestId: 'customer-acme-insert-1', mutations: [{ type: 'insert' as const, table: 'customers', values: { id: 'acme', name: 'Acme' } }], }; await client.databases.transact(database.id, insert); // If the response is uncertain, resend the identical requestId and payload. await client.databases.transact(database.id, insert); const filtered = await client.query({ sourceIds: [database.id], sql: "SELECT id, name FROM app.customers WHERE id = 'acme'", }); console.log(filtered.rows); const rows = await client.databases.records(database.id, 'customers', { limit: 25 }); const customer = rows.items.find((row) => row.id === 'acme'); if (!customer || typeof customer.__version !== 'string') throw new Error('Customer not found'); await client.databases.transact(database.id, { requestId: 'customer-acme-update-1', mutations: [{ type: 'update', table: 'customers', key: { id: 'acme' }, expectedVersion: customer.__version, values: { name: 'Acme Inc.' }, }], }); try { await client.databases.transact(database.id, { requestId: 'customer-acme-stale-update-1', mutations: [{ type: 'update', table: 'customers', key: { id: 'acme' }, expectedVersion: customer.__version, values: { name: 'Acme LLC' }, }], }); } catch (error) { if (!(error instanceof PermuteApiError) || error.status !== 409) throw error; const current = await client.databases.records(database.id, 'customers'); console.log('Record changed; review before retrying:', current.items); } ``` #### Schema drafts and publication client.databases.definition returns draft and published schemas. saveDefinition replaces the complete table list and requires the current draft ref (revisionId and sequence), or null for the first save. Preserve existing tables when adding one. History, restore, and publication use client.revisions. Publishing adds tables and nullable columns, then advances the published schema. Existing rows read NULL in new columns and keep their record-version tokens. Preserve existing columns, types, nullability and primary keys. Table or column removal and adding required columns are rejected. Restoring changes the draft only; it does not restore rows or undo schema changes. client.databases.createTable is a convenience for adding one table to the draft. Since 0.4.0, callers must publish the database before writing records to that table. Direct transactions require database admin permission; use a published business action for a narrower delegated write. #### Add a column to an existing table Use the same definition and publication APIs to extend a table while preserving its records. New columns must explicitly set nullable: true. The complete definition must retain every existing table and column. This example extends the customers table from the guide above. DDL is applied before the published ref advances. If publication fails partway through, some additions may already be live; retry the same definition to resume. A conflicting live schema stops publication. Existing action revisions retain their schema bindings; update and publish the action contract and binding when exposing a new field. ```typescript const schema = await client.databases.definition(database.id); if (!schema.published || schema.draft?.revisionId !== schema.published.revisionId) { throw new Error('Review the unpublished schema draft first'); } const definition = structuredClone(schema.published.definition); const customers = definition.tables.find((table) => table.name === 'customers'); if (!customers) throw new Error('Customers table not found'); if (customers.columns.some((column) => column.name === 'notes')) { throw new Error('Notes column already exists; inspect the current definition'); } customers.columns.push({ name: 'notes', type: 'text', nullable: true }); const saved = await client.databases.saveDefinition(database.id, { expectedDraft: { revisionId: schema.draft.revisionId, sequence: schema.draft.sequence }, definition, }); const refs = await client.revisions.list(database.id); if (refs.draft?.revisionId !== saved.revisionId || refs.published?.revisionId !== schema.published.revisionId || refs.published?.sequence !== schema.published.sequence) { throw new Error('Schema changed; review before publishing'); } await client.revisions.publish(database.id, { expectedVersion: refs.version }); ``` #### Write rules Run this example once with a fresh database key. Table creation and insertion are one-time operations. Use a new requestId for each new mutation; reuse it only when retrying the same payload after an uncertain response. Record reads return __version for managed tables. An update or delete needs the complete primary key and the version just read. A 409 means the record changed or the request ID was reused with different data (read current state and decide what to write). Filtered reads use client.query with read-only SQL against the database. client.databases.records provides bounded current-state pages but does not accept a filter. Pagination is not a stable snapshot. ### Create and invoke a business action Publish a typed action with an approved database scope, then invoke it through the SDK or a hosted app. Documentation: https://permute.ai/docs/guides/business-actions Workflow: 1. Follow the managed-database guide to publish a customers table with id and name columns. Set PERMUTE_DATABASE_ID to that database ID. 2. Create an action with business action creation admin permission. Editing and publishing require admin permission on the action and each selected source. 3. Save the handler, input and output contracts, invocation permission, and published schema bindings as one draft. 4. Publish the exact draft through client.revisions, then invoke it with a stable requestId and input. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const client = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY! }) .withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const databaseId = process.env.PERMUTE_DATABASE_ID!; const schema = await client.databases.definition(databaseId); if (!schema.published) throw new Error('Publish the database schema first'); // Keep the ID after creation. Set PERMUTE_ACTION_ID to edit this action later. const action = process.env.PERMUTE_ACTION_ID ? await client.actions.get(process.env.PERMUTE_ACTION_ID) : await client.actions.create({ name: 'Create customer' }); console.log('Action ID:', action.id); const current = await client.actions.definition(action.id); const contract = { type: 'object', properties: { id: { type: 'string', minLength: 1 }, name: { type: 'string', minLength: 1 } }, required: ['id', 'name'], additionalProperties: false, }; const saved = await client.actions.saveDefinition(action.id, { expectedDraft: current.draft ? { revisionId: current.draft.revisionId, sequence: current.draft.sequence } : null, definition: { source: "exports.handler = ({ input, db }) => { db.insert('customers', input); return input; };", inputSchema: contract, outputSchema: contract, invokePermission: 'write', sources: [{ sourceId: databaseId, revisionId: schema.published.revisionId, readTables: [], writeTables: ['customers'], }], }, }); const history = await client.revisions.list(action.id); if (history.draft?.revisionId !== saved.revisionId) { throw new Error('Action draft changed; review before publishing'); } await client.revisions.publish(action.id, { expectedVersion: history.version }); // Persist this request before sending if retries must survive a process restart. const request = { requestId: crypto.randomUUID(), input: { id: crypto.randomUUID(), name: 'Acme' } }; const receipt = await client.actions.invoke(action.id, request); console.log(receipt.revisionId, receipt.output); // After a lost response, retry with the identical requestId and input. ``` #### Permissions and publication The author delegates selected database tables and connector accounts when saving and publishing. Callers need the declared read, write, or admin permission on the action; they do not need general write access to its database. Use the narrowest action contract that fits the operation. client.actions.list exposes published input and output contracts without source code. client.actions.definition requires admin permission and returns draft and published definitions. History, restore, and publication use client.revisions; restoring does not publish or execute the action. Hosted apps bind an exact published action revision in apps.saveSite and call it through @permute/sdk/app. The deploy-app guide covers bindings and browser calls. API-key invocations use the current published action, while a retry retains the revision selected by its first attempt. #### Read warehouse sources and operation outputs Bind queryable warehouse sources with { sourceId }: data_ for processed files, conn_ for synced connectors, oper_ for Data Tables, Data Flows, reports, and dashboards, or enty_ for completed entity outputs. Operations need a completed output and entity outputs must be outside draft lifecycle state. Google Drive imports (like uploads) become individual data_ datasets; bind those file IDs. Saving and publishing delegate read access to the whole selected source and require its admin permission. warehouse.read({ sourceId }) returns text blocks with their IDs, order, and page numbers, plus table IDs, display names, and column schemas. Pass a table block ID or an unambiguous display name to read rows. where accepts equality filters with string, boolean, or null values. Unknown or ambiguous tables and unknown columns are rejected. Each read permits at most 1,000 rows or text blocks and 1 MB of data; the default limit is 100. Oversized reads fail, so narrow table filters or raise limit within that bound. Warehouse reads use current available output, including retained output after a failed refresh; publishing an action does not freeze source content. Schemas and subscriptions have no query capability. Live SQL and external writes use supported named connector calls. ```typescript // Add { sourceId: fileId } to the action definition's sources, then publish. // Handler source: exports.handler = async ({ inputs, warehouse }) => { const document = await warehouse.read({ sourceId: inputs.fileId }); if (!document.tables.length) return { blocks: document.blocks, rows: [] }; const rows = await warehouse.read({ sourceId: inputs.fileId, table: document.tables[0].id, limit: 1000 }); return { blocks: document.blocks, rows }; }; ``` #### Reject a business operation Handlers receive reject(code, message, details?). Use it for expected business failures: validation_failed (400), record_conflict or invalid_transition (409), permission_denied (403), or not_found (404). Rejection stops the handler and discards staged database writes; connector calls already sent cannot be undone. Messages and details are returned to the caller, so keep diagnostics and secrets out. Messages permit 1–1,000 characters. Optional details accepts up to 20 string fields, with keys of 1–100 characters and values up to 500. Invalid rejections and unexpected exceptions return execution_failed (500), with private exception text in server logs. Both the API-key and browser SDKs expose PermuteApiError.code, status, and details. Recover using codes rather than matching messages. Reload a stale record on record_conflict; correct input on validation_failed. For request_in_progress (409), wait and retry the same request. request_conflict (409) means the ID was already used for different input or a different action revision than the app’s pin. outcome_unknown (409) requires retaining the original request and checking external state before another write. Transport and authentication failures can still use general API error codes. ```typescript // Handler source, published with its input and output contracts. exports.handler = ({ input, db, reject }) => { if (!input.name.trim()) reject('validation_failed', 'Enter a name.', { field: 'name' }); db.insert('customers', input); return input; }; ``` #### Writes, retries, and limits Handlers run sandboxed JavaScript. db.insert, db.update, and db.delete stage local writes until the handler completes and its output validates. An action can write to one managed database, with up to 100 mutations; the mutations and receipt commit atomically. Reads are outside that write transaction. A new user operation needs a new requestId. Reuse both requestId and input when retrying an uncertain response (changing the payload is not a retry). Do not generate a new ID merely because a network request failed. Optional connectors maps names to connector IDs. Handlers call connectors..request with provider-relative HTTP paths, query for supported SQL providers, or putObject for S3. Each external call needs a stable stepId; credentials remain on the server, and the connected account’s grants apply. External effects happen when awaited and cannot be rolled back with local database writes. Successful steps replay their saved responses; uncertain outcomes block automatic resend and require checking provider state. There is no reconciliation API or long-running workflow runner. Changing a published database write target requires a new action. ### Set up a workspace Create a workspace with an organization-scoped management key, then use its ID for workspace data operations. Documentation: https://permute.ai/docs/guides/workspace Workflow: 1. Create the workspace 2. Keep the returned workspace ID 3. Use that workspace for data operations Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const workspace = await orgClient.workspaces.create({ name: 'Finance', context: 'Revenue, customers, and operating performance', // Optional. }); const client = orgClient.withWorkspace(workspace.id); const sources = await client.sources.list(); console.log(workspace.id, sources.items); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" # context is optional. WORKSPACE=$(curl -fsS -X POST "$BASE_URL/v1/workspaces" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Finance", "context": "Revenue, customers, and operating performance" }') WORKSPACE_ID=$(printf '%s' "$WORKSPACE" | jq -r '.data.id') curl -fsS "$BASE_URL/v1/sources" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $WORKSPACE_ID" | jq '.data.items' ``` ### Ask a business question Discover an accessible source and ask Permute a business question. The SDK waits for the durable analysis automatically. Documentation: https://permute.ai/docs/guides/analysis Workflow: 1. List accessible sources 2. Start an answer job 3. Poll the job 4. Use the completed answer Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const sources = await client.sources.list(); const sourceId = sources.items[0].id; const answer = await client.answer({ question: 'What changed in revenue this quarter?', sourceIds: [sourceId], }); console.log(answer); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" SOURCES=$(curl -fsS "$BASE_URL/v1/sources" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID") SOURCE_ID=$(printf '%s' "$SOURCES" | jq -r '.data.items[0].id') JOB=$(curl -fsS -X POST "$BASE_URL/v1/answer-jobs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg sourceId "$SOURCE_ID" \ '{question: "What changed in revenue this quarter?", sourceIds: [$sourceId]}')") JOB_ID=$(printf '%s' "$JOB" | jq -r '.data.answerJobId') while true; do RESULT=$(curl -fsS "$BASE_URL/v1/answer-jobs/$JOB_ID" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID") STATUS=$(printf '%s' "$RESULT" | jq -r '.data.status') [ "$STATUS" = "completed" ] && break case "$STATUS" in failed|cancelled) printf '%s\n' "$RESULT" >&2; exit 1 ;; esac sleep 2 done printf '%s' "$RESULT" | jq '.data.result' ``` ### Upload and process a file Create a file dataset, PUT the bytes to its signed S3 target, and poll while processing runs automatically. CSV, TSV, JSON, NDJSON, and Parquet files can be up to 1 GiB; other supported file types can be up to 100 MiB. Permute applies the limit from contentType. Documentation: https://permute.ai/docs/guides/file-upload Workflow: 1. Create the file dataset 2. PUT bytes to S3 3. Let processing start automatically 4. Poll until completed Preferred SDK example: ```typescript import { readFile } from 'node:fs/promises'; import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const bytes = await readFile('./revenue.csv'); const dataset = await client.datasets.create({ type: 'file', fileName: 'revenue.csv', contentType: 'text/csv', data: bytes, // The SDK infers sizeBytes. }); while (dataset.status !== 'completed' && dataset.status !== 'failed') { await new Promise((resolve) => setTimeout(resolve, 2000)); await dataset.refresh(); } if (dataset.status === 'failed') throw new Error(dataset.data.error ?? 'Dataset processing failed'); console.log(dataset.data); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" FILE_PATH="./revenue.csv" SIZE_BYTES=$(wc -c < "$FILE_PATH" | tr -d ' ') DATASET=$(curl -fsS -X POST "$BASE_URL/v1/datasets" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg fileName "revenue.csv" --arg contentType "text/csv" \ --argjson sizeBytes "$SIZE_BYTES" \ '{type: "file", fileName: $fileName, contentType: $contentType, sizeBytes: $sizeBytes}')") DATASET_ID=$(printf '%s' "$DATASET" | jq -r '.data.id') UPLOAD_URL=$(printf '%s' "$DATASET" | jq -r '.data.upload.url') UPLOAD_TYPE=$(printf '%s' "$DATASET" | jq -r '.data.upload.headers["Content-Type"]') UPLOAD_MATCH=$(printf '%s' "$DATASET" | jq -r '.data.upload.headers["If-None-Match"]') curl -fsS -X PUT "$UPLOAD_URL" \ -H "Content-Type: $UPLOAD_TYPE" \ -H "If-None-Match: $UPLOAD_MATCH" \ --data-binary "@$FILE_PATH" while true; do DATASET=$(curl -fsS "$BASE_URL/v1/datasets/$DATASET_ID" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID") STATUS=$(printf '%s' "$DATASET" | jq -r '.data.status') [ "$STATUS" = "completed" ] && break [ "$STATUS" = "failed" ] && { printf '%s\n' "$DATASET" >&2; exit 1; } sleep 2 done printf '%s' "$DATASET" | jq '.data' ``` ### Sync related application data Create or recover one stable custom dataset. Replace current source state when rows can change or disappear; append only for additive sources. Documentation: https://permute.ai/docs/guides/custom-dataset Workflow: 1. Get or create by stable key 2. Replace current state or append new rows 3. Keep stable table identities 4. Resolve the dataset by key later Preferred SDK example: ```typescript import { Context, PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const dataset = await client.datasets.create({ type: 'custom', key: 'quickbooks', name: 'QuickBooks', context: [Context.QUICKBOOKS], }); await dataset.replace({ orders: [{ id: 1, customerId: 10, amount: 100 }], customers: [{ id: 10, name: 'Acme' }], }); // The SDK uploads chunked NDJSON to S3 and waits for processing. // OR: replace one complete named table without changing the others. await dataset.replaceTable('payments', [{ id: 500, orderId: 1, amount: 100 }]); // OR: append only for an additive table. Reuse batchId when retrying this batch. await dataset.appendTable('events', [{ id: 'evt_501', type: 'payment.created', orderId: 2 }], { batchId: 'events-2026-07-15-2', }); const sameDataset = await client.datasets.getByKey('quickbooks'); console.log(sameDataset.id, sameDataset.data.tables); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" DATASET=$(curl -fsS -X POST "$BASE_URL/v1/datasets" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "type": "custom", "key": "quickbooks", "name": "QuickBooks", "context": ["quickbooks"] }') DATASET_ID=$(printf '%s' "$DATASET" | jq -r '.data.id') printf '%s\n' \ '{"tableName":"orders","rows":[{"id":1,"customerId":10,"amount":100}]}' \ '{"tableName":"customers","rows":[{"id":10,"name":"Acme"}]}' \ > /tmp/permute-dataset.ndjson SIZE_BYTES=$(wc -c < /tmp/permute-dataset.ndjson | tr -d ' ') CONTENT_HASH=$(shasum -a 256 /tmp/permute-dataset.ndjson | awk '{print $1}') WRITE=$(curl -fsS -X POST "$BASE_URL/v1/datasets/$DATASET_ID/tables" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d "$(jq -n --argjson size "$SIZE_BYTES" --arg hash "$CONTENT_HASH" '{ mode: "replace", tables: [{name: "orders", rowCount: 1}, {name: "customers", rowCount: 1}], sizeBytes: $size, contentHash: $hash }')") UPLOAD_URL=$(printf '%s' "$WRITE" | jq -r '.data.upload.url') curl -fsS -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/x-ndjson" \ --data-binary @/tmp/permute-dataset.ndjson while :; do CURRENT=$(curl -fsS "$BASE_URL/v1/datasets/$DATASET_ID" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID") STATUS=$(printf '%s' "$CURRENT" | jq -r '.data.write.status') [ "$STATUS" = "completed" ] && break [ "$STATUS" = "failed" ] && { printf '%s\n' "$CURRENT"; exit 1; } sleep 2 done curl -fsS "$BASE_URL/v1/datasets?key=quickbooks" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" | jq '.data' ``` ### Build and publish a data flow Turn two sample orders into a published revenue total of 350. This complete SDK example creates the input, saves a transform, runs the flow, checks its output, and publishes it (no built-in coding agent required). Documentation: https://permute.ai/docs/guides/data-flow Workflow: 1. Install @permute/sdk with bun add @permute/sdk. Set PERMUTE_API_KEY and PERMUTE_WORKSPACE_ID for a workspace where the key can read and write data and operations. Save the example as data-flow.ts and run bun data-flow.ts. 2. Write two sample orders and inspect the materialized source for its exact table name. 3. Create a flow, add one transform, then read its source state before saving the complete handler. 4. Run the draft and wait for that workflow to complete. Check the exact run output equals 350 before publishing. 5. For later edits, repeat read → save → run → inspect → publish. On a 409 conflict, read again and reconcile your changes before retrying. Each new run has its own output; published operations are available through sources and queries. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const client = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }).withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); // 1. Create sample input and wait for materialization. const dataset = await client.datasets.create({ type: 'custom', key: 'data-flow-guide', name: 'Sample orders', context: [], }); await dataset.replace({ orders: [{ amount: 100 }, { amount: 250 }] }); const input = await client.sources.get(dataset.id); const table = input.tables[0]; if (!table) throw new Error('Sample table is missing'); const sql = 'SELECT SUM(amount) AS total FROM "' + table.name.replaceAll('"', '""') + '"'; // 2. Add a transform to a new flow. let flow = await client.dataFlows.create({ name: 'Revenue total', description: 'Sum sample orders', dataSourceIds: [dataset.id], }); await client.dataFlows.command(flow.operation.id, { command: { type: 'addTransform', expectedRevision: flow.revision, input: { name: 'Total', instruction: 'Sum order amounts', inputIds: [dataset.id] }, }, }); flow = await client.dataFlows.get(flow.operation.id); const operationId = flow.operationIds[0]; if (!operationId) throw new Error('Transform is missing'); // 3. Save code using the state we just read. const current = await client.operations.getSource(operationId); await client.operations.updateSource(operationId, { source: 'module.exports = { handler: async (event) => ({ revenue: await event.query(' + JSON.stringify(sql) + ') }) };', expectedRevision: current.revision, expectedDataFlowRevision: current.dataFlowRevision, expectedUpdatedAt: current.updatedAt, }); // 4. Run the whole draft and wait for this workflow, not an older run. const started = await client.operations.run(flow.operation.id, { version: 'draft' }); async function waitForWorkflow() { const deadline = Date.now() + 5 * 60_000; while (Date.now() < deadline) { const workflow = await client.operations.listRuns({ workflowId: started.workflowId }); if (workflow.status === 'failed') throw new Error(workflow.items.find((run) => run.error)?.error ?? 'Workflow failed'); if (workflow.status === 'completed') return workflow; await new Promise((resolve) => setTimeout(resolve, 1500)); } throw new Error('Timed out waiting for workflow'); } const workflow = await waitForWorkflow(); const run = workflow.items.find((run) => run.operationId === operationId); if (!run?.outputDocId) throw new Error('Run output is missing'); const preview = await client.operations.getRunOutput(operationId, run.id); const revenueBlockId = Object.entries(preview.names).find(([, name]) => name.toLowerCase() === 'revenue')?.[0]; const rows = revenueBlockId ? preview.tables[revenueBlockId] as Array<{ total: unknown }> : undefined; console.table(rows); if (rows?.length !== 1 || Number(rows[0].total) !== 350) { throw new Error('Unexpected total; inspect the source and fix the handler before publishing'); } // 5. Publish only after validating the output, using its current version token. const ready = await client.revisions.list(flow.operation.id); await client.revisions.publish(flow.operation.id, { expectedVersion: ready.version }); console.log('Published flow:', flow.operation.id, 'Output source:', operationId); ``` ### Restore and publish an operation Recover an earlier revenue calculation, test its output, and publish the restored draft. Restore preserves history (including the later revision) and does not start a run. Documentation: https://permute.ai/docs/guides/versioning Workflow: 1. Install @permute/sdk. Set PERMUTE_API_KEY, PERMUTE_WORKSPACE_ID, PERMUTE_OPERATION_ID, and PERMUTE_REVISION_ID for an existing standalone operation and the revision you want to restore. The key needs operation write access and read access to its inputs. 2. Use client.revisions.list(operationId) to choose a revision, then client.revisions.get(operationId, revisionId) to review its saved definition. This example expects one revenue row with a total field; set EXPECTED_TOTAL to the value you want to verify. 3. Save the example as restore-operation.ts and run bun restore-operation.ts. It restores the chosen revision, runs the draft, checks the exact run output, and publishes only if the draft is still the one it tested. 4. A 409 means the resource changed. Read the latest state and reconcile before retrying. For a Data Flow child, restore its revision with unchanged graph connections, then run and publish the parent flow. Dashboard source uses the resourceId returned by dashboards.getSource and takes effect when restored, with no separate publication step. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const client = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }).withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const operationId = process.env.PERMUTE_OPERATION_ID!; const revisionId = process.env.PERMUTE_REVISION_ID!; const expectedTotal = Number(process.env.EXPECTED_TOTAL); if (!Number.isFinite(expectedTotal)) throw new Error('Set EXPECTED_TOTAL to the expected revenue'); // Restore the selected definition using the current version token. const history = await client.revisions.list(operationId); if (!history.capabilities.restore || !history.capabilities.publish) { throw new Error('Choose a standalone operation that supports restore and publish'); } const restored = await client.revisions.restore(operationId, revisionId, { expectedVersion: history.version, }); const started = await client.operations.run(operationId, { version: 'draft' }); async function waitForRun() { const deadline = Date.now() + 5 * 60_000; while (Date.now() < deadline) { const workflow = await client.operations.listRuns({ workflowId: started.workflowId }); if (workflow.status === 'failed') { throw new Error(workflow.items.find((run) => run.error)?.error ?? 'Workflow failed'); } if (workflow.status === 'completed') return workflow.items.find((run) => run.operationId === operationId); await new Promise((resolve) => setTimeout(resolve, 1500)); } throw new Error('Timed out waiting for the test run'); } const run = await waitForRun(); if (!run?.outputDocId) throw new Error('Run output is missing'); if (run.revisionId !== restored.draft?.revisionId) throw new Error('The run used a different draft'); const preview = await client.operations.getRunOutput(operationId, run.id); const blockId = Object.entries(preview.names).find(([, name]) => name.toLowerCase() === 'revenue')?.[0]; const rows = blockId ? preview.tables[blockId] as Array<{ total: unknown }> : undefined; console.table(rows); if (rows?.length !== 1 || Number(rows[0].total) !== expectedTotal) { throw new Error('Unexpected revenue; inspect the calculation before publishing'); } // Re-read after testing and reject intervening draft changes. const ready = await client.revisions.list(operationId); if (ready.draft?.revisionId !== restored.draft?.revisionId) { throw new Error('The draft changed during testing; review and test it again'); } await client.revisions.publish(operationId, { expectedVersion: ready.version }); console.log('Published restored revision:', ready.draft?.revisionId); ``` ### Publish a contact write sync Send a published Data Table to Quo after reviewing a read-only preview. Publication starts delivery immediately and approves new captures every 24 hours under the same API key. Documentation: https://permute.ai/docs/guides/write-sync Workflow: 1. Publish and run a Data Table with a contacts table containing email, first_name and last_name columns. Use unique, stable, nonempty email strings as keys. Grant the API key write-sync write, source read and destination read/write access (operation sources also require access to their inputs). 2. Use operations.get for the published revision and sources.get for output tables and fields. Configuration and preview validate the published output. Discover Quo destination fields and map their names to source column names. Mapping emails to the email key enables matching against Quo’s last read sync; refresh Quo before previewing when newer contacts may exist. 3. Create the paused definition, capture a preview, and poll until completion or error. Review every page, including blocked and unchanged records. Quo creates missing contacts and skips unchanged matches; changed linked records and ambiguous matches remain blocked. It does not update or delete contacts. 4. Use revisions.list for the version token, then revisions.publish with that token and the reviewed runId. Publication requires the same API key and a completed preview of the exact draft definition; it sends records and enables future captures. Each new operation capture follows the source’s current published revision, separately from the captured write-sync definition revision. Write syncs do not refresh upstream sources. 5. Inspect every result page after delivery, even when execution is complete. Pause to stop future writes; requests already sent may finish. To resume, preview and publish again. execute sends an existing reviewed capture only while the current definition is enabled, under the capturing API key; it does not refresh source data or change the schedule. 6. For a direct connector source, discover its table through sources.get and use source: { resourceId: connectorId, object: table, scope: "" } with input: { type: "source", table }. Capture uses the connector’s last read sync. Use REST or the SDK for these controls; they are not exposed through MCP. Preferred SDK example: ```typescript import { createInterface } from 'node:readline/promises'; import { PermuteClient } from '@permute/sdk'; const client = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }).withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const operationId = process.env.PERMUTE_CONTACTS_OPERATION_ID!; const destinationId = process.env.PERMUTE_QUO_CONNECTOR_ID!; const [operation, source] = await Promise.all([ client.operations.get(operationId), client.sources.get(operationId), ]); const revisionId = operation.published?.revisionId; const table = source.tables.find((item) => item.name === 'contacts'); if (!revisionId || !table || ['email', 'first_name', 'last_name'].some( (name) => !table.fields?.some((field) => field.name === name), )) throw new Error('Publish and run the expected contacts Data Table first'); const destinations = await client.writeSyncs.destinations(); const destination = destinations.items.find( (item) => item.id === destinationId && item.provider === 'quo', ); const contacts = destination?.objects.find((item) => item.object === 'contacts'); if (!contacts || ['emails', 'firstName', 'lastName'].some( (name) => !contacts.fields.some((field) => field.name === name), )) throw new Error('The selected Quo destination is unavailable'); const sync = await client.writeSyncs.create({ definition: { source: { resourceId: operationId, object: table.name, scope: '' }, destination: { resourceId: destinationId, object: contacts.object, scope: contacts.scope }, input: { type: 'operation', operationId, revisionId, table: table.name }, keyColumn: 'email', fields: { emails: 'email', firstName: 'first_name', lastName: 'last_name' }, }, }); const capture = await client.writeSyncs.preview(sync.id); // No external writes. console.log('Saved sync and preview:', sync.id, capture.id); async function waitForRun(delivery = false) { const deadline = Date.now() + 10 * 60_000; while (Date.now() < deadline) { const run = await client.writeSyncs.getRun(sync.id, capture.id); if (run.previewError) throw new Error(run.previewError); if (run.execution?.error) throw new Error(run.execution.error); if (delivery ? run.execution?.completedAt : run.previewComplete) return run; await new Promise((resolve) => setTimeout(resolve, 2000)); } throw new Error('Polling timed out; inspect the saved run before taking further action'); } async function inspectAllPages(run: Awaited>) { let needsAttention = false; for (let page = 0; page < run.pageCount; page++) { const result = page === 0 ? run : await client.writeSyncs.getRun(sync.id, run.id, { page }); console.log('Page', page + 1, 'of', run.pageCount); console.dir(result.records, { depth: null }); needsAttention ||= result.records.some((record) => record.action === 'blocked' || record.status === 'rejected' || record.status === 'unknown', ); } return needsAttention; } const preview = await waitForRun(); if (await inspectAllPages(preview)) { throw new Error('Resolve records needing attention before publishing'); } const terminal = createInterface({ input: process.stdin, output: process.stdout }); let approval: string; try { approval = await terminal.question( 'Review all captured values above. To send them and enable captures every 24 hours, type ' + sync.id + ': ', ); } finally { terminal.close(); } if (approval.trim() !== sync.id) throw new Error('Sync remains paused'); const history = await client.revisions.list(sync.id); if (!preview.definitionRevisionId || history.draft?.revisionId !== preview.definitionRevisionId) { throw new Error('The definition changed; capture and review a new preview'); } await client.revisions.publish(sync.id, { expectedVersion: history.version, runId: preview.id }); const delivered = await waitForRun(true); if (await inspectAllPages(delivered)) { console.error('Delivery completed with records needing attention; inspect their reasons'); } console.log('Sync remains enabled. Pause with client.writeSyncs.pause(' + JSON.stringify(sync.id) + ').'); ``` ### Manage context with facts and skills Save business definitions as organization facts and reusable instructions as shared Markdown skills. Create, read, replace, and delete sample context through the SDK. Documentation: https://permute.ai/docs/guides/context Workflow: 1. Create an organization-scoped API key with Write access to All Facts and All Skills. Set PERMUTE_API_KEY and PERMUTE_WORKSPACE_ID to an existing workspace in that organization. 2. Use facts for definitions, aliases, formulas, and baselines. Use organizationSkills for company-wide instructions and skills on a workspace client for workspace instructions (shared skills only). 3. Read back the saved context. Fact updates replace every editable field, so resend fields you want to keep. Skill updates create or replace a complete Markdown file of at most 2,000 words. 4. Run the example to verify each replacement. It uses unique names and paths, then deletes its sample context in finally; remove the cleanup block when adapting it to save real business context. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, baseUrl: process.env.PERMUTE_API_BASE_URL, // Optional; defaults to the public API. }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const sample = crypto.randomUUID(); const path = 'examples/' + sample + '/revenue.md'; // SDK encodes nested paths. const definition = { name: 'Sample net revenue ' + sample, definition: 'Revenue after returns, excluding tax.', formula: 'gross_revenue - returns', formulaType: 'math' as const, }; let factId: string | undefined; try { // Facts belong to the organization, regardless of the selected workspace. const fact = await orgClient.facts.create(definition); factId = fact.id; await orgClient.organizationSkills.update(path, { content: '# Revenue policy\nUse the net revenue fact when reporting revenue.', }); await client.skills.update(path, { content: '# Revenue reporting\nGroup net revenue by customer for this workspace.', }); const facts = await orgClient.facts.list(); const skills = await client.skills.list(); if (!facts.items.some((item) => item.id === factId) || !skills.items.some((item) => item.file === path)) { throw new Error('Saved context is missing from the catalog'); } console.log(await orgClient.organizationSkills.get(path)); // Resend the complete fact, preserving its definition and formula type. await orgClient.facts.update(factId, { ...definition, formula: 'gross_revenue - returns - discounts', }); const content = '# Revenue reporting\nGroup net revenue by customer and month.'; await client.skills.update(path, { content }); const savedFact = await orgClient.facts.get(factId); const savedSkill = await client.skills.get(path); if (savedFact.formula !== 'gross_revenue - returns - discounts' || savedSkill.content !== content) { throw new Error('Context replacement did not match'); } console.log('Verified fact and skill:', savedFact.id, path); } finally { // Delete only the unique sample records created by this example. await Promise.all([ ...(factId ? [orgClient.facts.delete(factId)] : []), orgClient.organizationSkills.delete(path), client.skills.delete(path), ]); } ``` ### Find supporting evidence Retrieve the most relevant evidence and citations for a specific claim or topic. Search reranks results by default. Documentation: https://permute.ai/docs/guides/search Workflow: 1. Describe the evidence you need 2. Review the highest-ranked results 3. Use returned citations Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); // sourceIds, types, limit, and rerank are optional. // rerank defaults to true; types filters broad table or text evidence categories. const evidence = await client.search({ query: 'Q4 revenue growth drivers', limit: 5, }); console.log(evidence.items); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" # sourceIds, types, limit, and rerank are optional. # rerank defaults to true; types filters broad table or text evidence categories. curl -fsS -X POST "$BASE_URL/v1/search" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "query": "Q4 revenue growth drivers", "limit": 5 }' | jq '.data' ``` ### Retrieve exact rows Inspect the source, then run read-only SQL against its exact table names when you need rows instead of interpretation. Documentation: https://permute.ai/docs/guides/query Workflow: 1. Inspect the source for table names and provider 2. Use the matching SQL dialect 3. Use the returned rows Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.query({ sourceIds: ['data_quickbooks123'], sql: 'SELECT * FROM orders LIMIT 100', }); console.table(result.rows); ``` Raw REST example: ```bash BASE_URL="https://api.permute.ai" curl -fsS -X POST "$BASE_URL/v1/queries" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "sourceIds": ["data_quickbooks123"], "sql": "SELECT * FROM orders LIMIT 100" }' | jq '.data' ``` ## Shopify Connector Documentation: https://permute.ai/docs/connectors/shopify The Shopify connector uses a merchant-owned app created in the Shopify Dev Dashboard. The app and target store must belong to the same Shopify organization. An App developer creates and releases a version. A store owner or administrator with installation permission can install it directly; otherwise, the App developer copies the installation link beside the Install app button and sends it to such a user. After installation, the merchant gives Permute the myshopify.com domain, client ID, and client secret. Shopify optional_scopes are not used. Every scope that Permute should receive must be placed in Shopify’s required Scopes field before the app is installed or its permissions are updated. Include read_all_orders in that first version. Without it, the first sync will only include the last 60 days of orders. Same-organization custom apps can add this scope without a Shopify security review. If Shopify shows Invalid scopes after the list is pasted, open Select Scopes and close the popup; that is a Shopify display bug. ## Autodesk Forma Connector Documentation: https://permute.ai/docs/connectors/autodesk Before connecting, a hub administrator must add the exact APS client ID shown by Permute under Hub Admin > Custom integrations. The direct Autodesk route contains the hub ID: https://acc.autodesk.com/account-admin/custom-integrations/accounts/. If the app is not added first, Permute cannot find the hub or sync its data. Autodesk shows Full Hub Access for this custom integration. Permute uses that access only to read the 13 listed tables and does not write to Autodesk: hubs, projects, companies, folders, items, versions, budgets, change_orders, cost_items, expenses, expense_items, contracts, and payments. ## Procore Connector Documentation: https://permute.ai/docs/connectors/procore The Procore connector reads data using a customer-owned Data Connection App with a Developer Managed Service Account (DMSA). The customer configures tool permissions, obtains Procore production access approval for live data, and installs the app version in the intended company. A company administrator grants permitted project access. Permute requires the environment, Client ID, and Client Secret. Developer Sandbox uses Sandbox credentials; Production and Monthly Sandbox use Production credentials. A Company ID may be required for company discovery with Multiple Procore Regions; it is not a company-selection filter. Users can select all available tables or choose tables manually. Automatic syncs use a six-hour schedule and refresh the selected data. Coverage depends on Procore tools, app permissions, permitted projects, selected tables, and endpoint limits. Attachment records contain file details and links, not file contents. Schedule summaries are not complete project schedules. The connector does not write to Procore or provide a complete history of edits and deletions. Check the last successful sync and any errors before using retained data as current. ## Amazon S3 Connector Documentation: https://permute.ai/docs/connectors/s3 The Amazon S3 connector assumes a customer-owned read-only IAM role using a server-issued external ID. The production trust principal is arn:aws:iam::474668386399:role/PermuteS3ConnectorAssumer. Permute limits each temporary session to the configured bucket paths and explicit customer-managed KMS keys. The customer role needs s3:ListBucket on selected bucket prefixes and s3:GetObject plus s3:GetObjectVersion on selected objects. Customer-managed KMS keys also require kms:Decrypt and a key policy that allows the role. Each mapping produces one governed table from CSV, TSV, pipe-separated, semicolon-separated, JSON, NDJSON, or Parquet objects. One record per file requires each matched object to produce exactly one row. Multiple records per file combines every row from every current match. Full snapshot file selects one object by S3 modification time or a YYYY-MM-DD or YYYYMMDD timestamp in its key. Each successful sync replaces the prior tables with current mapped state. Row lineage uses _source_file, _source_version, and _source_row, while _source_files records the selected object manifest. ## Acumatica Connector Documentation: https://permute.ai/docs/connectors/acumatica Sync Acumatica customers, orders, inventory, and financial records through OData. Use the full client ID. It includes an @Tenant suffix. The instance root, tenant, and connected application must match. Before you start: - The Acumatica instance root, such as https://erp.example.com/AcumaticaERP. - Access to Connected Applications (SM303010) in the target tenant. ### 1. Create a connected application Create an active Authorization Code application in Connected Applications (SM303010). Use a connected application in the Acumatica tenant you want to read. Its client ID, shared secret, and redirect URI must belong to the same application. The client ID includes the tenant after an @ sign, so copy the full value into Permute. Choose Authorization Code in Connected Applications (SM303010). Add Permute’s OAuth callback URI to that application. A different URI will stop authorization before Permute can connect. - Keep the application active. - Copy the secret when you generate it and store it securely. - If you use several tenants, check the tenant suffix before authorizing. Source: https://www.acumatica.com/media/2020/09/AcumaticaERP_IntegrationDevelopmentGuide.pdf ### 2. Set the redirect URI and secret Add https://api.permute.ai/oauth/callback under Redirect URIs and generate a shared secret. For other Permute environments, use the callback URL shown on the setup screen. ### 3. Set token and data access Use Sliding Expiration for refresh tokens. Grant only the access needed for read-only reporting. The application authorizes access through an Acumatica user. That user must be able to see the OData records Permute needs. If a customer, order, inventory, or financial record is missing, check the user’s access in Acumatica before changing the Permute connection. Grant only the access needed for reporting. In Acumatica, set Sliding Expiration for refresh tokens as the setup screen instructs. Source: https://www.acumatica.com/media/2020/09/AcumaticaERP_IntegrationDevelopmentGuide.pdf ### 4. Add Acumatica in Permute In your workspace, open Connectors, select Add Connector, choose Acumatica, and name the account. Under Sync which data?, choose: - Recommended: core tables. - Select manually: choose the tables Permute syncs after connecting. Connection details: - Instance URL: The root URL of the Acumatica instance, including its application path. - OAuth Client ID: The ID from Connected Applications, including @Tenant. - OAuth Client Secret: The shared secret from that application. - Enter the Instance URL, OAuth Client ID, and OAuth Client Secret. The client ID includes an @Tenant suffix. - Select Connect to authorize the application. ### 5. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 6. Check the first result 1. Choose a known customer or order in the intended Acumatica tenant. 2. Confirm the authorizing user can see it, then compare the connected data and selected table. Available data: - Customers and orders: Customer and order records available through the connected OData account. - Inventory and finance: Inventory and financial records available through that account. ### Understand Acumatica OData Acumatica places the DAC endpoint under the instance and tenant: /t//api/odata/dac. Its metadata lists DACs, fields, field types, and links to related DACs. The instance root and tenant suffix in the OAuth client ID are separate values to check during setup. Source: https://beacon.acumatica.com/r/Reporting-Tools-Guide/Accessing-DACs-Through-OData/DAC-Based-OData-General-Information?contentId=OaVbqQu6GzabTSBdScjckw ### Read related fields carefully Acumatica metadata describes links between DACs as navigation properties. For example, a sales order can link to order lines and to a customer account. An order header and its lines are distinct records; compare the same level of detail on both sides of a report. If a field is missing from a DAC-based OData result, inspect that tenant’s DAC metadata and the user’s access. The field may belong to a related DAC. Use the metadata field names rather than assuming that an Acumatica screen label is also the OData field name. Source: https://beacon.acumatica.com/r/Reporting-Tools-Guide/Accessing-DACs-Through-OData/DAC-Based-OData-General-Information?contentId=OaVbqQu6GzabTSBdScjckw Access and source details: ### Check the instance address Permute asks for the instance root, such as https://erp.example.com/AcumaticaERP. Use the address of the instance that contains the chosen tenant. Do not paste a screen URL from inside Acumatica; the OAuth app and OData requests need the instance root. If your company has test and live instances, prepare a connected application and credentials for the instance you select. Check the address, tenant suffix, and app together before retrying a failed connection. Source: https://www.acumatica.com/media/2020/09/AcumaticaERP_IntegrationDevelopmentGuide.pdf ### Review refresh-token settings The Permute setup screen calls for Sliding Expiration on refresh tokens. That setting lets the app renew access while the connection is used. If the connected application’s token policy or secret changes later, the saved authorization may need to be renewed. When access stops, confirm the connected application is active before changing the Permute account name or table choice. Those choices do not repair a revoked application or invalid secret. Source: https://www.acumatica.com/media/2020/09/AcumaticaERP_IntegrationDevelopmentGuide.pdf Troubleshooting: - Authorization stops: Check the instance root, callback URI, full client ID, secret, and application status. - Records are absent: Check the tenant, the authorizing user’s access, and the selected tables. References: - Acumatica integration guide: https://www.acumatica.com/media/2020/09/AcumaticaERP_IntegrationDevelopmentGuide.pdf - Acumatica DAC metadata: https://beacon.acumatica.com/r/Reporting-Tools-Guide/Accessing-DACs-Through-OData/DAC-Based-OData-General-Information?contentId=OaVbqQu6GzabTSBdScjckw ## Attio Connector Documentation: https://permute.ai/docs/connectors/attio Connect Attio CRM. Before you start: - An Attio workspace admin to authorize the connection. ### 1. Add Attio in Permute In your workspace, open Connectors, select Add Connector, choose Attio, and name the account. ### 2. Authorize Attio - Select Connect with OAuth. An Attio workspace admin must authorize Permute. The connection uses a workspace token with the approved app scopes. Select Connect with OAuth in Permute. The setup screen does not ask you to create or paste an Attio API key. Source: https://docs.attio.com/rest-api/guides/authentication #### Understand the access request Attio uses permissions for records and object configuration. For example, reading deal records requires record read access and object configuration read access. The data available to a connection depends on the access granted during authorization. If the expected CRM data is absent, check the required scopes and the source records in Attio. Source: https://docs.attio.com/rest-api/endpoint-reference/standard-objects/deals/list-deal-records ### 3. Check the first result 1. Choose a known record in the Attio workspace used for OAuth. 2. Note its object and attribute; if the value is on a list, note that list too. How Attio organizes records: - Object: A type of record, such as a person or company. A workspace can define custom objects too. - Attribute: A value stored on a record, such as a name or another field. - List: A set of records for a workflow; a list entry can have its own attributes. Source: https://attio.com/help/reference/attio-101/attios-data-model/understanding-attio-data-model ### Account for custom objects Attio includes standard objects and lets teams define custom objects for their own work. A custom object can have attributes and relationships to other objects. Its name and fields come from that workspace’s data model, so another Attio workspace may look different. If a team refers to a custom deal stage or other field, first locate the object and attribute in the connected Attio workspace. Check whether the value lives on an object record or a list entry. Source: https://attio.com/help/reference/attio-101/attios-data-model/define-your-data-model-objects-lists-and-views Access and source details: ### Check Attio workspace access Permute uses a workspace token. Its approved scopes govern access independently of the authorizing member’s permissions. If expected records are absent, check the connected workspace and the app’s approved scopes. Source: https://docs.attio.com/rest-api/guides/authentication Troubleshooting: - An object is absent: Check the connected workspace and the app’s approved read scopes. - A field is absent: Find whether the value is on the object record or a list entry. References: - Attio authentication: https://docs.attio.com/rest-api/guides/authentication - Attio deal API permissions: https://docs.attio.com/rest-api/endpoint-reference/standard-objects/deals/list-deal-records - Attio objects and lists: https://attio.com/help/reference/attio-101/attios-data-model/define-your-data-model-objects-lists-and-views - Attio data model: https://attio.com/help/reference/attio-101/attios-data-model/understanding-attio-data-model ## BigQuery Connector Documentation: https://permute.ai/docs/connectors/bigquery Connect BigQuery for live queries and schema discovery. The JSON file is a private key. Download it when Google Cloud creates it. If key creation is blocked, ask your Google Cloud administrator to review the policy. Before you start: - The Google Cloud project containing the BigQuery data to connect. - Permission to create service account keys. ### 1. Create a service account In Google Cloud Console, select the project containing the datasets Permute should discover. Open Service Accounts and create a service account in that project. Google Cloud Service Accounts: https://console.cloud.google.com/iam-admin/serviceaccounts ### 2. Grant BigQuery access Grant BigQuery Data Viewer on the datasets to read and BigQuery Job User on that same project. Grant BigQuery Data Viewer on each dataset Permute should read. It allows data access and table discovery. Grant BigQuery Job User on the service account’s project. For narrower access, grant Data Viewer on individual tables or views and Metadata Viewer on their datasets. Table access alone does not let Permute discover them. Source: https://docs.cloud.google.com/bigquery/docs/access-control ### 3. Create the JSON key Open the service account’s Keys tab. Select Add Key → Create new key → JSON. In Google Cloud IAM, open the service account, then Keys → Add key → Create new key → JSON. Google downloads a private key file. Upload that file in Permute’s setup screen. Treat it as a secret and do not share it in chat or a public repository. Your organization may block service account key creation. If the Add key action is unavailable, ask a Google Cloud administrator about the key-creation policy before you continue. Source: https://docs.cloud.google.com/iam/docs/keys-create-delete ### 4. Add BigQuery in Permute In your workspace, open Connectors, select Add Connector, choose BigQuery, and name the account. Connection details: - Service account: Create it in the project containing the datasets Permute should discover. - BigQuery Job User: Grant on the service account’s project. - BigQuery Data Viewer: Grant on the datasets to read. Table-only grants also need Metadata Viewer on their datasets. - JSON key: Download from the service account’s Keys tab and upload in Permute. - Select I have my keys. - Upload the JSON service account key file, then select Connect. ### 5. Check the first result 1. Choose a known dataset and table that the service account may read. 2. Check that its schema is visible, then run a read-only query against that table. Available data: - Schema discovery: Inspect the BigQuery data visible to the service account. - Live queries: Query data within the service account’s BigQuery grants. ### Choose the grant boundary A dataset-level Data Viewer grant supports discovery and reading its tables and views. To limit row access to selected tables or views, grant Data Viewer on those resources and Metadata Viewer on their datasets. Grant Job User on the project where queries run. When adding a dataset, check both discovery and read permissions for the service account. Source: https://docs.cloud.google.com/bigquery/docs/access-control Access and source details: ### Replace a service-account key A JSON key is a private credential for the service account. Google Cloud lets administrators create and delete keys. If a key is exposed, create a replacement in Google Cloud and retire the old one under your organization’s key policy. If your organization disables key creation, the Permute setup screen asks you to work with an administrator. Do not upload a key from a different service account merely to get past that policy; its IAM grants may point to different data. Source: https://docs.cloud.google.com/iam/docs/keys-create-delete Troubleshooting: - Cannot create a key: Check the service-account key creation policy with a Google Cloud administrator. - A query is denied: Check Job User on the query project and Data Viewer on the referenced data. References: - BigQuery access control: https://docs.cloud.google.com/bigquery/docs/access-control - Google Cloud key management: https://docs.cloud.google.com/iam/docs/keys-create-delete - BigQuery query requirements: https://docs.cloud.google.com/bigquery/docs/running-queries - Google Cloud Service Accounts: https://console.cloud.google.com/iam-admin/serviceaccounts ## Bill.com Connector Documentation: https://permute.ai/docs/connectors/billcom Sync Bill.com bills, vendors, invoices, customers, and payments. Keep the environments separate. Production and Staging use different accounts and developer credentials. Before you start: - A Bill.com login email and password. - A Developer Key and Organization ID for the chosen environment. ### 1. Open Bill.com developer settings In Bill.com, open Sync & Integrations → Manage Developer Keys. ### 2. Copy the key and organization ID Generate a Developer Key and copy the Organization ID shown at the bottom of that page. #### Find the key and organization In Bill.com, open Sync & Integrations → Manage Developer Keys. Generate a Developer Key for the environment you will connect. The Organization ID appears at the bottom of that page. Copy both values before opening Permute. Production and sandbox have separate credentials. A production key cannot stand in for a staging key, so set Permute’s environment to match the Bill.com account and key. Source: https://developer.bill.com/v2/docs/sign-up-process #### Use the matching Bill.com login Bill.com API sign-in uses a username, password, Organization ID, and Developer Key together. Enter a user who can access the organization identified by the Organization ID. Keep the environment consistent across the credentials. Bill.com sessions expire after inactivity. If sign-in keeps failing, check the current login credentials. Source: https://developer.bill.com/docs/api-sandbox-sign-in ### 3. Add Bill.com in Permute In your workspace, open Connectors, select Add Connector, choose Bill.com, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. Connection details: - Environment: Choose Production or Staging for the Bill.com account. - Username and Password: Use a login for the chosen organization and environment. - Developer Key: Copy it from Sync & Integrations → Manage Developer Keys. - Organization ID: Copy it from the same Bill.com environment. - Choose Production or Staging (sandbox) to match the Bill.com account. - Enter Username, Password, Developer Key, and Organization ID. Select Connect. ### 4. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 5. Check the first result 1. Choose a bill in the selected Bill.com organization and environment. 2. Compare its vendor, amount, due date, and payment status with the source bill. Available data: - Payables: Bills and vendors. - Receivables and payments: Invoices, customers, and payments. ### Distinguish bills from customer invoices In BILL, a bill records money owed to a vendor for goods or services. Its vendor ID links it to the payee; due date and bill line items describe when and why payment is owed. The bill can also carry the vendor’s invoice number and date. When comparing bill data, use the bill ID and vendor rather than invoice number alone. An invoice number on a bill comes from the vendor’s document. The setup screen also names customer invoices as a separate group, so confirm which side of the transaction you are checking. Source: https://developer.bill.com/docs/ap-bills ### Check bill detail and status BILL bills can include several line items. A line has an amount and may use quantity and price to calculate it. The bill response also includes payment and approval status. Compare the bill total with the line items, and check status before treating an unpaid bill as a missing payment. Use a known bill in the organization you entered in Permute. Confirm its vendor, due date, amount, and status in BILL first. This is more precise than checking only whether a table has rows. Source: https://developer.bill.com/docs/ap-bills Access and source details: ### Keep sandbox and production separate Bill.com issues developer credentials for each environment. Its sandbox is for testing and its production account holds live business data. Select Staging only when the login, Developer Key, and Organization ID are for the sandbox. A successful sandbox test does not connect live data. When you move to production, repeat setup with the production organization and key. Source: https://developer.bill.com/v2/docs/sign-up-process Troubleshooting: - Sign-in is rejected: Check all four credentials and the selected environment together. - Live records are absent: Check whether Permute is connected to Staging instead of Production. References: - Bill.com developer signup: https://developer.bill.com/v2/docs/sign-up-process - Bill.com API sign-in: https://developer.bill.com/docs/api-sandbox-sign-in - Bill.com authentication keys: https://developer.bill.com/docs/bill-keys-tokens - BILL bills guide: https://developer.bill.com/docs/ap-bills ## Brex Connector Documentation: https://permute.ai/docs/connectors/brex Sync Brex transactions, expenses, budgets, vendors, and team data. Choose read scopes when creating the token. A token without the scope for a data group cannot read that group. User tokens expire after 90 days without API use. Before you start: - A Brex user token with read scopes for the data to sync. ### 1. Open Brex Developer settings Open Brex Dashboard → Developer and select Create token. Brex Developer settings: https://dashboard.brex.com/p/settings/developer ### 2. Choose read scopes Grant the read scopes you want Permute to sync, such as Team, Transactions, Expenses, Budgets, or Vendors. Create a user token in Brex Dashboard → Developer. Brex asks you to select scopes when you create it. Include read scopes for each data group you want in Permute, such as transactions, expenses, budgets, vendors, or team data. A token carries the scopes chosen for it. If a data group is missing, inspect those scopes. Source: https://developer.brex.com/guides/authentication ### 3. Copy the user token Copy the user token. Brex shows the full token when you create it, then hides part of it in the dashboard. Copy it directly into Permute’s API Key field. If you lose the full value, create another token. Revoke a token in Brex when it is no longer needed or may have been exposed. Calls using a revoked token fail at once. Source: https://developer.brex.com/guides/authentication ### 4. Add Brex in Permute In your workspace, open Connectors, select Add Connector, choose Brex, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the token into API Key and select Connect. ### 5. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 6. Check the first result 1. Choose a Brex transaction or expense that exists in the account tied to the token. 2. Compare its data group with the token’s read scope before checking other groups. Available data: - Spending: Transactions, expenses, and budgets. - People and payees: Team and vendor data. ### Match scopes to data groups Brex lists separate read scopes for budgets, card expenses, vendors, users, and card or cash transactions. For example, budgets.readonly permits budget data; expenses.card.readonly permits card expense data; vendors.readonly permits vendor data. Transaction scopes distinguish card from cash transactions. Choose the scopes for the groups named on the Permute setup screen. A token with only card transaction access does not also grant budget or vendor access. If one group is missing while others sync, compare its specific read scope with the token you created. Source: https://developer.brex.com/guides/roles_permissions_scopes Access and source details: ### Plan for token ownership A Brex user token belongs to the person who made it. Brex stops accepting the token when that user becomes inactive. Choose an appropriate user for a long-lived connection and review ownership before that person’s status changes. The Developer page shows created tokens with part of each value hidden. Keep a record of which token serves Permute so you can revoke or replace the correct one. Source: https://developer.brex.com/guides/authentication Troubleshooting: - One group is absent: Check that the token has its Brex read scope. - A working token stops: Check whether it expired or its user became inactive. References: - Brex user-token guide: https://developer.brex.com/guides/authentication - Brex scopes: https://developer.brex.com/guides/roles_permissions_scopes - Brex Developer settings: https://dashboard.brex.com/p/settings/developer ## Double Connector Documentation: https://permute.ai/docs/connectors/double Sync Double clients, contacts, emails, activity, and close data. Copy the secret when the key is created. Double shows the Client Secret once. A Super Admin must generate the key. Before you start: - A Client ID and Client Secret from a Double API key. ### 1. Open Double practice settings Open Double → Practice Settings → General. Double limits API key creation to Super Admins. In Practice Settings → Practice Management → General, select Generate API Key. The key gives a Client ID and Client Secret; Permute needs both. If the action is missing, check the Double role of the person preparing the connection. A regular user cannot create the key on behalf of the practice. Source: https://help.doublehq.com/en/articles/12713717-double-api-key ### 2. Generate and copy the API key Select Generate API Key and copy the Client ID and Client Secret. Double displays the Client Secret once when the key is generated. Copy it with the Client ID before closing that screen, then paste both in Permute. The Client ID identifies the key; the secret authenticates it. If a copied value fails, check that the ID and secret came from the same generated key. Keep the secret in a secure location and replace a compromised key. Source: https://help.doublehq.com/en/articles/12713717-double-api-key ### 3. Add Double in Permute In your workspace, open Connectors, select Add Connector, choose Double, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. Connection details: - Client ID: Copy from the key generated in Practice Settings → General. - Client Secret: Copy from the same key when Double shows it. - Enter the Client ID and Client Secret in Permute, then select Connect. ### 4. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 5. Check the first result 1. Choose a known client in Double and note a linked contact. 2. Compare those two records separately; a contact can belong to several clients. Available data: - Clients and contacts: Business clients and the people linked to them. - Practice work: Emails, activity, and close data named on the setup screen. ### Separate clients and contacts Double uses client records for the businesses a practice serves and contact records for people at those businesses. A contact can be linked to more than one client. A client name and a contact name therefore identify different things. When checking the clients and contacts named on the Permute setup screen, find the client first, then the related contact. If one person appears under two clients, check the links in Double before treating the second appearance as a duplicate sync. Source: https://help.doublehq.com/en/articles/6980924-client-vs-client-portal-contact ### Understand email history Double can keep email history for messages sent from Double to client contacts. Where a contact is linked to several clients, the same email may be visible in more than one client context. Compare the message and contact before counting such entries as separate conversations. The Permute setup screen lists emails and activity among the data groups. When investigating an absent email, first check whether it exists in Double’s email history for the expected contact and client. Source: https://help.doublehq.com/en/articles/6345317-email-history Troubleshooting: - Generate API Key is missing: Ask a Double Super Admin to create the key pair. - The connection is rejected: Check that Client ID and Client Secret came from the same key. References: - Double API key instructions: https://help.doublehq.com/en/articles/12713717-double-api-key - Double roles and permissions: https://help.doublehq.com/en/articles/6060398-manage-users-permissions-roles - Double clients and contacts: https://help.doublehq.com/en/articles/6980924-client-vs-client-portal-contact - Double email history: https://help.doublehq.com/en/articles/6345317-email-history ## Fireflies Connector Documentation: https://permute.ai/docs/connectors/fireflies Sync Fireflies meeting details, transcripts, and AI summaries. Check the source transcript. Fireflies recording settings can remove transcript text after processing. Check whether the source meeting still has a transcript. Before you start: - Access to the Fireflies account that holds the meetings to connect. ### 1. Open Fireflies API settings Open Fireflies → Integrations → Fireflies API. Fireflies API: https://app.fireflies.ai/integrations/custom/fireflies Open Fireflies Integrations → Fireflies API and select Get API Key. Copy the key from Developer settings into Permute. Keep it private and replace it if exposed. Fireflies describes its API as a way to access meeting records, transcripts, and summaries. Permute’s setup screen identifies those as the data this connector syncs. Source: https://guide.fireflies.ai/articles/3737786777-learn-about-fireflies-api ### 2. Copy the API key Select Get API Key, then copy the key from Developer settings. The key works within the Fireflies account that issued it. If a meeting or transcript is absent, confirm that the account can see that meeting and that Fireflies has processed it. Check the Fireflies source before changing the connector. Source: https://guide.fireflies.ai/articles/3737786777-learn-about-fireflies-api ### 3. Add Fireflies in Permute In your workspace, open Connectors, select Add Connector, choose Fireflies, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the key into API Key and select Connect. ### 4. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 5. Check the first result 1. Choose a completed Fireflies meeting with a transcript and summary. 2. Compare its title, date, transcript, and summary with the source meeting. Available data: - Meeting details: Title, date, duration, participants, and a link to the Fireflies transcript. - Transcript: Meeting text when Fireflies retains it. - AI summary: The summary generated for the meeting. ### Know what the meeting retains Fireflies recording settings determine whether a meeting retains audio, a transcript, and a summary. For example, Transcript + Summary keeps text but deletes the recording after processing. Summary Only keeps the summary and deletes the transcript and recording. The available modes depend on the Fireflies plan. If Permute shows a summary but no transcript, check the meeting’s recording format in Fireflies. A key cannot retrieve text that Fireflies did not retain. Source: https://guide.fireflies.ai/articles/3598930706-set-up-transcript-only-mode-for-compliance ### Use a known transcript for validation Permute imports a meeting’s title, date, duration, participants, transcript text with speaker names, AI summary, and source transcript link. Pick a completed meeting and compare those details with Fireflies. Audio and video recording files are not imported. Use the source link to open the meeting in Fireflies. Source: https://guide.fireflies.ai/articles/3737786777-learn-about-fireflies-api Troubleshooting: - API key is rejected: Copy the key from the Fireflies account you intend to connect. - Transcript text is absent: Check the source meeting’s processing state and recording format in Fireflies. References: - Fireflies API guide: https://guide.fireflies.ai/articles/3737786777-learn-about-fireflies-api - Fireflies recording modes: https://guide.fireflies.ai/articles/3598930706-set-up-transcript-only-mode-for-compliance - Fireflies API: https://app.fireflies.ai/integrations/custom/fireflies ## Google Ads Connector Documentation: https://permute.ai/docs/connectors/googleads Sync Google Ads account data, performance, and reporting breakdowns. The first sync includes all available tables. After it finishes, use Select manually to limit later syncs. Before you start: - A Google account with access to the advertisers you want to import. ### 1. Add Google Ads in Permute In your workspace, open Connectors, select Add Connector, choose Google Ads, and name the account. ### 2. Authorize Google Ads - Select Connect with OAuth. - Sign in with the Google account used for Google Ads and grant access. Use the Google account that can see the advertisers you want in Permute. Google Ads access follows that account’s direct advertiser access and its manager-account links. You do not enter a developer token in the Permute setup screen; its Connect action starts Google OAuth. If your team has more than one Google identity, check which account is signed in before approving access. A successful OAuth connection cannot read an advertiser the chosen identity cannot access. Source: https://developers.google.com/google-ads/api/docs/oauth/access-model #### Review advertiser coverage Permute’s setup screen says it imports all advertisers available to the authorized Google account, including those under manager accounts. It also says advertisers granted later join future syncs. Review that account’s manager links when you want to change which advertisers appear. The first sync starts with all available tables. After it completes, open the connector and choose Select manually if you want fewer tables in later syncs. Source: https://developers.google.com/google-ads/api/docs/oauth/access-model ### 3. Check the first result 1. List the advertisers visible to the Google identity used for OAuth. 2. Choose one advertiser and compare an available report after the first sync. Available data: - Advertisers: Accounts reached directly or through manager accounts. - Reports: Account performance and reporting breakdowns. ### Check new advertiser access An advertiser linked to a manager account becomes reachable only when the signed-in identity has access through that manager. Google Ads also allows direct advertiser access. These paths can yield different account lists for two Google identities in the same company. The setup screen says newly accessible advertisers join later syncs. If a new one has not appeared, confirm the authorized identity can open it in Google Ads, then allow for a later sync. A Permute table choice cannot grant advertiser access. Source: https://developers.google.com/google-ads/api/docs/oauth/access-model ### Separate accounts from reporting tables The first sync starts with all available Google Ads tables. After it completes, the setup screen allows Select manually for later syncs. That table choice controls which data is refreshed; the Google identity and account links control which advertisers can be reached. When investigating a gap, name the advertiser and the report group separately. If every group for one advertiser is absent, review Google Ads account access. If one group is absent across advertisers, review the table choice and the source reporting data. Source: https://developers.google.com/google-ads/api/docs/oauth/access-model Troubleshooting: - An advertiser is absent: Check that the authorized Google identity can access it in Google Ads. - A report group is absent: Check its selected table after the first sync and the source reporting data. References: - Google Ads access model: https://developers.google.com/google-ads/api/docs/oauth/access-model ## Granola Connector Documentation: https://permute.ai/docs/connectors/granola Sync Granola meeting notes, summaries, folders, and transcripts. A note must have a summary and transcript. Granola exposes it through the API only after both parts are ready. Before you start: - A Granola Business or Enterprise plan for API access. - A Personal or Workspace API key; Workspace keys are recommended for team syncs. ### 1. Open Granola connector settings In the Granola desktop app, open Settings → Connectors. Granola API setup guide: https://docs.granola.ai/help-center/sharing/integrations/granola-api ### 2. Choose the API key scope Create a Personal or Workspace API key. For a Personal key, choose the note access Permute may sync. Granola offers Personal keys and Workspace keys on Business and Enterprise plans. A Personal key follows one user’s note access. A Workspace key is managed for the workspace and is the better fit for a team connection. For a Personal key, choose its note scope before copying it. Personal notes and Public notes are different access choices. A Workspace key cannot read private notes that have not been shared with the workspace. Source: https://docs.granola.ai/help-center/sharing/integrations/granola-api ### 3. Copy the key Copy the key. Granola makes a note available through its API after both a summary and transcript exist. Notes still being processed, and manual notes without that material, do not appear through the API. Check the note in Granola before troubleshooting a missing Permute row. Personal keys follow their selected note scope. Workspace keys can read public notes and notes in spaces with API access enabled; they cannot read unshared private notes. Source: https://docs.granola.ai/help-center/sharing/integrations/granola-api ### 4. Add Granola in Permute In your workspace, open Connectors, select Add Connector, choose Granola, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the key into API Key and select Connect. ### 5. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 6. Check the first result 1. Choose a Granola note with both summary and transcript complete. 2. Check that the key’s Personal or Workspace scope includes that note. Available data: - Meeting content: Notes, summaries, and transcripts. - Organization: Folders within the key’s permitted note scope. ### Choose a key for the note scope Granola Personal keys and Workspace keys have different reach. A Personal key can use its selected personal or public note scope. A Workspace key can reach public notes and notes in spaces where API access is enabled; it does not reach unshared private notes. Use a Personal key when the intended notes belong to one person’s allowed scope. Use a Workspace key for a team collection whose sharing and space settings allow API access. Check the scope in Granola before treating a private note as a failed sync. Source: https://docs.granola.ai/help-center/sharing/integrations/granola-api ### Wait for a complete note Granola exposes a note through its API only after it has both an AI summary and a transcript. A manual note or a meeting still being processed can appear in the app before it qualifies for the API. For a first check, choose a meeting note with both parts complete. Confirm that the key’s scope includes it, then compare the note title and content. If a newer note is absent, inspect its processing state in Granola before changing the key. Source: https://docs.granola.ai/help-center/sharing/integrations/granola-api ### Review space and key changes Workspace keys do not have a fixed expiry in Granola. Access still depends on which spaces allow the API and how notes are shared. A space setting or note-sharing change may alter what the same key can read. Granola lets an authorized user revoke a key. If a previously working connection loses its notes, check whether the key still exists and whether the notes remain inside its scope. Use one known public or enabled-space note to separate a key failure from a sharing change. Source: https://docs.granola.ai/help-center/sharing/integrations/granola-api Troubleshooting: - A private note is absent: Check whether the Personal or Workspace key can read it. - A new note is absent: Check that Granola finished its summary and transcript. References: - Granola API setup guide: https://docs.granola.ai/help-center/sharing/integrations/granola-api ## HubSpot Connector Documentation: https://permute.ai/docs/connectors/hubspot Connect HubSpot CRM. Check app installation access. A HubSpot administrator may limit who can install or approve connected apps. Before you start: - A HubSpot identity that can access the account you want to authorize. ### 1. Add HubSpot in Permute In your workspace, open Connectors, select Add Connector, choose HubSpot, and name the account. ### 2. Authorize HubSpot - Select Connect with OAuth. Select Connect with OAuth in Permute, then choose the HubSpot account whose CRM data you need. HubSpot asks the connecting user to approve the app’s requested access. Read that consent screen and check the account name before continuing. Source: https://developers.hubspot.com/docs/guides/apps/public-apps/overview?offset=0 #### Review the app connection HubSpot lists authorized apps under Settings → Integrations → Connected Apps. Open the app there to review connection details and permissions. This is also where an account admin can manage or remove access. If the connection succeeded but expected CRM data is absent, check the account selected during OAuth and the app’s approved permissions in HubSpot. Source: https://knowledge.hubspot.com/integrations/manage-your-connected-apps ### 3. Check the first result 1. Confirm the HubSpot account selected on the OAuth approval screen. 2. For a known CRM value, note its object, record, property, and association in that account. How HubSpot organizes CRM data: - Object: A record type, such as a contact, company, deal, or ticket. - Property: A field on one record in that object. - Association: A link between records in the same object or different objects. Source: https://knowledge.hubspot.com/records/understand-objects ### Locate a CRM field HubSpot groups CRM records into objects such as contacts, companies, deals, and tickets. A record’s properties store its field values, while associations connect it to records in other objects. A company name on an associated deal is not necessarily a property of the deal itself. For a missing value, note the object, record, property, and any association in HubSpot. Then compare that exact source location with the connected data. Custom objects and properties can differ by account, so use the authorized account as the reference. Source: https://knowledge.hubspot.com/records/understand-objects Access and source details: ### Check the HubSpot account before consent HubSpot can limit who installs approved apps. If a user is blocked, an admin can review that user’s app access in Connected Apps. If your organization restricts installation to named users or teams, a Super Admin can review those controls under Connected Apps. Ask for that review when a valid HubSpot login is blocked. Source: https://knowledge.hubspot.com/integrations/manage-access-to-apps Troubleshooting: - OAuth approval is blocked: Ask a HubSpot Super Admin to review Connected Apps access. - A CRM value is absent: Find its object, record, property, and any association in HubSpot. References: - HubSpot OAuth account selection: https://developers.hubspot.com/docs/guides/apps/public-apps/overview?offset=0 - HubSpot connected apps: https://knowledge.hubspot.com/integrations/manage-your-connected-apps - HubSpot app governance: https://knowledge.hubspot.com/integrations/manage-access-to-apps - HubSpot CRM objects: https://knowledge.hubspot.com/records/understand-objects ## Klaviyo Connector Documentation: https://permute.ai/docs/connectors/klaviyo Sync Klaviyo profiles, lists, segments, campaigns, and flows. Review the read scopes on the approval screen. Klaviyo has separate read scopes for profiles, lists, segments, campaigns, and flows. Before you start: - A Klaviyo account that can approve the requested OAuth read scopes. ### 1. Add Klaviyo in Permute In your workspace, open Connectors, select Add Connector, choose Klaviyo, and name the account. ### 2. Authorize Klaviyo - Select Connect with OAuth. Permute starts Klaviyo OAuth when you select Connect with OAuth. Sign in to the account that holds the profiles and marketing data you want. Review the account and requested scopes on Klaviyo’s approval screen. The setup screen does not ask for a private API key. Klaviyo supports both private keys and OAuth, but this Permute connection uses OAuth. Source: https://developers.klaviyo.com/en/v2024-06-15/docs/set_up_oauth #### Understand read scopes Klaviyo uses scopes to decide which API records an app may read. Profile data needs profiles:read; lists, segments, campaigns, and flows have their own read scopes. The connection can only access the data allowed by the approved OAuth scopes. If one data group is missing, check the approved read scopes and the account’s data. Klaviyo lists separate read and write scopes; review the exact scopes requested on its approval screen. Source: https://developers.klaviyo.com/en/v2026-01-15/docs/authenticate_ ### 3. Check the first result 1. Find a known Klaviyo profile in the account you approved. 2. If checking a list or segment, verify the profile’s membership in that source group. Available data: - Audience: Profiles, lists, and segments. - Marketing: Campaigns and flows. ### Read the Klaviyo data model A Klaviyo profile represents a person. Lists hold chosen memberships; segments use rules to include profiles whose data matches them. A profile may exist without belonging to a particular list or segment. Campaigns are a separate marketing group. When comparing data, identify whether you expect the profile, its list membership, or a segment result. A changing segment count can reflect its rules or profile data rather than a connector issue. Check the source group in the Klaviyo account approved during OAuth. Source: https://developers.klaviyo.com/en/docs/introduction_to_klaviyos_data_model ### Check the profile before its groups Klaviyo’s Profiles API retrieves profile records under profiles:read. A list or segment then describes membership for profiles. If a named person is absent from a list, search for the profile first, then inspect the list or segment in Klaviyo. This check separates a missing person from a membership difference. It also helps when the same person has changed details: the profile record and a dynamic segment can change at different times as Klaviyo evaluates its rules. Source: https://developers.klaviyo.com/en/reference/get_profiles Troubleshooting: - One group is absent: Check the group’s read scope and that it contains records in Klaviyo. - A person is absent from a list: Find the profile first, then check its list membership. References: - Klaviyo OAuth setup: https://developers.klaviyo.com/en/v2024-06-15/docs/set_up_oauth - Klaviyo API scopes: https://developers.klaviyo.com/en/v2026-01-15/docs/authenticate_ - Klaviyo Profiles API: https://developers.klaviyo.com/en/reference/get_profiles - Klaviyo data model: https://developers.klaviyo.com/en/docs/introduction_to_klaviyos_data_model ## Mercury Connector Documentation: https://permute.ai/docs/connectors/mercury Sync Mercury banking, card, credit, treasury, and transaction data. Use a read-only token. Use a read-only token or a custom token with only the read scopes needed for the data. Before you start: - A read-only Mercury API token for Production or Test account. ### 1. Open Mercury API settings In Mercury, open Settings → API Tokens. ### 2. Create a read-only token Create a read-only token, or a custom token with only read scopes. Copy it. #### Select a read-only token Mercury offers read-only, read/write, and custom API tokens. Use read-only for Permute, or a custom token limited to the read access you need. A read/write token can initiate transactions or manage recipients, which the connection does not require. Choose Production for a real account or Test account for a test token in Permute. The environment and token must match. Source: https://docs.mercury.com/docs/getting-started #### Save and protect the token Mercury shows a generated token once. Copy it before closing the dialog and paste it into Permute’s API Key field. Do not save it in source control or send it in a message. If a token is exposed, revoke it in Mercury and create a new one. A lost token must also be replaced because Mercury will not display it again. Source: https://docs.mercury.com/docs/getting-started ### 3. Add Mercury in Permute In your workspace, open Connectors, select Add Connector, choose Mercury, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Choose Production for real data or Test account for a sandbox token. - Paste the matching token into API Key and select Connect. ### 4. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 5. Check the first result 1. Choose a known Mercury account or transaction in the selected environment. 2. For a custom token, confirm its read permissions cover that group. Available data: - Banking: Banking and transaction data. - Other products: Card, credit, and treasury data. ### Keep test and production apart Permute has Production and Test account choices for Mercury. Use a token made in the corresponding Mercury environment. A working test connection only shows test-account data; it does not prove access to live banking records. Pick one known account or transaction in the chosen environment and compare it after connecting. If the token is accepted but the record belongs to the other environment, return to the environment choice and token source before changing table selection. Source: https://docs.mercury.com/docs/getting-started Access and source details: ### Recover from a lost token Mercury does not reveal a token again after its creation dialog closes. Create a new token in the right Production or Test account and revoke the old token if it is no longer needed. Do not store the token in a public file. Mercury warns that anyone with the token can use its assigned API privileges. Source: https://docs.mercury.com/docs/getting-started Troubleshooting: - The token is rejected: Check that it belongs to the selected Production or Test account. - One group is absent: Check the custom token’s read permissions for that group. References: - Mercury API getting started: https://docs.mercury.com/docs/getting-started ## Meta Ads Connector Documentation: https://permute.ai/docs/connectors/meta_ads Sync Meta campaign structure and daily ad performance. Assign the app and ad accounts before generating the token. The dedicated system user needs Full access to the app and View performance on each intended ad account. Before you start: - A customer-owned Meta app, a dedicated system user, and ad account IDs for this customer. ### 1. Create a Meta app In the customer Business Portfolio, open Business Settings → Apps. Select Add → Create a new app ID. Meta Business Settings: https://business.facebook.com/latest/settings/apps ### 2. Choose Marketing API and check ads_read Choose Create & manage ads with Marketing API and confirm ads_read is Ready for testing. ### 3. Create a system user Under System users, add a dedicated Employee user. Assign the app with Full access. ### 4. Grant ad account access Assign each intended ad account with Partial access limited to View performance. Use a customer-owned Meta app and a dedicated system user for this connection. Give the system user the app and only the ad accounts Permute should read. The setup screen asks for ads_read and View performance rather than ad-management access. Enter each assigned ad account ID in Permute. An ID can include or omit the act_ prefix. The list is an explicit limit on the accounts you connect. Source: https://www.postman.com/meta/facebook-marketing-api/documentation/0zr4mes/facebook-marketing-api-mapi ### 5. Generate a token Generate a token for the app with expiration Never and only ads_read. Copy it immediately. The form needs the Meta App ID, App Secret, system-user access token, and ad account IDs. Copy the ID and secret from the same app that was assigned to the system user. Generate its token with ads_read, then copy it before closing the token dialog. Treat the App Secret and token as passwords. If the token is exposed, replace it in Meta. Source: https://www.postman.com/meta/facebook-marketing-api/documentation/0zr4mes/facebook-marketing-api-mapi ### 6. Add Meta Ads in Permute In your workspace, open Connectors, select Add Connector, choose Meta Ads, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. Connection details: - App ID and App Secret: Copy from the customer-owned Meta app. - System user token: Generate for that app with only ads_read. - Ad account IDs: Enter the assigned IDs, separated by commas. The act_ prefix is optional. - Copy the App ID and App Secret from the app settings. - Enter the App ID, App Secret, system user token, and assigned ad account IDs. Separate IDs with commas; the act_ prefix is optional. - Select Connect. ### 7. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 8. Check the first result 1. Check one ad account ID against the assigned IDs in Meta Business Settings. 2. Compare an available day of performance for that account after connecting. Available data: - Campaign structure: Campaign structure named on the setup screen. - Performance: Daily ad performance. ### Check the explicit account list Permute asks for ad account IDs separated by commas. You may include or omit the act_ prefix. Only enter IDs assigned to the dedicated system user. A typo in one ID can leave that account out even if the others connect. When one advertiser is absent, compare its ID in Meta with the Permute form and check View performance on that account. If all accounts fail, check the app, secret, system-user token, and ads_read setup together. Source: https://business.facebook.com/latest/settings/apps Troubleshooting: - No permissions available: Check the Marketing API use case, app assignment, and ad-account assignments. - An account is absent: Check its ID and View performance assignment for the system user. References: - Meta Marketing API collection: https://www.postman.com/meta/facebook-marketing-api/documentation/0zr4mes/facebook-marketing-api-mapi - Meta Business Settings: https://business.facebook.com/latest/settings/apps ## Microsoft Advertising Connector Documentation: https://permute.ai/docs/connectors/microsoft_advertising Sync Microsoft Advertising records and daily or hourly performance reports. Production reports have a date delay. Report history ends two UTC dates before each daily sync. Before you start: - A Microsoft identity that can access the advertiser accounts you want to analyze. ### 1. Add Microsoft Advertising in Permute In your workspace, open Connectors, select Add Connector, choose Microsoft Advertising, and name the account. ### 2. Authorize Microsoft Advertising - Select Connect with OAuth. - Sign in through Permute’s Microsoft OAuth application and grant Microsoft Advertising and offline access. Sign in with a Microsoft identity that can access the advertiser accounts you want. Permute’s OAuth screen requests Microsoft Advertising access and offline access so daily syncs can continue after the browser session ends. If an advertiser is absent, check the accounts available to that Microsoft identity. Completing OAuth does not add advertising-account access to the user. Source: https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-consent?view=bingads-13 #### Read report dates correctly The setup screen says Permute refreshes account records and available daily or hourly reports once each day. Production report history ends two UTC dates before each sync. A newer report may therefore be missing even when the connection is healthy. All tables begin syncing after authorization. After the first sync, choose Select manually in the connector if you need fewer tables. Source: https://learn.microsoft.com/en-us/advertising/guides/sdk-reporting-service-manager?view=bingads-13 ### 3. Check the first result 1. Confirm a known advertiser is visible to the Microsoft identity used for OAuth. 2. Compare a report dated before the two-UTC-date production cutoff. Available data: - Accounts: Advertiser account records. - Reports: Available daily and hourly performance history. ### Allow for report availability The Permute setup screen says account records and available daily and hourly reports sync once a day. Production report history ends two UTC dates before each sync. A report for yesterday can therefore be absent even when account records are current. Compare the report date in UTC with that cutoff before treating a gap as a failure. Microsoft’s reporting service handles report generation separately from account access, so an accessible advertiser does not mean every recent report is ready at the same time. Source: https://learn.microsoft.com/en-us/advertising/guides/sdk-reporting-service-manager?view=bingads-13 ### Choose tables after the first sync All available tables begin syncing after OAuth. Once that first sync ends, the setup screen allows Select manually to limit later syncs. This table choice affects which report groups Permute refreshes; it does not change the Microsoft identity’s account permissions. When a report group is absent, check the selected tables and its report date. When every group for one advertiser is absent, check account access under the authorized Microsoft identity. These are different causes and need different fixes. Source: https://learn.microsoft.com/en-us/advertising/guides/sdk-reporting-service-manager?view=bingads-13 Access and source details: ### Know what offline access does Microsoft Advertising OAuth asks for offline access so the approved connection can obtain new access tokens after the initial browser session. This supports the daily refresh described in Permute’s setup screen. If a user or administrator revokes that consent, return to Permute and authorize the connector again with an identity that still has access to the intended advertiser accounts. Source: https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-consent?view=bingads-13 Troubleshooting: - An advertiser is absent: Check its access for the authorized Microsoft identity. - A recent report is absent: Compare its UTC date with the two-date cutoff and check table selection. References: - Microsoft Advertising OAuth: https://learn.microsoft.com/en-us/advertising/guides/authentication-oauth-consent?view=bingads-13 - Microsoft Advertising reporting: https://learn.microsoft.com/en-us/advertising/guides/sdk-reporting-service-manager?view=bingads-13 ## Monday.com Connector Documentation: https://permute.ai/docs/connectors/monday Import items from Monday.com boards as structured tables. The token uses its owner’s permissions. It can read only the Monday.com boards and items that user may read. Before you start: - A personal API token from your Monday.com account. ### 1. Open access token settings Open Monday.com → Profile → Developers → My Access Tokens. Monday.com access tokens: https://monday.com/apps/manage/tokens ### 2. Show or generate a personal token Select Show beside your personal token, or generate a new one. Open your profile in Monday.com, select Developers, then API token → Show. Copy the personal token into Permute. Account admins can also find it under Administration → Connections → Personal API token. The token identifies one Monday.com user. Choose a user who can see the boards and items you want Permute to import. Source: https://developer.monday.com/api-reference/docs/authentication ### 3. Copy the token Copy the token. Monday.com says a personal API token has the same board, column, item, and account permissions as its user in the app. If the user cannot see a workspace or board in Monday.com, the token cannot retrieve it for Permute. Before changing the connection, open the missing board as the token owner and review that person’s workspace and board access. Source: https://developer.monday.com/api-reference/docs/authentication ### 4. Add Monday.com in Permute In your workspace, open Connectors, select Add Connector, choose Monday.com, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the token into API Key and select Connect. ### 5. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 6. Check the first result 1. Open a known board as the personal token’s owner. 2. Note an item ID, its group, and the column value you want to compare. How Monday.com organizes a board: - Board: A collection of work with its own columns and groups. - Item: One row on a board; an item can have subitems. - Column: A value on an item, with a type such as status or date. Source: https://developer.monday.com/api-reference/reference/items ### Use stable item references Monday.com identifies items by ID and exposes the board, group, creator, creation time, and column values as separate fields. A board can change its groups or column labels while the item remains the same record. When validating a row, use the item and board together instead of relying only on a displayed item name. If a value looks wrong, inspect the column type and source value in Monday.com. A date, status, person, and text column have different value forms. Source: https://developer.monday.com/api-reference/reference/items Access and source details: ### Avoid an unexpected token cutoff The Monday.com Developer Center lets a user regenerate a personal API token. The old token becomes invalid immediately. Update Permute with the new value after regeneration; a previously connected account will otherwise fail its next API call. If a board is missing while other boards sync, inspect board access. If every request fails after regeneration, inspect the token itself. Source: https://developer.monday.com/api-reference/docs/authentication Troubleshooting: - A board is absent: Open it as the token owner and check that user’s board access. - A working token stops: Check whether the personal token was regenerated. References: - Monday.com authentication: https://developer.monday.com/api-reference/docs/authentication - Monday.com items: https://developer.monday.com/api-reference/reference/items - Monday.com access tokens: https://monday.com/apps/manage/tokens ## QuickBooks Connector Documentation: https://permute.ai/docs/connectors/quickbooks Connect QuickBooks Online accounting software. Choose the right company and environment. Connect a Test Account selects a sandbox company, whose data is separate from a live company. Before you start: - A QuickBooks primary admin, Company Admin, or Accountant Firm Owner who can connect apps for the intended company. ### 1. Add QuickBooks in Permute In your workspace, open Connectors, select Add Connector, choose QuickBooks, and name the account. ### 2. Authorize QuickBooks - For a QuickBooks sandbox, select Connect a Test Account. For a standard account, leave that option off. - Select Connect to QuickBooks. Permute connects to a live QuickBooks Online company by default. Select Connect a Test Account for an Intuit sandbox company. To return to live data, select Connect a standard account. A sandbox is separate from live company data. Check this choice before selecting Connect to QuickBooks. An empty test company will not contain the transactions from your live company. Source: https://developer.intuit.com/app/developer/qbo/docs/develop/sandboxes/manage-your-sandboxes #### Approve the right company Sign in as the QuickBooks company’s primary admin or a Company Admin. Accountant Firm Owners can also connect apps for their clients. Choose the intended company and review the authorization before approving. If you manage several companies, confirm the company shown on the authorization screen. A completed connection reads the company you approved, not every company under your Intuit login. Source: https://quickbooks.intuit.com/learn-support/en-uk/help-article/mobile-apps/connect-app/L1L5cXGvI_GB_en_GB ### 3. Check the first result 1. Confirm whether you selected a live or test QuickBooks company. 2. Check the company name on the Intuit approval screen against the accounting company you intended. QuickBooks source records: - Invoice: A customer transaction to which a payment may be applied. - Payment: A separate record that may be applied to one or several invoices, or left unapplied. - Customer reference: The link from an invoice or payment to its customer. Source: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment ### Compare the right accounting record In QuickBooks Online, an invoice has a customer reference and line items. A payment is a separate object and may be applied to several invoices or left unapplied. An invoice total and a payment amount therefore answer different questions. In QuickBooks, identify the company and the specific record before comparing totals. For an invoice with an unpaid balance, check whether any payments were applied. A payment applied to several invoices has several links to inspect. These source records explain differences between invoice and payment amounts. Source: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment Troubleshooting: - Cannot approve the connection: Ask the company’s primary admin, a Company Admin, or its Accountant Firm Owner to authorize it. - Live records are absent: Check whether Connect a Test Account was selected. - The wrong company appears: Repeat authorization and choose the intended company in Intuit. References: - QuickBooks Online sandboxes: https://developer.intuit.com/app/developer/qbo/docs/develop/sandboxes/manage-your-sandboxes - QuickBooks app connections: https://quickbooks.intuit.com/learn-support/en-uk/help-article/mobile-apps/connect-app/L1L5cXGvI_GB_en_GB - QuickBooks payments: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/payment ## Quo Connector Documentation: https://permute.ai/docs/connectors/quo Sync Quo contacts, conversations, and call history. Call history has a limited scope. It covers one-to-one conversations with standard phone numbers. Group and anonymous conversations are excluded. Before you start: - An active Quo subscription and workspace owner or admin access to create an API key. ### 1. Open Quo API settings In Quo, open Settings → API. Quo API setup guide: https://www.quo.com/docs/mdx/api-reference/authentication ### 2. Generate the key Generate an API key for Permute and copy it. #### Create a named key A Quo workspace owner or admin opens Workspace Settings → API and selects Generate API key. Give the key a descriptive label, such as Permute. Quo does not allow spaces in key names. Copy the key into Permute’s API Key field. The setup screen asks for an active Quo subscription and owner or admin access to create it. Source: https://www.quo.com/docs/mdx/api-reference/authentication #### Protect and rotate the key Quo says an API key carries the privileges of its account and grants full API access. Keep it private. If it is exposed or no longer needed, delete it in Workspace Settings → API, then create a replacement for Permute. Deleting one key immediately stops only the integrations that use that key. Other keys continue to work. Source: https://www.quo.com/docs/mdx/api-reference/authentication ### 3. Add Quo in Permute In your workspace, open Connectors, select Add Connector, choose Quo, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the key into API Key and select Connect. ### 4. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 5. Check the first result 1. Choose a contact or an eligible one-to-one call in the Quo workspace. 2. Check the call’s phone-number type before comparing the daily-refreshed history. Available data: - People and messages: Contacts and conversations. - Calls: Eligible one-to-one call history. ### Know which calls are in scope The Permute setup screen limits call history to one-to-one conversations using standard phone numbers. Group conversations and anonymous calls are excluded. It also names contacts and conversations as data groups and says the connector refreshes daily by default. For a missing call, check whether it was one-to-one and had a standard number before inspecting the key. A call outside this scope will remain absent even when contacts and other conversations sync. Use a recent eligible call as your first comparison. Source: https://www.quo.com/api Access and source details: ### Use a separate key for Permute Quo lets owners and admins create labeled API keys. A distinct key named for Permute makes it easier to find and revoke this one connection without stopping other tools. Quo says deleting a key only affects integrations using that key. Each key carries full API access and the privileges of its Quo account. Keep it private even though Permute only reads the data described in its setup screen. Source: https://www.quo.com/docs/mdx/api-reference/authentication Troubleshooting: - Generate API key is unavailable: Check that you are a workspace owner or admin with an active subscription. - A call is absent: Check whether it is one-to-one and uses a standard phone number. References: - Quo API setup guide: https://www.quo.com/docs/mdx/api-reference/authentication - Quo API overview: https://www.quo.com/api ## Salesforce Connector Documentation: https://permute.ai/docs/connectors/salesforce Connect Salesforce CRM. Set the app’s Run As user. Client Credentials Flow cannot connect without the read-access integration user. Before you start: - A Salesforce integration user with read access to the objects you want to sync. ### 1. Copy the My Domain URL In Salesforce Setup, find My Domain and copy the Current My Domain URL. ### 2. Create an External Client App Open External Client App Manager → New External Client App. In Salesforce Setup, open External Client App Manager and create an app for Permute. Enable OAuth, the api scope, and Client Credentials Flow. That flow lets the app request access without an interactive user sign-in during each sync. Use your Salesforce My Domain URL in Permute. Keep the Consumer Key and Consumer Secret from this same app together. Source: https://help.salesforce.com/s/articleView?id=configure_client_credentials_flow_for_external_client_apps.htm&language=en_US&type=5 ### 3. Enable OAuth client credentials Enable OAuth, add the api scope, and enable Client Credentials Flow. ### 4. Set the Run As user On the app’s Policies tab, set Run As to the read-access integration user. Salesforce requires an integration user for Client Credentials Flow. Set that user on the app’s Policies tab under Run As. Give the user read access to the objects Permute should sync. Without Run As, the connection cannot use this flow. If only some objects are absent, check the integration user’s Salesforce permissions before changing app credentials. Source: https://help.salesforce.com/s/articleView?id=configure_client_credentials_flow_for_external_client_apps.htm&language=en_US&type=5 ### 5. Copy the Consumer Key and Secret Under Settings → OAuth Settings, copy the Consumer Key and Consumer Secret. ### 6. Add Salesforce in Permute In your workspace, open Connectors, select Add Connector, choose Salesforce, and name the account. Under Sync which data?, choose: - Recommended: core tables. - Select manually: choose the tables Permute syncs after connecting. Connection details: - My Domain URL: Copy Current My Domain URL from Salesforce Setup. - Consumer Key and Secret: Copy both from the same External Client App. - Run As user: Set on the app’s Policies tab. - Enter My Domain URL, Consumer Key, and Consumer Secret in Permute. - Select Connect. ### 7. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 8. Check the first result 1. Choose a Salesforce record the Run As integration user can read. 2. Check the app’s api scope and Client Credentials Flow if the connection fails. Salesforce source records: - Object: A record type, such as Account or Contact. - Record: One entry in an object, identified by its Salesforce ID. - Field: A value on the record; field-level security can hide it from a user. Source: https://developer.salesforce.com/docs/platform/api-rest/guide/dome-get-field-values.html Access and source details: ### Keep app credentials private The Consumer Key identifies the External Client App, and its Consumer Secret authenticates it. Copy both from that app’s OAuth Settings into Permute. If the secret is changed or compromised, copy the new value from Salesforce. A newly created Consumer Key may need a few minutes before Salesforce accepts it, as the Permute setup screen notes. Source: https://help.salesforce.com/s/articleView?id=configure_client_credentials_flow_for_external_client_apps.htm&language=en_US&type=5 Troubleshooting: - Connection fails: Check the domain, api scope, Client Credentials Flow, and Run As user. - A new key fails at first: Wait a few minutes for activation, then retry. References: - Salesforce client credentials setup: https://help.salesforce.com/s/articleView?id=configure_client_credentials_flow_for_external_client_apps.htm&language=en_US&type=5 - Salesforce record fields: https://developer.salesforce.com/docs/platform/api-rest/guide/dome-get-field-values.html ## Stripe Connector Documentation: https://permute.ai/docs/connectors/stripe Connect Stripe payments, customers, and subscriptions. Use a restricted key with Read permissions. Permute detects test or live mode from the rk_test_ or rk_live_ prefix. Before you start: - A restricted Stripe API key with read access to the resources you want to sync. ### 1. Open restricted key setup In Stripe Dashboard → API Keys, select Create restricted key. Stripe API Keys: https://dashboard.stripe.com/apikeys In Stripe Dashboard → API Keys, create a restricted key for Permute. Restricted keys let you choose resource permissions. Select Read for the resources you want to connect. A publishable key cannot read account data, while an unrestricted secret key grants more access than this setup needs. The setup screen asks you to mark the key as provided to a third-party application and name Permute as the recipient. Source: https://docs.stripe.com/keys ### 2. Choose a third-party application Choose Providing this key to a third-party application. ### 3. Name Permute Enter Permute as the name and https://permute.ai as the URL. ### 4. Grant read access and copy the key Set resource permissions to Read, then copy the key. Stripe restricted keys start with rk_test_ for sandbox data or rk_live_ for live data. Permute detects the environment from that prefix. Test and live modes have separate objects and keys, so a test key will not show live customers or payments. Check the key prefix before connecting. Use the environment whose data you intend to analyze. Source: https://docs.stripe.com/keys ### 5. Add Stripe in Permute In your workspace, open Connectors, select Add Connector, choose Stripe, and name the account. Under Sync which data?, choose: - Everything: all discovered data. - Select manually: choose the tables Permute syncs. - Paste the restricted key into API Key and select Connect. ### 6. Choose tables (manual selection) If you chose Select manually, open the connector after connecting. Use the checkboxes to select at least one table, then select Save selection. ### 7. Check the first result 1. Choose a Stripe payment or customer in the same test or live mode as the key. 2. Check its resource Read permission before comparing the connected data. Available data: - Money movement: Payments. - Relationships: Customers and subscriptions. ### Limit the key to reads Stripe restricted keys let you choose permissions by resource. The Permute setup screen asks for Read on the resources you want to sync. A publishable key cannot read account records, and a full secret key grants more access than this reporting connection needs. Review the key’s resource list before copying it. If payments sync but another group is absent, inspect that group’s Read setting in Stripe. Creating a restricted key with a useful name also helps identify it later in the API Keys page. Source: https://docs.stripe.com/keys ### Read the key prefix Stripe separates test and live data. A restricted test key begins rk_test_; a restricted live key begins rk_live_. Permute uses that prefix to detect the environment, so the form does not ask for a separate test toggle. Compare a test key with test customers or payments, and a live key with live ones. If the connection is healthy but every expected production payment is absent, check whether the key begins rk_test_ before changing resource permissions. Source: https://docs.stripe.com/keys Access and source details: ### Handle key rotation Stripe’s API Keys page lets you expire and rotate keys. Keep track of whether the connector uses an rk_test_ or rk_live_ key. Use a key of the same mode when rotating, unless you intend to switch from test to live data. Stripe keeps sandbox and live objects separate, so a mode switch changes which records are visible. Source: https://docs.stripe.com/keys Troubleshooting: - Live objects are absent: Check that the key starts with rk_live_ rather than rk_test_. - One resource is denied: Check its Read permission on the restricted key. References: - Stripe API keys: https://docs.stripe.com/keys - Stripe API Keys: https://dashboard.stripe.com/apikeys ## Supabase Connector Documentation: https://permute.ai/docs/connectors/supabase Connect Supabase Postgres for live queries and schema discovery. Use Postgres credentials for a read-only role. The Connection URL field does not take a Supabase service_role API key. Before you start: - A read-only Postgres connection URL for your Supabase project. ### 1. Create a read-only Postgres role In the Supabase SQL editor, create a read-only role. ### 2. Grant access to selected data Grant it access only to the schemas and tables Permute should query. Avoid service_role keys. Permute asks for a read-only Postgres connection URL for live queries and schema discovery. Grant the role access only to the schemas and tables Permute should query. Use Postgres credentials, not a Supabase service_role API key. Table and column names can remain visible during discovery even when the role cannot read their data. If a query is denied, check schema USAGE and table SELECT grants. Source: https://www.postgresql.org/docs/current/ddl-priv.html ### 3. Add Supabase in Permute In your workspace, open Connectors, select Add Connector, choose Supabase, and name the account. Connection details: - Connection URL: Copy a direct or pooled Postgres URL from the Supabase Connect panel. - Username and password: Use the read-only role in that connection mode’s username format. - Paste the read-only Postgres URL into Connection URL. - Select Connect. #### Copy a Postgres connection string In the Supabase project dashboard, select Connect and copy a Postgres connection string. Use your read-only role in the username format for that connection mode, with its password. For the shared pooler, the username is .. If the password contains a reserved URL character, percent-encode it in the connection string. Source: https://supabase.com/docs/guides/database/connecting-to-postgres ### 4. Check the first result 1. Choose a known table in a schema granted to the read-only role. 2. Check that its schema is visible, then run a read-only query against it. Available data: - Schema discovery: See table and column definitions, including tables the role cannot query. - Live queries: Query tables the role may read. ### Check the endpoint and SSL Supabase documents direct and pooler endpoints. Direct connections use IPv6 unless the project has the IPv4 add-on; the pooler can help IPv4-only clients. Supabase also documents SSL settings for Postgres connection strings. If the URL cannot connect at all, check its host, port, network path, and SSL mode. If it connects but a query is denied, check the role’s schema and table permissions. Source: https://supabase.com/docs/guides/database/connecting-to-postgres Troubleshooting: - Cannot connect: Check the endpoint, network path, username format, password encoding, and SSL mode. - A query is denied: Check the role’s schema USAGE and table SELECT grants. References: - Supabase Postgres connections: https://supabase.com/docs/guides/database/connecting-to-postgres - PostgreSQL privileges: https://www.postgresql.org/docs/current/ddl-priv.html ## Xero Connector Documentation: https://permute.ai/docs/connectors/xero Connect Xero accounting software. Use a Xero user who can approve connected apps. Xero requires a Standard, Adviser, or Administrator role with Connected Apps permission. Before you start: - A Xero user with Standard, Adviser, or Administrator access and Connected Apps permission. ### 1. Add Xero in Permute In your workspace, open Connectors, select Add Connector, choose Xero, and name the account. ### 2. Authorize Xero - Select Connect with OAuth. Xero requires the authorizing user to have Standard, Adviser, or Administrator access with the Connected Apps permission. Sign in with that user when Permute opens Xero OAuth. The Xero approval screen lists organizations available to the user. Select the organization whose accounting data you want to connect. Source: https://developer.xero.com/faq/permissions #### Check reports access Xero requires Reports access to use its Reporting APIs and journal endpoints. Review this permission in Xero when those provider APIs are needed. OAuth acts on behalf of the user who approves the app. A connection cannot bypass that user’s organization access. Source: https://developer.xero.com/faq/permissions ### 3. Check the first result 1. Confirm the organization selected on the Xero consent screen. 2. For a known contact or invoice in Xero, note its ID, type, and status before comparing reports. Xero invoice types and states: - ACCPAY: A supplier bill. - ACCREC: A customer sales invoice. - Authorised: Approved and awaiting payment, or partly paid. Paid means fully paid. Source: https://developer.xero.com/documentation/api/accounting/types/ ### Use the contact ID Xero gives a contact a ContactID that is unique within an organization. Xero warns that a contact name may not remain unique. A contact can also have customer or supplier status according to its transactions. When comparing a customer or supplier across Xero reports, use its organization and ContactID rather than its name alone. If two contacts share a name, inspect their IDs before combining their invoices. Source: https://developer.xero.com/documentation/api/accounting/contacts ### Read invoice type and status Xero uses ACCPAY for supplier bills and ACCREC for customer invoices. Invoice status can be Draft, Submitted, Authorised, Paid, or Voided. An authorised invoice can still await payment or be only partly paid. Before comparing invoice totals with payments, check the type and status in Xero. A draft supplier bill and a paid customer invoice belong to different flows. Source: https://developer.xero.com/documentation/api/accounting/types/ Troubleshooting: - An organization is absent: Check the user’s role, Connected Apps permission, and existing app connection. - A source contact is unclear: Use its ContactID within the selected Xero organization. References: - Xero permissions FAQ: https://developer.xero.com/faq/permissions - Xero contacts: https://developer.xero.com/documentation/api/accounting/contacts - Xero accounting types: https://developer.xero.com/documentation/api/accounting/types/ ## REST API Reference API version: 1.0.0-beta Base URL: https://api.permute.ai ### GET /v1/workspaces — List workspaces Lists workspaces visible within the API key boundary and explicit read grants. Documentation: https://permute.ai/docs/api-reference/workspaces-list Operation ID: workspacesList Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover the workspaces this API key can read. Agent guidance: Only explicitly readable workspaces are returned. Use an ID to derive a narrowed SDK client or X-Workspace-Id header. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.workspaces.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/workspaces" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "items": [ { "id": "wksp_finance123", "organizationId": "org_acme123", "name": "Finance", "context": "Revenue, customers, and operating performance", "visibility": "private", "createdAt": "2026-07-14T12:00:00.000Z", "updatedAt": "2026-07-14T12:00:00.000Z" } ] } } ``` ### POST /v1/workspaces — Create a workspace Creates a private workspace. Requires an organization-scoped request and write access to all workspaces. Documentation: https://permute.ai/docs/api-reference/workspaces-create Operation ID: workspacesCreate Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Provision a new workspace before uploading or querying its data. Agent guidance: Requires an organization-scoped request and a write grant for all workspace resources. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.workspaces.create({ "name": "Finance", "context": "Revenue, customers, and operating performance" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/workspaces" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Finance", "context": "Revenue, customers, and operating performance" }' ``` Request example: ```json { "name": "Finance", "context": "Revenue, customers, and operating performance" } ``` Response example: ```json { "data": { "id": "wksp_finance123", "organizationId": "org_acme123", "name": "Finance", "context": "Revenue, customers, and operating performance", "visibility": "private", "createdAt": "2026-07-14T12:00:00.000Z", "updatedAt": "2026-07-14T12:00:00.000Z" } } ``` ### GET /v1/workspaces/{workspaceId} — Get a workspace Returns one workspace when it is inside the API key boundary and the key has read access. Documentation: https://permute.ai/docs/api-reference/workspaces-get Operation ID: workspacesGet Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the current name, context, and visibility of one workspace. Agent guidance: Use an exact workspace ID returned by create or list. The key needs read access to it. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.workspaces.get('wksp_finance123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/workspaces/wksp_finance123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "id": "wksp_finance123", "organizationId": "org_acme123", "name": "Finance", "context": "Revenue, customers, and operating performance", "visibility": "private", "createdAt": "2026-07-14T12:00:00.000Z", "updatedAt": "2026-07-14T12:00:00.000Z" } } ``` ### PATCH /v1/workspaces/{workspaceId} — Update a workspace Updates the workspace name or business context when the key has workspace write access. Documentation: https://permute.ai/docs/api-reference/workspaces-update Operation ID: workspacesUpdate Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Change a workspace name or business context. Agent guidance: Requires workspace write access. Send only the fields that should change; at least one field is required. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.workspaces.update('wksp_finance123', { "name": "Finance and Operations" }); ``` Raw REST example: ```bash curl -X PATCH "https://api.permute.ai/v1/workspaces/wksp_finance123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Finance and Operations" }' ``` Request example: ```json { "name": "Finance and Operations" } ``` Response example: ```json { "data": { "id": "wksp_finance123", "organizationId": "org_acme123", "name": "Finance and Operations", "context": "Revenue, customers, and operating performance", "visibility": "private", "createdAt": "2026-07-14T12:00:00.000Z", "updatedAt": "2026-07-14T12:05:00.000Z" } } ``` ### DELETE /v1/workspaces/{workspaceId} — Delete a workspace Soft-deletes a workspace when the key has workspace admin access. Documentation: https://permute.ai/docs/api-reference/workspaces-delete Operation ID: workspacesDelete Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove a workspace that should no longer be available. Agent guidance: Requires workspace admin access. Deletion is destructive from the caller perspective; do not use it as routine cleanup. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.workspaces.delete('wksp_finance123'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/workspaces/wksp_finance123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "id": "wksp_finance123", "deleted": true } } ``` ### GET /v1/sources — List queryable sources Returns business data sources available for search, answers, and structured queries. Documentation: https://permute.ai/docs/api-reference/sources-list Operation ID: sourcesList Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: yes When to use: Start here to discover queryable datasets and connectors in the current workspace. Agent guidance: Call this before selecting sources. Prefer source IDs returned here over guessed IDs. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.sources.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/sources" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "data_abc123", "type": "dataset", "name": "Revenue workbook.xlsx", "summary": "Monthly revenue and customer detail", "fileType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, { "id": "conn_hubspot123", "type": "connector", "name": "HubSpot", "provider": "hubspot", "summary": "hubspot connector" } ] } } ``` ### GET /v1/sources/{sourceId} — Get a source Returns source details and queryable tables. Set includeFields=false to list tables without columns, then pass tableId to describe one table. Materialized connector fields reflect the stored SQL table. Documentation: https://permute.ai/docs/api-reference/sources-get Operation ID: sourcesGet Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: yes When to use: List queryable tables or describe one table before writing SQL. Agent guidance: Set includeFields=false to list tables without every column. Then pass the returned tableId to inspect one table. Materialized connector columns reflect stored SQL data. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await await client.sources.listTables('conn_hubspot123'); await client.sources.describeTable('conn_hubspot123', 'blok_deals');; ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/sources/conn_hubspot123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "source": { "id": "conn_hubspot123", "type": "connector", "name": "HubSpot", "provider": "hubspot", "summary": "hubspot connector" }, "metadata": { "provider": "hubspot", "status": "active" }, "tables": [ { "id": "blok_deals", "name": "deals", "label": "Deals", "provider": "hubspot", "fieldCount": 2, "fields": [ { "name": "dealname", "type": "VARCHAR", "description": null }, { "name": "amount", "type": "DOUBLE", "description": null } ] } ] } } ``` ### POST /v1/connector-sessions — Create a hosted connector session Creates a single-use launch URL for one source. Provide a supported provider ID for a new source or connectorId to reconnect an existing source. Requires connector write permission. Keep the API key on your server. Return URLs must be registered on the API key. Documentation: https://permute.ai/docs/api-reference/connector-sessions-create Operation ID: connectorSessionsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Let a customer authorize one source through a Permute-hosted page. Agent guidance: Call from your server after authenticating the customer. Save the session-to-customer mapping. Return URLs must be registered on the API key. Treat the launch URL as a secret. Preferred SDK example: ```typescript import { PermuteClient, Providers } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.connectorSessions.create({ provider: Providers.HUBSPOT, name: 'CRM', returnUrl: 'https://your-app.example/settings/connections', }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/connector-sessions" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "provider": "hubspot", "name": "CRM", "returnUrl": "https://your-app.example/settings/connections" }' ``` Request example: ```json { "provider": "hubspot", "name": "CRM", "returnUrl": "https://your-app.example/settings/connections" } ``` Response example: ```json { "data": { "id": "cses_aaaaaaaaaaaaaaaaaaaaaaaa", "connectorId": "conn_aaaaaaaaaaaaaaaaaaaaaaaa", "provider": "hubspot", "status": "pending", "connectorStatus": "inactive", "expiresAt": "2026-09-21T12:15:00.000Z", "url": "https://app.permute.ai/connect/cses_aaaaaaaaaaaaaaaaaaaaaaaa#token=EXAMPLE_LAUNCH_TOKEN" } } ``` ### GET /v1/connector-sessions/{sessionId} — Get hosted connector session status Only the issuing API key can retrieve the session. Completed means credentials were authorized or submitted and sync was queued; inspect the source separately for data readiness. Documentation: https://permute.ai/docs/api-reference/connector-sessions-get Operation ID: connectorSessionsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Check whether the customer finished source setup. Agent guidance: Use the issuing API key and workspace. Completion does not mean data is ready; check the resulting source separately. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.connectorSessions.get('cses_aaaaaaaaaaaaaaaaaaaaaaaa'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/connector-sessions/cses_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "cses_aaaaaaaaaaaaaaaaaaaaaaaa", "connectorId": "conn_aaaaaaaaaaaaaaaaaaaaaaaa", "provider": "hubspot", "status": "completed", "connectorStatus": "syncing", "expiresAt": "2026-09-21T12:15:00.000Z" } } ``` ### GET /v1/actions — List business actions Lists visible actions and published contracts without exposing source code. Documentation: https://permute.ai/docs/api-reference/actions-list Operation ID: actionsList Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: no When to use: Discover independent workspace actions and their published contracts. Agent guidance: Inspect the published input contract and invocation permission before calling an action. Unpublished actions cannot be invoked. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/actions" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [] } } ``` ### POST /v1/actions — Create a business action Requires business action creation admin permission. Creates an independent workspace resource; save and publish its definition before invoking. Documentation: https://permute.ai/docs/api-reference/actions-create Operation ID: actionsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Create an action resource before saving its code and selected sources. Agent guidance: Requires business action creation admin permission. Save a definition and publish it through client.revisions before invoking. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.create({ "name": "Create contact" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/actions" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "Create contact" }' ``` Request example: ```json { "name": "Create contact" } ``` Response example: ```json { "data": { "id": "actn_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Create contact", "createdAt": "2026-10-02T12:00:00.000Z", "updatedAt": "2026-10-02T12:00:00.000Z" } } ``` ### GET /v1/actions/{actionId} — Get a business action Read the identity and metadata of an existing business action. Documentation: https://permute.ai/docs/api-reference/actions-get Operation ID: actionsGet Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: no When to use: Read the identity and metadata of an existing business action. Agent guidance: Use client.actions.list to inspect published contracts, or client.actions.definition with admin permission to edit code and source bindings. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.get('actn_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/actions/actn_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "actn_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Create contact", "createdAt": "2026-10-02T12:00:00.000Z", "updatedAt": "2026-10-02T12:00:00.000Z" } } ``` ### DELETE /v1/actions/{actionId} — Delete a business action Requires action admin permission. Prevents future execution without deleting source databases. Documentation: https://permute.ai/docs/api-reference/actions-delete Operation ID: actionsDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove a business action and prevent future execution. Agent guidance: Requires action admin permission. Deleting an action does not delete its source databases or undo earlier writes. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.delete('actn_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/actions/actn_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "actn_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Create contact", "createdAt": "2026-10-02T12:00:00.000Z", "updatedAt": "2026-10-02T12:00:00.000Z" } } ``` ### GET /v1/actions/{actionId}/definition — Get action definitions Requires action admin permission. Returns draft and published code, contracts, policies, and pinned source bindings. Documentation: https://permute.ai/docs/api-reference/action-definition-get Operation ID: actionDefinitionGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read draft and published action code, contracts, and source bindings before editing. Agent guidance: Requires action admin permission. Pass the current draft revisionId and sequence as expectedDraft when saving; use null only when no draft exists. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.definition('actn_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/actions/actn_abcdefghijklmnopqrstuvwx/definition" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "draft": null, "published": null } } ``` ### PUT /v1/actions/{actionId}/definition — Save an action draft Requires action and selected-source admin permission. Sources can bind published managed database schemas or queryable warehouse sources, including processed files, synced connectors, Data Tables, Data Flows, reports, dashboards, and completed entity outputs. Database schema revisions are dependency revisions; warehouse reads use current available output. Named connector bindings authorize provider calls separately. History, restore, and publish use the shared resource versioning endpoints. Documentation: https://permute.ai/docs/api-reference/action-definition-save Operation ID: actionDefinitionSave Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save action code, contracts, permissions, and source bindings together. Agent guidance: Requires admin access to the action and selected databases and connectors. Database bindings pin published schema revisions. Optional connectors maps names to connector IDs; handlers use connectors..request, query, or putObject according to the provider. Each external call needs a stable stepId. Publish through client.revisions. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.saveDefinition('actn_abcdefghijklmnopqrstuvwx', { "expectedDraft": null, "definition": { "source": "exports.handler = ({ input, db }) => { db.insert('contacts', input); return input; };", "inputSchema": { "type": "object" }, "outputSchema": { "type": "object" }, "invokePermission": "write", "sources": [ { "sourceId": "dbas_crm123", "revisionId": "rvsn_schema123", "readTables": [], "writeTables": [ "contacts" ] } ] } }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/actions/actn_abcdefghijklmnopqrstuvwx/definition" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "expectedDraft": null, "definition": { "source": "exports.handler = ({ input, db }) => { db.insert('contacts', input); return input; };", "inputSchema": { "type": "object" }, "outputSchema": { "type": "object" }, "invokePermission": "write", "sources": [ { "sourceId": "dbas_crm123", "revisionId": "rvsn_schema123", "readTables": [], "writeTables": [ "contacts" ] } ] } }' ``` Request example: ```json { "expectedDraft": null, "definition": { "source": "exports.handler = ({ input, db }) => { db.insert('contacts', input); return input; };", "inputSchema": { "type": "object" }, "outputSchema": { "type": "object" }, "invokePermission": "write", "sources": [ { "sourceId": "dbas_crm123", "revisionId": "rvsn_schema123", "readTables": [], "writeTables": [ "contacts" ] } ] } } ``` Response example: ```json { "data": { "revisionId": "rvsn_action123" } } ``` ### POST /v1/actions/{actionId}/invoke — Invoke a published action Requires the action’s declared permission. Runs one immutable revision with its declared source capabilities. Currently commits mutations and a receipt in one selected managed database. Reads are outside the write transaction. Reuse requestId with identical input after an uncertain response. Expected handler rejections use reject(code, message, details) and return a structured error; record conflicts return 409, input validation returns 400, and unexpected execution failures return a safe 500. Check error.code rather than matching message text. See the business-actions guide for codes and recovery. Documentation: https://permute.ai/docs/api-reference/actions-invoke Operation ID: actionsInvoke Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: no When to use: Run a published business action with its declared source capabilities. Agent guidance: Reuse requestId and input when retrying. The first invocation pins its revision. Managed database writes and their receipt are atomic; reads and external connector effects are outside that transaction. Completed connector steps replay their saved response, while uncertain steps block automatic resend. Changing a published database write target requires a new action. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.actions.invoke('actn_abcdefghijklmnopqrstuvwx', { "requestId": "create-contact-1", "input": { "id": "contact_1", "name": "Alex" } }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/actions/actn_abcdefghijklmnopqrstuvwx/invoke" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "requestId": "create-contact-1", "input": { "id": "contact_1", "name": "Alex" } }' ``` Request example: ```json { "requestId": "create-contact-1", "input": { "id": "contact_1", "name": "Alex" } } ``` Response example: ```json { "data": { "requestId": "create-contact-1", "actionId": "actn_abcdefghijklmnopqrstuvwx", "revisionId": "rvsn_action123", "versions": [], "output": { "id": "contact_1" } } } ``` ### POST /v1/chats — Start a conversation Starts a user-owned conversation with its first message. Reuse clientRequestId and the same message to retry. Poll GET /v1/chats/{chatId} for status, messages, citations and steps. Hosted sessions retain app, release and session scope. Unlike answer jobs, chats preserve conversation and support further turns. Documentation: https://permute.ai/docs/api-reference/chats-create Operation ID: chatsCreate Scope: workspace Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Start a conversation that will accept further messages. Agent guidance: Requires a user session. Reuse the first request ID on retry; poll the returned chat ID. Answer jobs remain one-shot analysis. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.chats.create({ "action": "send_message", "clientRequestId": "first_turn", "message": "Show the pipeline" }); ``` Raw REST example: ```bash curl -X POST "$PERMUTE_APP_ORIGIN/v1/chats" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" \ -H "Origin: $PERMUTE_APP_ORIGIN" \ -H "Content-Type: application/json" \ -d '{ "action": "send_message", "clientRequestId": "first_turn", "message": "Show the pipeline" }' ``` Request example: ```json { "action": "send_message", "clientRequestId": "first_turn", "message": "Show the pipeline" } ``` Response example: ```json { "data": { "id": "chat_abcdefghijklmnopqrstuvwx", "status": "running", "error": null, "conversation": [ { "role": "user", "content": "Show the pipeline" } ], "traceSteps": [] } } ``` ### GET /v1/chats/{chatId} — Read a conversation Returns messages, citations and presentation steps. Native workflow proposals appear as waiting with pendingTask; resolve them through the existing platform task approval API. Hosted Business Action proposals use the chat decisions endpoint. Documentation: https://permute.ai/docs/api-reference/chats-get Operation ID: chatsGet Scope: workspace Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Poll a conversation for messages, citations and generation status. Agent guidance: Hosted history requires current app, source and action access, as well as ownership. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.chats.get('chat_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "$PERMUTE_APP_ORIGIN/v1/chats/chat_abcdefghijklmnopqrstuvwx" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" ``` Response example: ```json { "data": { "id": "chat_abcdefghijklmnopqrstuvwx", "status": "completed", "error": null, "conversation": [ { "role": "assistant", "content": "The pipeline contains 11 opportunities." } ], "traceSteps": [] } } ``` ### POST /v1/chats/{chatId}/turns — Send a message Continue a conversation using its prior context. Documentation: https://permute.ai/docs/api-reference/chat-turns-create Operation ID: chatTurnsCreate Scope: workspace Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Continue a conversation using its prior context. Agent guidance: Use a new request ID for each message and keep it unchanged on retry. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.chats.send('chat_abcdefghijklmnopqrstuvwx', { "action": "send_message", "clientRequestId": "next_turn", "message": "Group it by stage" }); ``` Raw REST example: ```bash curl -X POST "$PERMUTE_APP_ORIGIN/v1/chats/chat_abcdefghijklmnopqrstuvwx/turns" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" \ -H "Origin: $PERMUTE_APP_ORIGIN" \ -H "Content-Type: application/json" \ -d '{ "action": "send_message", "clientRequestId": "next_turn", "message": "Group it by stage" }' ``` Request example: ```json { "action": "send_message", "clientRequestId": "next_turn", "message": "Group it by stage" } ``` Response example: ```json { "data": { "id": "chat_abcdefghijklmnopqrstuvwx", "status": "running", "error": null, "conversation": [ { "role": "user", "content": "Group it by stage" } ], "traceSteps": [] } } ``` ### POST /v1/chats/{chatId}/stop — Stop generation Stop an active generation. Documentation: https://permute.ai/docs/api-reference/chats-stop Operation ID: chatsStop Scope: workspace Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Stop an active generation. Agent guidance: Cancellation does not reverse an action already submitted for execution. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.chats.stop('chat_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X POST "$PERMUTE_APP_ORIGIN/v1/chats/chat_abcdefghijklmnopqrstuvwx/stop" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" \ -H "Origin: $PERMUTE_APP_ORIGIN" \ -H "Content-Type: application/json" ``` Response example: ```json { "data": { "id": "chat_abcdefghijklmnopqrstuvwx", "status": "cancelled", "error": null, "conversation": [], "traceSteps": [] } } ``` ### POST /v1/chats/{chatId}/decisions — Confirm or dismiss an action proposal Resolves a hosted Business Action proposal using its original request ID. Confirmation uses the release-pinned revision and current permissions. It does not accept an arbitrary action or replacement input. Documentation: https://permute.ai/docs/api-reference/chat-decisions-create Operation ID: chatDecisionsCreate Scope: workspace Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Confirm or dismiss a hosted assistant’s proposed Business Action. Agent guidance: Use the proposal’s request ID. Confirmation preserves its reviewed input and the app’s pinned action revision. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.chats.decide('chat_abcdefghijklmnopqrstuvwx', { "requestId": "fce05008-ce10-43f5-ad6c-9c5795f5d1b3", "approve": true }); ``` Raw REST example: ```bash curl -X POST "$PERMUTE_APP_ORIGIN/v1/chats/chat_abcdefghijklmnopqrstuvwx/decisions" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" \ -H "Origin: $PERMUTE_APP_ORIGIN" \ -H "Content-Type: application/json" \ -d '{ "requestId": "fce05008-ce10-43f5-ad6c-9c5795f5d1b3", "approve": true }' ``` Request example: ```json { "requestId": "fce05008-ce10-43f5-ad6c-9c5795f5d1b3", "approve": true } ``` Response example: ```json { "data": { "id": "chat_abcdefghijklmnopqrstuvwx", "status": "completed", "error": null, "conversation": [], "traceSteps": [] } } ``` ### GET /v1/me — Read your basic profile Read the current signed-in user’s basic profile. Documentation: https://permute.ai/docs/api-reference/users-me Operation ID: usersMe Scope: none Authentication: user Stability: beta SDK supported: yes MCP promoted: no When to use: Read the current signed-in user’s basic profile. Agent guidance: Returns no workspace or organization inventory. API keys cannot substitute for a user session. Preferred SDK example: ```typescript import { PermuteAppClient } from '@permute/sdk/app'; const client = new PermuteAppClient(); const result = await client.me(); ``` Raw REST example: ```bash curl -X GET "$PERMUTE_APP_ORIGIN/v1/me" \ -b "$PERMUTE_APP_COOKIE_JAR" \ -H "X-Permute-App-Revision: $PERMUTE_APP_REVISION_ID" ``` Response example: ```json { "data": { "id": "user_example", "name": "Ada", "email": "ada@example.com" } } ``` ### GET /v1/dashboards/{dashboardId}/source — Get dashboard source Returns the complete dashboard TSX source plus the dashboard draft revision and data run required for a safe replacement. Documentation: https://permute.ai/docs/api-reference/dashboards-source-get Operation ID: dashboardsSourceGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read an existing dashboard source before editing its TSX. Agent guidance: Keep the returned revision and dataRunId unchanged in the corresponding update. Refetch after a conflict. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.dashboards.getSource('oper_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/dashboards/oper_revenue123/source" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "dashboardId": "oper_revenue123", "resourceId": "rept_dashboard123", "source": "export default function Dashboard({ data }) { return
{data.revenue}
; }", "revision": { "revisionId": "rvsn_dashboard1", "sequence": 1 }, "dataRunId": "oprn_revenue123" } } ``` ### PUT /v1/dashboards/{dashboardId}/source — Update dashboard source Validates and replaces the complete dashboard TSX source when its dashboard draft revision and data run are unchanged. Documentation: https://permute.ai/docs/api-reference/dashboards-source-update Operation ID: dashboardsSourceUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Replace a dashboard frontend after reading and editing its complete TSX source. Agent guidance: Requires dashboard write access. The server validates the source and derives its data contract from expectedDataRunId. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.dashboards.updateSource('oper_revenue123', { "source": "export default function Dashboard({ data }) { return
{data.revenue}
; }", "expectedRevision": { "revisionId": "rvsn_dashboard1", "sequence": 1 }, "expectedDataRunId": "oprn_revenue123" }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/dashboards/oper_revenue123/source" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "source": "export default function Dashboard({ data }) { return
{data.revenue}
; }", "expectedRevision": { "revisionId": "rvsn_dashboard1", "sequence": 1 }, "expectedDataRunId": "oprn_revenue123" }' ``` Request example: ```json { "source": "export default function Dashboard({ data }) { return
{data.revenue}
; }", "expectedRevision": { "revisionId": "rvsn_dashboard1", "sequence": 1 }, "expectedDataRunId": "oprn_revenue123" } ``` Response example: ```json { "data": { "dashboardId": "oper_revenue123", "resourceId": "rept_dashboard123", "source": "export default function Dashboard({ data }) { return
{data.revenue}
; }", "revision": { "revisionId": "rvsn_dashboard2", "sequence": 2 }, "dataRunId": "oprn_revenue123" } } ``` ### POST /v1/datasets — Create a dataset Creates a custom dataset or a file dataset. Custom datasets accept related named tables; file datasets return a signed S3 upload target and begin processing automatically after the object is uploaded. CSV, TSV, JSON, NDJSON, and Parquet files can be up to 1 GiB (1,073,741,824 bytes); other supported file types can be up to 100 MiB (104,857,600 bytes). The file limit is selected from contentType. Documentation: https://permute.ai/docs/api-reference/datasets-create Operation ID: datasetsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Create or retrieve a keyed custom dataset, or create a file dataset with a signed upload target. Agent guidance: For files, PUT bytes with every returned upload header; S3 starts processing automatically. For tables, reuse the same key. Preferred SDK example: ```typescript import { PermuteClient, Context } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.datasets.create({ type: 'custom', key: 'quickbooks', name: 'QuickBooks', context: [Context.QUICKBOOKS], }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/datasets" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "type": "custom", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ] }' ``` Request example: ```json { "type": "custom", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ] } ``` Response example: ```json { "data": { "type": "custom", "id": "data_quickbooks123", "sourceId": "data_quickbooks123", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ], "status": "completed", "version": 3, "tables": [ { "name": "orders", "rowCount": 250, "columnCount": 3 }, { "name": "customers", "rowCount": 80, "columnCount": 2 } ], "createdAt": "2026-06-27T12:00:00.000Z", "updatedAt": "2026-06-27T12:05:00.000Z" } } ``` ### GET /v1/datasets — Get a dataset by key Returns the workspace dataset associated with an exact caller-owned stable key. Documentation: https://permute.ai/docs/api-reference/datasets-get-by-key Operation ID: datasetsGetByKey Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Resolve the dataset associated with an exact caller-owned stable key. Agent guidance: Use this on later runs when the caller retained the key instead of the Permute dataset ID. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.datasets.getByKey('quickbooks'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/datasets?key=quickbooks" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "type": "custom", "id": "data_quickbooks123", "sourceId": "data_quickbooks123", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ], "status": "completed", "version": 3, "tables": [ { "name": "orders", "rowCount": 250, "columnCount": 3 }, { "name": "customers", "rowCount": 80, "columnCount": 2 } ], "createdAt": "2026-06-27T12:00:00.000Z", "updatedAt": "2026-06-27T12:05:00.000Z" } } ``` ### GET /v1/datasets/{datasetId} — Get a dataset Returns the current dataset status and either its file metadata or its keyed logical tables. Documentation: https://permute.ai/docs/api-reference/datasets-get Operation ID: datasetsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the current configuration, version, status, and tables for a keyed dataset. Agent guidance: Use the dataset ID returned by create. Refresh after writes when another process may also update the dataset. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.datasets.get('data_quickbooks123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/datasets/data_quickbooks123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "type": "custom", "id": "data_quickbooks123", "sourceId": "data_quickbooks123", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ], "status": "completed", "version": 3, "tables": [ { "name": "orders", "rowCount": 250, "columnCount": 3 }, { "name": "customers", "rowCount": 80, "columnCount": 2 } ], "createdAt": "2026-06-27T12:00:00.000Z", "updatedAt": "2026-06-27T12:05:00.000Z" } } ``` ### GET /v1/datasets/{datasetId}/download — Download a file dataset Returns a 15-minute signed download URL for the original uploaded file. Documentation: https://permute.ai/docs/api-reference/datasets-download Operation ID: datasetsDownload Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Retrieve the original bytes uploaded for a file dataset. Agent guidance: The returned URL expires after 15 minutes. Fetch it promptly and do not persist it as a durable link. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.datasets.download('data_quickbooks123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/datasets/data_quickbooks123/download" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "url": "https://datasets.example.com/signed-download" } } ``` ### DELETE /v1/datasets/{datasetId} — Delete a dataset Soft-deletes one file or custom dataset when the key has dataset admin access. Documentation: https://permute.ai/docs/api-reference/datasets-delete Operation ID: datasetsDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove one file or custom dataset that should no longer be available. Agent guidance: Requires dataset admin access. Deletion is destructive from the caller perspective. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.datasets.delete('data_quickbooks123'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/datasets/data_quickbooks123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "data_quickbooks123", "deleted": true } } ``` ### POST /v1/datasets/{datasetId}/tables — Write dataset tables Creates a signed NDJSON upload for a complete dataset replacement, one-table replacement, or additive table batch. Uploading to S3 starts processing automatically. Documentation: https://permute.ai/docs/api-reference/datasets-write-tables Operation ID: datasetsWriteTables Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Write named JSON tables within an existing keyed dataset. Agent guidance: Use replace when rows can change or disappear. Append only for additive sources where the caller knows every row is new, and reuse the required batchId when retrying the same append. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await (await client.datasets.create({ type: 'custom', key: 'quickbooks' })).replace({ orders: [{ id: 1 }, { id: 2 }], customers: [{ id: 10 }, { id: 20 }], }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/datasets/data_quickbooks123/tables" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "mode": "replace", "tables": [ { "name": "orders", "rowCount": 2 }, { "name": "customers", "rowCount": 2 } ], "sizeBytes": 238, "contentHash": "d6d0ca8f6f15abca704f06170b14abecc0b18edb88685a9f0ffb844a1ebf2902" }' ``` Request example: ```json { "mode": "replace", "tables": [ { "name": "orders", "rowCount": 2 }, { "name": "customers", "rowCount": 2 } ], "sizeBytes": 238, "contentHash": "d6d0ca8f6f15abca704f06170b14abecc0b18edb88685a9f0ffb844a1ebf2902" } ``` Response example: ```json { "data": { "dataset": { "type": "custom", "id": "data_quickbooks123", "sourceId": "data_quickbooks123", "key": "quickbooks", "name": "QuickBooks", "context": [ "quickbooks" ], "status": "pending", "version": 3, "tables": [ { "name": "orders", "rowCount": 250, "columnCount": 3 }, { "name": "customers", "rowCount": 80, "columnCount": 2 } ], "createdAt": "2026-06-27T12:00:00.000Z", "updatedAt": "2026-06-27T12:05:00.000Z", "write": { "id": "dwrt_abc123", "status": "pending" } }, "write": { "id": "dwrt_abc123", "status": "pending" }, "upload": { "method": "PUT", "url": "https://datasets.example.com/signed-upload", "headers": { "Content-Type": "application/x-ndjson" }, "expiresAt": "2026-06-27T12:20:00.000Z" }, "pollAfterMs": 1000 } } ``` ### POST /v1/search — Search business evidence Finds relevant records, documents, tables, and text blocks when you need supporting evidence instead of interpretation. Documentation: https://permute.ai/docs/api-reference/search-create Operation ID: searchCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: yes When to use: Find evidence snippets, records, and text blocks when support for an answer matters. Agent guidance: Use this for citations and raw support. Reranking is on by default. Add sourceIds only when the search must be limited to specific sources. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.search({ "query": "Find evidence about Q4 revenue growth", "limit": 5 }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/search" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "query": "Find evidence about Q4 revenue growth", "limit": 5 }' ``` Request example: ```json { "query": "Find evidence about Q4 revenue growth", "limit": 5 } ``` Response example: ```json { "data": { "items": [ { "type": "record", "sourceId": "conn_hubspot123", "title": "deals", "content": "Enterprise renewal deal with $48,000 amount", "score": 0.91, "citation": { "connectorId": "conn_hubspot123", "blockId": "blok_deals" }, "metadata": { "provider": "hubspot", "tableName": "deals" } } ], "meta": { "strategy": "semantic", "reranked": true, "candidateCount": 12, "totalMs": 86 } } } ``` ### POST /v1/answer-jobs — Start an answer job Starts a Permute analysis job and returns immediately. Poll the returned answerJobId until the result is completed. Documentation: https://permute.ai/docs/api-reference/answer-jobs-create Operation ID: answerJobsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: yes When to use: Start a durable Permute analysis when the answer may need multiple tool calls or larger source context. Agent guidance: Prefer this for business questions. Treat the completed result as the analysis system of record. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.answerJobs({ "question": "What changed in revenue this quarter?", "sourceIds": [ "conn_hubspot123" ] }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/answer-jobs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "question": "What changed in revenue this quarter?", "sourceIds": [ "conn_hubspot123" ] }' ``` Request example: ```json { "question": "What changed in revenue this quarter?", "sourceIds": [ "conn_hubspot123" ] } ``` Response example: ```json { "data": { "answerJobId": "ansj_abc123", "status": "queued", "pollAfterMs": 1500, "createdAt": "2026-05-18T12:00:00.000Z", "updatedAt": "2026-05-18T12:00:00.000Z" } } ``` ### GET /v1/answer-jobs/{answerJobId} — Get an answer job Returns the current status and final result for a Permute answer job. Documentation: https://permute.ai/docs/api-reference/answer-jobs-get Operation ID: answerJobsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: yes When to use: Poll a previously started analysis until it is completed or failed. Agent guidance: Call this with the answerJobId returned by answerJobsCreate. Do not redo Permute analysis while waiting. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.getAnswerJob('ansj_abc123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/answer-jobs/ansj_abc123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "answerJobId": "ansj_abc123", "status": "completed", "result": { "answer": "Revenue increased because expansion and renewal activity outweighed churn in the selected period.", "citations": [ { "type": "table", "sourceId": "conn_hubspot123", "ref": "table_1" } ], "followUps": [ "Break this down by segment", "Show the largest underlying deals" ], "confidence": { "score": 88, "queryComplexity": "medium", "reason": "Answer is limited to accessible workspace sources." } }, "createdAt": "2026-05-18T12:00:00.000Z", "updatedAt": "2026-05-18T12:00:08.000Z", "completedAt": "2026-05-18T12:00:08.000Z" } } ``` ### POST /v1/queries — Run a structured query Executes read-only SQL against explicitly selected accessible sources. Use GET /v1/sources/{sourceId}?includeFields=false to list tables, then tableId to inspect one table. Warehouse-backed sources use DuckDB SQL and can be queried together. Live remote database connectors use their native dialect (GoogleSQL for BigQuery and PostgreSQL for Supabase), must be queried one at a time, and cannot be combined with other sources. timings separates source validation, DuckDB startup, source loading, and SQL execution; totalMs excludes network transit and response serialization. Documentation: https://permute.ai/docs/api-reference/queries-create Operation ID: queriesCreate Scope: workspace Authentication: authenticated Stability: beta SDK supported: yes MCP promoted: yes When to use: Run read-only SQL when you already know the exact table names and source IDs. Agent guidance: Use after source inspection for exact rows. Use DuckDB SQL for warehouse-backed sources and the native dialect for one live remote connector. timings separates validation, initialization, source loading, and SQL execution; totalMs excludes network transit and response serialization. For natural-language questions, start an answer job first. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.query({ "sourceIds": [ "data_abc123", "conn_hubspot123" ], "sql": "SELECT * FROM \"blok_revenue\" LIMIT 10", "limit": 100 }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/queries" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "sourceIds": [ "data_abc123", "conn_hubspot123" ], "sql": "SELECT * FROM \"blok_revenue\" LIMIT 10", "limit": 100 }' ``` Request example: ```json { "sourceIds": [ "data_abc123", "conn_hubspot123" ], "sql": "SELECT * FROM \"blok_revenue\" LIMIT 10", "limit": 100 } ``` Response example: ```json { "data": { "queryId": "qry_abc123", "sourceIds": [ "data_abc123", "conn_hubspot123" ], "sql": "SELECT * FROM \"blok_revenue\" LIMIT 10", "columns": [ { "name": "month" }, { "name": "revenue" } ], "rows": [ { "month": "2026-03", "revenue": 120000 } ], "rowCount": 1, "truncated": false, "executionMs": 18, "timings": { "sourceValidationMs": 2, "initializationMs": 5, "sourceLoadMs": 8, "sqlExecutionMs": 3, "totalMs": 19 } } } ``` ### GET /v1/operation-runs — List operation runs by workflow Groups existing operation runs by the workflowId returned from execution. Returns progress, step errors, and available output references. Requires read access to every operation in the group. No separate workflow resource is created. Documentation: https://permute.ai/docs/api-reference/operation-runs-list Operation ID: operationRunsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Check progress after starting a data flow. Agent guidance: Pass the workflowId returned by operations.run. Inspect step errors, then use operations.getRunOutput with the completed step’s operation and run IDs. Requires read access to all steps. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.listRuns({ workflowId: 'oper_flow123_wkfl_run123' }); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operation-runs?workflowId=oper_flow123_wkfl_run123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "workflowId": "oper_flow123_wkfl_run123", "status": "running", "totalRuns": 1, "completedRuns": 0, "failedRuns": 0, "pendingRuns": 1, "progressPct": 0, "startedAt": "2026-09-10T12:00:00.000Z", "completedAt": null, "items": [ { "id": "oprn_run123", "operationId": "oper_revenue123", "workflowId": "oper_flow123_wkfl_run123", "revisionId": "rvsn_code123", "status": "running", "createdAt": "2026-09-10T12:00:00.000Z", "completedAt": null, "error": null, "outputDocId": null } ] } } ``` ### GET /v1/operations — List operations Discover operations, including drafts that have no published output. Documentation: https://permute.ai/docs/api-reference/operations-list Operation ID: operationsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover operations, including drafts that have no published output. Agent guidance: Filter kind=dataFlow to discover flows, or dataFlowId to list one flow’s operations. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operations" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "oper_revenue123", "name": "Revenue by customer", "kind": "dataTable", "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "parentDataFlowId": "oper_flow123", "status": "draft", "codeStale": false, "schedule": { "type": "manual" }, "isEnabled": true, "nextRunAt": null, "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": null } ] } } ``` ### GET /v1/operations/{operationId} — Get an operation Inspect an operation’s instructions, inputs, revisions, schedule, enablement, and next run time. Documentation: https://permute.ai/docs/api-reference/operations-get Operation ID: operationsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect an operation’s instructions, inputs, revisions, schedule, enablement, and next run time. Agent guidance: Check codeStale before running. Read handler code separately with getSource. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.get('oper_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operations/oper_revenue123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "oper_revenue123", "name": "Revenue by customer", "kind": "dataTable", "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "parentDataFlowId": "oper_flow123", "status": "draft", "codeStale": false, "schedule": { "type": "manual" }, "isEnabled": true, "nextRunAt": null, "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": null } } ``` ### PATCH /v1/operations/{operationId} — Schedule, pause, or resume an operation Updates schedule or isEnabled without executing or publishing. All schedule times are UTC; weekly dayOfWeek uses Sunday=0. Null or manual clears the schedule. Pausing preserves the schedule and clears nextRunAt. Scheduled runs use published code. Configure Data Flow scheduling on its parent operation, not individual children. Documentation: https://permute.ai/docs/api-reference/operations-update Operation ID: operationsUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Schedule a published operation or Data Flow, or pause and resume it. Agent guidance: All times are UTC (weekly dayOfWeek uses Sunday=0). Send only the fields you want to change. Use isEnabled: false to pause while keeping the schedule, isEnabled: true to resume, or schedule: null to return to manual runs. Publish first: scheduling does not publish draft code. Use the parent operation ID for a Data Flow. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.update('oper_revenue123', { "schedule": { "type": "daily", "hour": 8, "minute": 0 }, "isEnabled": true }); ``` Raw REST example: ```bash curl -X PATCH "https://api.permute.ai/v1/operations/oper_revenue123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "schedule": { "type": "daily", "hour": 8, "minute": 0 }, "isEnabled": true }' ``` Request example: ```json { "schedule": { "type": "daily", "hour": 8, "minute": 0 }, "isEnabled": true } ``` Response example: ```json { "data": { "id": "oper_revenue123", "name": "Revenue by customer", "kind": "dataTable", "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "parentDataFlowId": null, "status": "active", "codeStale": false, "schedule": { "type": "daily", "hour": 8, "minute": 0 }, "isEnabled": true, "nextRunAt": "2026-09-12T08:00:00.000Z", "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": { "revisionId": "rvsn_code123", "sequence": 1 } } } ``` ### GET /v1/operations/{operationId}/source — Read operation code and inputs Read handler code and current configured inputs before editing. Documentation: https://permute.ai/docs/api-reference/operations-source-get Operation ID: operationsSourceGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read handler code and current configured inputs before editing. Agent guidance: Keep revision, dataFlowRevision, and updatedAt for the save request. Configured inputs may differ from the saved code when codeStale is true. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.getSource('oper_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operations/oper_revenue123/source" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "operationId": "oper_revenue123", "source": "async function handler(event) { return await event.query(\"SELECT customer, SUM(revenue) AS revenue FROM blok_revenue GROUP BY customer\"); } module.exports = { handler };", "revision": { "revisionId": "rvsn_code123", "sequence": 1 }, "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "dependencyOperationIds": [], "dataFlowRevision": 2, "updatedAt": "2026-09-10T12:00:00.000Z" } } ``` ### PUT /v1/operations/{operationId}/source — Save operation code as a draft Saves externally authored handler code using the current operation configuration. Requires the revisions and updatedAt returned by getSource. Does not run an AI build, execute, or publish. Documentation: https://permute.ai/docs/api-reference/operations-source-update Operation ID: operationsSourceUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save a complete handler as a draft. First read getSource, edit that source, and pass its revision, dataFlowRevision, and updatedAt back as the expected values (the JSON values below are illustrative). A 409 conflict means the source or configuration changed; read again and reconcile your edits before retrying. Agent guidance: Use JavaScript and module.exports = { handler }. Use event.query(sql) for local rows, event.remoteQuery(connectorId, sql) for live SQL sources, and event.currentDate for relative dates (the execution's UTC date in YYYY-MM-DD format). For large SQL outputs, return await event.query(sql, { result: 'tableReference' }) or an object of named results from that mode; rows stay in DuckDB instead of entering JavaScript. Otherwise return plain data without functions. Custom toJSON() methods are not invoked; convert custom values to plain data before returning them. Imports and require() are unavailable. Saving does not execute or publish; run and inspect the output afterward. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const operationId = 'oper_revenue123'; const current = await client.operations.getSource(operationId); // Edit current.source, then submit the complete updated handler. const newSource = "async function handler(event) { return await event.query(\"SELECT customer, SUM(revenue) AS revenue FROM blok_revenue GROUP BY customer\"); } module.exports = { handler };"; const result = await client.operations.updateSource(operationId, { source: newSource, expectedRevision: current.revision, expectedDataFlowRevision: current.dataFlowRevision, expectedUpdatedAt: current.updatedAt, }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/operations/oper_revenue123/source" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "source": "async function handler(event) { return await event.query(\"SELECT customer, SUM(revenue) AS revenue FROM blok_revenue GROUP BY customer\"); } module.exports = { handler };", "expectedRevision": { "revisionId": "rvsn_code123", "sequence": 1 }, "expectedDataFlowRevision": 2, "expectedUpdatedAt": "2026-09-10T12:00:00.000Z" }' ``` Request example: ```json { "source": "async function handler(event) { return await event.query(\"SELECT customer, SUM(revenue) AS revenue FROM blok_revenue GROUP BY customer\"); } module.exports = { handler };", "expectedRevision": { "revisionId": "rvsn_code123", "sequence": 1 }, "expectedDataFlowRevision": 2, "expectedUpdatedAt": "2026-09-10T12:00:00.000Z" } ``` Response example: ```json { "data": { "operationId": "oper_revenue123", "source": "async function handler(event) { return await event.query(\"SELECT customer, SUM(revenue) AS revenue FROM blok_revenue GROUP BY customer\"); } module.exports = { handler };", "revision": { "revisionId": "rvsn_code456", "sequence": 2 }, "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "dependencyOperationIds": [], "dataFlowRevision": 3, "updatedAt": "2026-09-10T12:00:00.000Z" } } ``` ### GET /v1/operations/{operationId}/runs — List the latest 50 operation runs Track execution and inspect failures. Documentation: https://permute.ai/docs/api-reference/operations-runs-list Operation ID: operationsRunsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Track execution and inspect failures. Agent guidance: Match workflowId to the run you started. Results include at most 50 recent runs; completed output is inspected with getRunOutput. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.listRuns('oper_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operations/oper_revenue123/runs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "oprn_run123", "workflowId": "wkfl_run123", "revisionId": "rvsn_code123", "status": "completed", "createdAt": "2026-09-10T12:00:00.000Z", "completedAt": "2026-09-10T12:00:05.000Z", "error": null, "outputDocId": "oprn_run123" } ] } } ``` ### GET /v1/operations/{operationId}/runs/{runId}/output — Inspect a completed run before publishing Returns up to 1,000 rows per output table with schemas and display names from the exact run. This is a preview, not a complete dataset. Documentation: https://permute.ai/docs/api-reference/operations-run-output Operation ID: operationsRunOutput Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect exact completed run output before publishing. Agent guidance: The preview contains at most 1,000 rows per table. A preview is not a complete dataset. The run must belong to the requested operation and workspace. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.getRunOutput('oper_revenue123', 'oprn_run123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/operations/oper_revenue123/runs/oprn_run123/output" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "runId": "oprn_run123", "revisionId": "rvsn_code123", "rowLimitPerTable": 1000, "tables": { "blok_revenue": [ { "customer": "Example customer", "revenue": 1200 } ] }, "names": { "blok_revenue": "Revenue" }, "schemas": { "blok_revenue": { "columns": [ { "name": "customer", "type": "string" }, { "name": "revenue", "type": "number" } ] } } } } ``` ### POST /v1/operations/{operationId}/runs — Run an operation or data flow Runs saved code through the existing sandbox. Data flows execute their dependency graph. Draft runs do not deliver notifications. Poll runs to inspect completion and errors. Documentation: https://permute.ai/docs/api-reference/operations-run Operation ID: operationsRun Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Test saved draft code or execute published code. Agent guidance: Data Flows execute their dependency graph. Individual operations use existing prerequisite outputs. Poll listRuns with the returned workflowId and inspect node failures before publishing. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.operations.run('oper_revenue123', { "version": "draft" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/operations/oper_revenue123/runs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "version": "draft" }' ``` Request example: ```json { "version": "draft" } ``` Response example: ```json { "data": { "workflowId": "wkfl_run123", "runId": "oprn_run123" } } ``` ### POST /v1/data-flows — Create a draft data flow Create a flow for an external agent to build. Documentation: https://permute.ai/docs/api-reference/data-flows-create Operation ID: dataFlowsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Create a flow for an external agent to build. Agent guidance: Creates a draft without invoking an AI build. Add operations through graph commands, then save their code and test them. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.dataFlows.create({ "name": "Revenue flow", "description": "Revenue by customer", "dataSourceIds": [ "data_abc123" ] }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/data-flows" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "Revenue flow", "description": "Revenue by customer", "dataSourceIds": [ "data_abc123" ] }' ``` Request example: ```json { "name": "Revenue flow", "description": "Revenue by customer", "dataSourceIds": [ "data_abc123" ] } ``` Response example: ```json { "data": { "operation": { "id": "oper_flow123", "name": "Revenue flow", "kind": "dataFlow", "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "parentDataFlowId": null, "status": "draft", "codeStale": false, "schedule": { "type": "manual" }, "isEnabled": true, "nextRunAt": null, "draft": null, "published": null }, "revision": 0, "draftVersion": 1, "publishedVersion": null, "operationIds": [], "sourceIds": [ "data_abc123" ], "edges": [] } } ``` ### GET /v1/data-flows/{dataFlowId} — Get a data flow and its operation references Read a flow’s operation references, edges, and revision. Documentation: https://permute.ai/docs/api-reference/data-flows-get Operation ID: dataFlowsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read a flow’s operation references, edges, and revision. Agent guidance: Fetch operations and their code separately as needed. Use the numeric flow revision for the next graph command. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.dataFlows.get('oper_flow123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/data-flows/oper_flow123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "operation": { "id": "oper_flow123", "name": "Revenue flow", "kind": "dataFlow", "instructions": "Sum revenue by customer.", "dataSourceIds": [ "data_abc123" ], "parentDataFlowId": null, "status": "draft", "codeStale": false, "schedule": { "type": "manual" }, "isEnabled": true, "nextRunAt": null, "draft": null, "published": null }, "revision": 2, "draftVersion": 2, "publishedVersion": null, "operationIds": [ "oper_revenue123" ], "sourceIds": [ "data_abc123" ], "edges": [ { "id": "data_abc123->oper_revenue123", "fromNodeId": "data_abc123", "toNodeId": "oper_revenue123" } ] } } ``` ### POST /v1/data-flows/{dataFlowId}/commands — Edit or publish a data flow Uses the same revision-checked graph commands as the editor. Includes adding, editing, connecting, and deleting nodes, updating or deleting the flow, and publishing a successful run. Fetch the flow after each command. Documentation: https://permute.ai/docs/api-reference/data-flows-command Operation ID: dataFlowsCommand Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Add or edit operations, wire inputs, publish, or delete a flow. Agent guidance: Uses the editor’s commands and permissions. Fetch the flow after each command. Publishing requires a successful current run; deleting a flow requires admin access and removes its owned operations. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.dataFlows.command('oper_flow123', { "command": { "type": "addTransform", "expectedRevision": 2, "input": { "name": "Revenue by customer", "instruction": "Sum revenue by customer.", "inputIds": [ "data_abc123" ] } } }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/data-flows/oper_flow123/commands" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "command": { "type": "addTransform", "expectedRevision": 2, "input": { "name": "Revenue by customer", "instruction": "Sum revenue by customer.", "inputIds": [ "data_abc123" ] } } }' ``` Request example: ```json { "command": { "type": "addTransform", "expectedRevision": 2, "input": { "name": "Revenue by customer", "instruction": "Sum revenue by customer.", "inputIds": [ "data_abc123" ] } } } ``` Response example: ```json { "data": { "dataFlowId": "oper_flow123", "deleted": false } } ``` ### GET /v1/facts — List organization facts Reads shared business definitions, aliases, formulas, and baselines. Requires organization-wide fact read permission. Documentation: https://permute.ai/docs/api-reference/facts-list Operation ID: factsList Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover canonical business definitions and calculations. Agent guidance: Facts apply across the organization. This requires organization-wide fact read permission. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.facts.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/facts" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "items": [ { "id": "fact_revenue123", "organizationId": "org_acme123", "ownerId": "user_admin123", "key": "net_revenue", "name": "Net revenue", "type": "formula", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math", "aliases": null, "usualValue": null, "usualValueMin": null, "usualValueMax": null, "unit": null, "startDate": null, "endDate": null, "scope": null, "metadata": null, "createdAt": "2026-09-10T12:00:00.000Z", "updatedAt": "2026-09-10T12:00:00.000Z" } ] } } ``` ### GET /v1/facts/{factId} — Get a fact Reads a fact in the API key’s organization with fact read permission. Documentation: https://permute.ai/docs/api-reference/facts-get Operation ID: factsGet Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the exact rule referenced by a fact citation. Agent guidance: Use the returned fact ID; the API checks organization ownership and fact read permission. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.facts.get('fact_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/facts/fact_revenue123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "id": "fact_revenue123", "organizationId": "org_acme123", "ownerId": "user_admin123", "key": "net_revenue", "name": "Net revenue", "type": "formula", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math", "aliases": null, "usualValue": null, "usualValueMin": null, "usualValueMax": null, "unit": null, "startDate": null, "endDate": null, "scope": null, "metadata": null, "createdAt": "2026-09-10T12:00:00.000Z", "updatedAt": "2026-09-10T12:00:00.000Z" } } ``` ### POST /v1/facts — Create a fact Requires an organization-scoped request and fact write permission. The key is derived from the name; type is derived from the populated fields. Baselines require a scalar or ordered range, unit, dates, and scope. Documentation: https://permute.ai/docs/api-reference/facts-create Operation ID: factsCreate Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Define a shared business rule. Agent guidance: Use an organization-scoped key with fact write permission. Names produce unique keys; type is derived from the fields. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.facts.create({ "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/facts" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" }' ``` Request example: ```json { "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" } ``` Response example: ```json { "data": { "id": "fact_revenue123", "organizationId": "org_acme123", "ownerId": "user_admin123", "key": "net_revenue", "name": "Net revenue", "type": "formula", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math", "aliases": null, "usualValue": null, "usualValueMin": null, "usualValueMax": null, "unit": null, "startDate": null, "endDate": null, "scope": null, "metadata": null, "createdAt": "2026-09-10T12:00:00.000Z", "updatedAt": "2026-09-10T12:00:00.000Z" } } ``` ### PUT /v1/facts/{factId} — Replace a fact Replaces all editable fields. Omitted fields are cleared. Requires an organization-scoped request and fact write permission. Documentation: https://permute.ai/docs/api-reference/facts-update Operation ID: factsUpdate Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Replace an existing business rule. Agent guidance: Read first, then send every field you want to keep. Omitted editable fields are cleared. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.facts.update('fact_revenue123', { "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/facts/fact_revenue123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" }' ``` Request example: ```json { "name": "Net revenue", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math" } ``` Response example: ```json { "data": { "id": "fact_revenue123", "organizationId": "org_acme123", "ownerId": "user_admin123", "key": "net_revenue", "name": "Net revenue", "type": "formula", "definition": "Revenue after returns", "formula": "revenue - returns", "formulaType": "math", "aliases": null, "usualValue": null, "usualValueMin": null, "usualValueMax": null, "unit": null, "startDate": null, "endDate": null, "scope": null, "metadata": null, "createdAt": "2026-09-10T12:00:00.000Z", "updatedAt": "2026-09-10T12:00:00.000Z" } } ``` ### DELETE /v1/facts/{factId} — Delete a fact Soft-deletes a fact. Requires an organization-scoped request and fact write permission. Documentation: https://permute.ai/docs/api-reference/facts-delete Operation ID: factsDelete Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove an obsolete organization rule. Agent guidance: Requires an organization-scoped request and fact write permission. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.facts.delete('fact_revenue123'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/facts/fact_revenue123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "success": true, "message": "Fact deleted" } } ``` ### GET /v1/skills — List shared skills Workspace scope. Requires skill read permission. Private skills are excluded. Documentation: https://permute.ai/docs/api-reference/skills-list Operation ID: skillsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover shared Markdown context before analysis. Agent guidance: Read the relevant files next; private skills are excluded. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.skills.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/skills" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "file": "finance/revenue.md", "description": "Revenue reporting guidance", "lastModified": "2026-09-10T12:00:00.000Z" } ] } } ``` ### GET /v1/skills/{path} — Read a skill Workspace scope. Requires skill read permission. URL-encode nested Markdown paths; absent files return empty content, matching the shared skill library. Documentation: https://permute.ai/docs/api-reference/skills-get Operation ID: skillsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read a business instruction file. Agent guidance: The SDK URL-encodes nested paths. Missing files return empty content. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.skills.get('finance/revenue.md'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." } } ``` ### PUT /v1/skills/{path} — Create or replace a skill Workspace scope. Requires skill write permission. Saves a complete Markdown file of at most 2,000 words and generates its discovery description. Documentation: https://permute.ai/docs/api-reference/skills-update Operation ID: skillsUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save reusable business context. Agent guidance: Send the complete Markdown content (at most 2,000 words). This creates or replaces the file and generates its discovery description. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.skills.update('finance/revenue.md', { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." }' ``` Request example: ```json { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." } ``` Response example: ```json { "data": { "success": true, "message": "Updated finance/revenue.md" } } ``` ### DELETE /v1/skills/{path} — Delete a skill Workspace scope. Requires skill write permission. Soft-deletes a shared Markdown file. Documentation: https://permute.ai/docs/api-reference/skills-delete Operation ID: skillsDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove obsolete business context. Agent guidance: This soft-deletes the shared file in the selected scope. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.skills.delete('finance/revenue.md'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "success": true, "message": "Deleted finance/revenue.md" } } ``` ### GET /v1/organization/skills — List shared organization skills Organization scope. Requires skill read permission. Private skills are excluded. Documentation: https://permute.ai/docs/api-reference/organization-skills-list Operation ID: organizationSkillsList Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover shared Markdown context before analysis. Agent guidance: Read the relevant files next; private skills are excluded. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.organizationSkills.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/organization/skills" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "items": [ { "file": "finance/revenue.md", "description": "Revenue reporting guidance", "lastModified": "2026-09-10T12:00:00.000Z" } ] } } ``` ### GET /v1/organization/skills/{path} — Read an organization skill Organization scope. Requires skill read permission. URL-encode nested Markdown paths; absent files return empty content, matching the shared skill library. Documentation: https://permute.ai/docs/api-reference/organization-skills-get Operation ID: organizationSkillsGet Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read a business instruction file. Agent guidance: The SDK URL-encodes nested paths. Missing files return empty content. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.organizationSkills.get('finance/revenue.md'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/organization/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." } } ``` ### PUT /v1/organization/skills/{path} — Create or replace an organization skill Organization scope. Requires skill write permission. Requires an organization-scoped request. Saves a complete Markdown file of at most 2,000 words and generates its discovery description. Documentation: https://permute.ai/docs/api-reference/organization-skills-update Operation ID: organizationSkillsUpdate Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save reusable business context. Agent guidance: Send the complete Markdown content (at most 2,000 words). This creates or replaces the file and generates its discovery description. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.organizationSkills.update('finance/revenue.md', { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/organization/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." }' ``` Request example: ```json { "content": "# Revenue reporting\n\nUse the Net revenue fact for revenue analysis." } ``` Response example: ```json { "data": { "success": true, "message": "Updated finance/revenue.md" } } ``` ### DELETE /v1/organization/skills/{path} — Delete an organization skill Organization scope. Requires skill write permission. Requires an organization-scoped request. Soft-deletes a shared Markdown file. Documentation: https://permute.ai/docs/api-reference/organization-skills-delete Operation ID: organizationSkillsDelete Scope: organization Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Remove obsolete business context. Agent guidance: This soft-deletes the shared file in the selected scope. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const result = await orgClient.organizationSkills.delete('finance/revenue.md'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/organization/skills/finance%2Frevenue.md" \ -H "Authorization: Bearer $PERMUTE_API_KEY" ``` Response example: ```json { "data": { "success": true, "message": "Deleted finance/revenue.md" } } ``` ### GET /v1/databases — List databases List managed databases in the current workspace. Documentation: https://permute.ai/docs/api-reference/databases-list Operation ID: databasesList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: List managed databases in the current workspace. Agent guidance: Creation is limited to entitled Enterprise plans. Existing databases remain listed when creation is unavailable. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/databases" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "dbas_crm123", "workspaceId": "wksp_finance123", "name": "CRM", "key": "crm", "status": "ready", "createdAt": "2026-09-18T12:00:00.000Z", "updatedAt": "2026-09-18T12:00:08.000Z" } ] } } ``` ### POST /v1/databases — Create a managed database Requires database creation admin permission. Provisioning is asynchronous; repeat the same key and name to retrieve the existing request. Documentation: https://permute.ai/docs/api-reference/databases-create Operation ID: databasesCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Create a managed database or retrieve an in-flight request for the same workspace key. Agent guidance: Requires workspace database admin. Repeat the same key and name to resume. Provisioning is asynchronous; poll GET until status is ready. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.create({ "name": "CRM", "key": "crm" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/databases" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM", "key": "crm" }' ``` Request example: ```json { "name": "CRM", "key": "crm" } ``` Response example: ```json { "data": { "id": "dbas_crm123", "workspaceId": "wksp_finance123", "name": "CRM", "key": "crm", "status": "provisioning", "createdAt": "2026-09-18T12:00:00.000Z", "updatedAt": "2026-09-18T12:00:08.000Z" } } ``` ### GET /v1/databases/{databaseId} — Get a database Read the current status of one managed database. Documentation: https://permute.ai/docs/api-reference/databases-get Operation ID: databasesGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the current status of one managed database. Agent guidance: Use after create while status is provisioning, or to confirm a database is ready before writing tables. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.get('dbas_crm123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/databases/dbas_crm123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "dbas_crm123", "workspaceId": "wksp_finance123", "name": "CRM", "key": "crm", "status": "ready", "createdAt": "2026-09-18T12:00:00.000Z", "updatedAt": "2026-09-18T12:00:08.000Z" } } ``` ### DELETE /v1/databases/{databaseId} — Delete a database Requires admin permission. Blocks new data requests and schedules permanent cluster deletion. Documentation: https://permute.ai/docs/api-reference/databases-delete Operation ID: databasesDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Permanently delete a managed database and its cluster. Agent guidance: Requires admin permission. Deletion is asynchronous; later reads return 404 once the database is gone. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.delete('dbas_crm123'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/databases/dbas_crm123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "dbas_crm123", "workspaceId": "wksp_finance123", "name": "CRM", "key": "crm", "status": "deleting", "createdAt": "2026-09-18T12:00:00.000Z", "updatedAt": "2026-09-18T12:00:08.000Z" } } ``` ### GET /v1/databases/{databaseId}/definition — Get draft and published database definitions Requires database admin permission. Returns the versioned database table definitions. Documentation: https://permute.ai/docs/api-reference/database-definition-get Operation ID: databaseDefinitionGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the database application draft and published definition before editing. Agent guidance: Requires database admin permission. Save the complete definition with the expected draft ref. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.definition('dbas_crm123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/databases/dbas_crm123/definition" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "draft": null, "published": null } } ``` ### PUT /v1/databases/{databaseId}/definition — Save a database schema draft Requires database admin permission. Table schemas are one revision; actions are independent resources. History, restore, and publish use the existing resource versioning routes. Documentation: https://permute.ai/docs/api-reference/database-definition-save Operation ID: databaseDefinitionSave Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save managed table schemas as one draft revision. Agent guidance: Requires database admin permission. Publish through client.revisions after reading its current version token. Author business actions separately through client.actions and bind them to the published schema revision. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.saveDefinition('dbas_crm123', { "expectedDraft": null, "definition": { "tables": [ { "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] } ] } }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/databases/dbas_crm123/definition" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "expectedDraft": null, "definition": { "tables": [ { "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] } ] } }' ``` Request example: ```json { "expectedDraft": null, "definition": { "tables": [ { "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] } ] } } ``` Response example: ```json { "data": { "revisionId": "rvsn_database1" } } ``` ### GET /v1/databases/{databaseId}/tables — List database tables Lists readable tables across customer schemas, excluding internal schemas. Documentation: https://permute.ai/docs/api-reference/database-tables-list Operation ID: databaseTablesList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover readable tables and their columns across customer schemas. Agent guidance: Internal schemas are hidden. Imported tables may appear here even when they are not writable. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.tables('dbas_crm123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/databases/dbas_crm123/tables" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "schema": "app", "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] } ] } } ``` ### POST /v1/databases/{databaseId}/tables — Add a table to the database schema draft Requires admin permission. Save the table in a draft and publish the database definition to create it. Documentation: https://permute.ai/docs/api-reference/database-tables-create Operation ID: databaseTablesCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Add a typed table to the database application draft. Agent guidance: Requires admin permission. Publish the database definition through client.revisions to create the live table. Primary keys must be non-nullable scalar columns. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.createTable('dbas_crm123', { "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/databases/dbas_crm123/tables" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] }' ``` Request example: ```json { "name": "customers", "columns": [ { "name": "id", "type": "text", "nullable": false }, { "name": "name", "type": "text", "nullable": false } ], "primaryKey": [ "id" ] } ``` Response example: ```json { "data": { "name": "customers", "revisionId": "rvsn_database1" } } ``` ### GET /v1/databases/{databaseId}/tables/{table}/records — Read database records Reads a table in the requested schema (app by default). Managed tables include opaque __version tokens; imported tables may not. Pagination reflects current data, not a historical snapshot; tables without primary keys have no guaranteed row order. Documentation: https://permute.ai/docs/api-reference/database-records-list Operation ID: databaseRecordsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read current rows from one table. Agent guidance: Defaults to the app schema. Managed tables include opaque __version tokens needed for later updates. Pagination is not a historical snapshot. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.records('dbas_crm123', 'customers', { limit: 25 }); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/databases/dbas_crm123/tables/customers/records?limit=25" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "acme", "name": "Acme", "__version": "11111111-1111-4111-8111-111111111111" } ] } } ``` ### POST /v1/databases/{databaseId}/transactions — Admin record transaction Requires database admin permission. This is a maintenance write path. Up to 100 mutations and 1 MB per transaction. Updates/deletes require exact primary keys and expectedVersion tokens. Reuse requestId after an uncertain response; identical requests from the same principal replay their receipt. Numeric values use decimal strings. Documentation: https://permute.ai/docs/api-reference/database-transactions-create Operation ID: databaseTransactionsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Insert, update, or delete managed table rows in one atomic request. Agent guidance: Requires database admin permission. Updates and deletes need the exact primary key and expectedVersion. Reuse requestId after an uncertain response. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.databases.transact('dbas_crm123', { "requestId": "write-customers-1", "mutations": [ { "type": "insert", "table": "customers", "values": { "id": "acme", "name": "Acme" } } ] }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/databases/dbas_crm123/transactions" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "requestId": "write-customers-1", "mutations": [ { "type": "insert", "table": "customers", "values": { "id": "acme", "name": "Acme" } } ] }' ``` Request example: ```json { "requestId": "write-customers-1", "mutations": [ { "type": "insert", "table": "customers", "values": { "id": "acme", "name": "Acme" } } ] } ``` Response example: ```json { "data": { "requestId": "write-customers-1", "versions": [ "11111111-1111-4111-8111-111111111111" ] } } ``` ### GET /v1/apps/{applicationId}/address — Get an app custom address Returns the current custom address. App admins also receive ready namespaces belonging to the organization. Organization admins connect namespaces in Organization Settings. Requires app read permission. Documentation: https://permute.ai/docs/api-reference/apps-address-get Operation ID: appsAddressGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the app’s custom address and available organization namespaces. Agent guidance: Requires app read. Organization namespaces and their setup status are returned only to app admins. Only live namespaces accept address assignments. An organization admin must first delegate a dedicated subdomain using the four NS records in Organization Settings. Permute manages routing and HTTPS after delegation. Retain address.url (or null when absent) as expectedUrl for an update. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.address('appl_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/address" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "address": null, "canManage": true, "namespaces": [ { "id": "domn_abcdefghijklmnopqrstuvwx", "hostname": "apps.example.com", "status": "live" } ] } } ``` ### PUT /v1/apps/{applicationId}/address — Set an app custom address Requires app admin permission and a live app. Select a ready organization namespace and one DNS label, or set address=null to remove the custom address. Supply expectedUrl from the current address (null if absent) for concurrency control. Names are unique within the namespace. Takes effect immediately without publishing app content or changing permissions. The generated Permute URL remains available. Documentation: https://permute.ai/docs/api-reference/apps-address-set Operation ID: appsAddressSet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Assign or remove a custom address for a live app. Agent guidance: Requires app admin and a ready organization namespace. Use a single DNS label and expectedUrl from apps.address; a conflict requires reloading and reviewing the current address. Send address: null to remove it. Changes take effect without publishing or further DNS setup. The generated Permute URL and existing permissions remain; each hostname has its own sign-in session. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.setAddress('appl_abcdefghijklmnopqrstuvwx', { "address": { "namespaceId": "domn_abcdefghijklmnopqrstuvwx", "label": "crm" }, "expectedUrl": null }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/address" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "address": { "namespaceId": "domn_abcdefghijklmnopqrstuvwx", "label": "crm" }, "expectedUrl": null }' ``` Request example: ```json { "address": { "namespaceId": "domn_abcdefghijklmnopqrstuvwx", "label": "crm" }, "expectedUrl": null } ``` Response example: ```json { "data": { "address": { "namespaceId": "domn_abcdefghijklmnopqrstuvwx", "label": "crm", "url": "https://crm.apps.example.com" }, "canManage": true, "namespaces": [ { "id": "domn_abcdefghijklmnopqrstuvwx", "hostname": "apps.example.com", "status": "live" } ] } } ``` ### GET /v1/apps/{applicationId}/access — Get app access settings Requires app read permission. Returns the current access mode and concurrency version, never a password or hash. Missing configuration defaults to Permute sign-in. Documentation: https://permute.ai/docs/api-reference/apps-access-get Operation ID: appsAccessGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read how visitors authenticate to an app. Agent guidance: Access settings are mutable and separate from app revisions. Retain the version for a conditional update; passwords and hashes are never returned. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.access('appl_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/access" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "mode": "permute", "version": 0, "canManage": true } } ``` ### PUT /v1/apps/{applicationId}/access — Set app access settings Requires app admin permission and the current access version. Permute sign-in uses existing user permissions and app grants. Password mode serves published frontend files only; all hosted queries and actions are denied. Passwords are hashed, never returned or versioned. Changes take effect immediately and invalidate existing sessions without publishing app content. A password must be supplied whenever password mode is saved; switching to Permute sign-in clears it. Documentation: https://permute.ai/docs/api-reference/apps-access-set Operation ID: appsAccessSet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Choose Permute sign-in or a shared-password demo, or rotate the demo password. Agent guidance: Requires app admin. Password demos expose published frontend files, including older releases, to anyone with the password. Use mocked data only; hosted queries and Business Actions are blocked. Changes invalidate sessions immediately. Read the current version before saving; never retry a conflict without reviewing the changed settings. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.setAccess('appl_abcdefghijklmnopqrstuvwx', { "mode": "password", "password": "example-demo-password", "expectedVersion": 0 }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/access" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "mode": "password", "password": "example-demo-password", "expectedVersion": 0 }' ``` Request example: ```json { "mode": "password", "password": "example-demo-password", "expectedVersion": 0 } ``` Response example: ```json { "data": { "mode": "password", "version": 1, "canManage": true } } ``` ### GET /v1/apps/{applicationId}/grants — List app resource grants Lists the app’s current explicit resource grants. Requires app read permission. Grants limit access; users still need their own permissions. Action invocation additionally requires a revision binding; sources use current grants without bindings. Documentation: https://permute.ai/docs/api-reference/apps-grants-list Operation ID: appsGrantsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect the app’s current resource access ceiling. Agent guidance: Grants are independent of app revisions. A resource must also be declared by the served revision, and the current user must have access. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.listGrants('appl_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/grants" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "resourceId": "actn_abcdefghijklmnopqrstuvwx", "resourceType": "businessAction", "level": "write", "expiresAt": null } ], "canManage": true } } ``` ### PUT /v1/apps/{applicationId}/grants/{resourceId} — Set or revoke an app resource grant Requires app admin permission and the shared target grant-management authority (target admin or the applicable workspace/organization admin). Supports resource-type wildcards, table grants, and expiry. Supply resourceType when resourceId is *. Cross-workspace targets are rejected. Set level to null to revoke the matching resource/type/table grant. Grants apply to every published revision immediately on subsequent authorization checks. Saving, publishing, and restoring revisions never recreate grants. These grants do not expose additional platform routes through app sessions. Documentation: https://permute.ai/docs/api-reference/apps-grants-set Operation ID: appsGrantsSet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Approve or revoke an app’s resource or subsystem access. Agent guidance: Requires app admin and the shared target grant-management authority. Use level: null to revoke with app admin alone. Grants never give the user additional permissions or expose raw access to an action’s underlying sources. For all resources of a type, use resourceId * and an explicit resourceType, such as dataset. Optional subResourceId and expiresAt use the normal permission semantics. A grant does not expose additional app-session APIs. Existing apps require explicit grants before their data API calls can run. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.setGrant('appl_abcdefghijklmnopqrstuvwx', 'actn_abcdefghijklmnopqrstuvwx', { "level": "write" }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/grants/actn_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "level": "write" }' ``` Request example: ```json { "level": "write" } ``` Response example: ```json { "data": { "items": [ { "resourceId": "actn_abcdefghijklmnopqrstuvwx", "resourceType": "businessAction", "level": "write", "expiresAt": null } ], "canManage": true } } ``` ### GET /v1/apps — List apps Find apps in the current workspace. Documentation: https://permute.ai/docs/api-reference/apps-list Operation ID: appsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Find apps in the current workspace. Agent guidance: The list contains resources the caller can read. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/apps" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "appl_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Operations portal", "description": "Internal operations portal", "deployment": { "status": "deploying", "url": "https://operations-portal-abcdefghijklmnopqrstuvwx.permute.site", "revisionId": "rvsn_draft123", "error": null }, "draft": { "revisionId": "rvsn_draft123", "sequence": 1 }, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } ] } } ``` ### POST /v1/apps — Create an app Creates a workspace-owned app and provisions a starter page that requires Permute sign-in and app read permission. The response reports deployment progress. Supply a stable key to retry creation without duplicating the app. Requires app write permission. Documentation: https://permute.ai/docs/api-reference/apps-create Operation ID: appsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Create an app with a hosted starter page. Agent guidance: Use a stable key when retrying creation. Poll get until deployment.status is live, then open deployment.url. Apps default to Permute sign-in and app read permission. An app admin can enable shared-password access for a frontend-only demo. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.create({ "name": "Operations portal", "key": "operations-portal", "description": "Internal operations portal" }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/apps" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "Operations portal", "key": "operations-portal", "description": "Internal operations portal" }' ``` Request example: ```json { "name": "Operations portal", "key": "operations-portal", "description": "Internal operations portal" } ``` Response example: ```json { "data": { "id": "appl_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Operations portal", "description": "Internal operations portal", "deployment": { "status": "deploying", "url": "https://operations-portal-abcdefghijklmnopqrstuvwx.permute.site", "revisionId": "rvsn_draft123", "error": null }, "draft": { "revisionId": "rvsn_draft123", "sequence": 1 }, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### GET /v1/apps/{applicationId} — Get an app Read app details, deployment progress, and its current editing revision. Documentation: https://permute.ai/docs/api-reference/apps-get Operation ID: appsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read app details, deployment progress, and its current editing revision. Agent guidance: Pass the returned draft ref to update for concurrency control. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.get('appl_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "appl_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Operations portal", "description": "Internal operations portal", "deployment": { "status": "deploying", "url": "https://operations-portal-abcdefghijklmnopqrstuvwx.permute.site", "revisionId": "rvsn_draft123", "error": null }, "draft": { "revisionId": "rvsn_draft123", "sequence": 1 }, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### PATCH /v1/apps/{applicationId} — Update app settings Saves app settings with a compare-and-set draft ref. Optional login settings customize the side panel image, header, subheader, title, subtitle and primary/secondary hex colors for both access methods. Images use verified app uploads (PNG, JPEG or WebP, up to 2 MiB). Set login to null to restore the default appearance. Optional chat.enabled controls the platform assistant overlay (defaults to true for signed-in apps); password demos receive no assistant. Publish separately to apply settings; access policies and sessions are unchanged. Documentation: https://permute.ai/docs/api-reference/apps-update Operation ID: appsUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Change the app name, description, favicon or login appearance. Agent guidance: Read the latest draft ref first. A stale ref returns a conflict. Upload login images through apps.uploadFiles (PNG, JPEG or WebP, up to 2 MiB), then reference the returned descriptor in login.sidePanelImage. Upload a PNG favicon up to 256 KiB the same way and set favicon to its descriptor. Publish the saved draft to apply appearance; login: null restores default login styling, and favicon: null removes the icon override. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.update('appl_abcdefghijklmnopqrstuvwx', { "name": "Operations portal", "description": "Internal operations portal", "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } }); ``` Raw REST example: ```bash curl -X PATCH "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "name": "Operations portal", "description": "Internal operations portal", "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } }' ``` Request example: ```json { "name": "Operations portal", "description": "Internal operations portal", "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } } ``` Response example: ```json { "data": { "id": "appl_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Operations portal", "description": "Internal operations portal", "deployment": { "status": "deploying", "url": "https://operations-portal-abcdefghijklmnopqrstuvwx.permute.site", "revisionId": "rvsn_draft123", "error": null }, "draft": { "revisionId": "rvsn_draft456", "sequence": 2 }, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### POST /v1/apps/{applicationId}/uploads — Prepare app file uploads Returns ten-minute signed PUT URLs bound to each file SHA-256 and byte size. Send every returned header. A 412 means the immutable file already exists; saving verifies it. Limits: 200 files, 4 MiB per file, 50 MiB total. Requires app write permission. This does not save or publish a revision. Documentation: https://permute.ai/docs/api-reference/apps-uploads-create Operation ID: appsUploadsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Upload a compiled app before saving its draft. Agent guidance: Prefer client.apps.uploadFiles, which hashes bytes and performs signed PUTs. Send only returned headers to storage, never your API key. A 412 is safe to continue; saveSite verifies the stored checksum. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.prepareUploads('appl_abcdefghijklmnopqrstuvwx', { "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ] }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/uploads" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ] }' ``` Request example: ```json { "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ] } ``` Response example: ```json { "data": { "uploads": [ { "path": "index.html", "url": "https://uploads.example.com/signed-file", "headers": { "if-none-match": "*", "content-length": "1024", "x-amz-checksum-sha256": "checksum-from-response" } } ] } } ``` ### PUT /v1/apps/{applicationId}/site — Save an app site draft Verifies uploaded files and saves a complete manifest with a compare-and-set draft ref. Requires index.html. Build with relative asset URLs (Vite base ./); HTML receives a revision-specific base URL. Use spa=true for extensionless HTML navigation fallback. Omitted files are removed from the new draft only. Optional actions pin published action revisions as dependencies of the same app revision; omit it to preserve bindings or send [] to remove them. Query access follows current app grants and user permissions; source grant changes take effect without republishing. Saving and publishing require invocation permission on each selected action. App users still need their own action permissions. Publication additionally requires explicit app grants for every bound action. App admins manage these grants separately; saving or restoring a draft never grants access. Publish through the shared resource versioning API. No build or publication occurs here. Documentation: https://permute.ai/docs/api-reference/apps-site-update Operation ID: appsSiteUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save a complete uploaded site as the app draft. Agent guidance: Use the draft ref read before editing; a conflict requires reconciliation. Build locally with relative asset URLs. Optional actions pins published action revisions with the site; omit it to preserve bindings or send [] to clear them. Binding grants no additional user permissions. Publication requires current app resource grants; approve them separately with apps.setGrant using app and target admin access. Save does not publish: use client.revisions.publish with the version token returned by revisions.list, checking that its draft matches your saved revision. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.saveSite('appl_abcdefghijklmnopqrstuvwx', { "site": { "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ], "spa": true }, "actions": [ { "actionId": "actn_abcdefghijklmnopqrstuvwx", "revisionId": "rvsn_publishedaction" } ], "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx/site" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "site": { "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ], "spa": true }, "actions": [ { "actionId": "actn_abcdefghijklmnopqrstuvwx", "revisionId": "rvsn_publishedaction" } ], "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } }' ``` Request example: ```json { "site": { "files": [ { "path": "index.html", "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "size": 1024 } ], "spa": true }, "actions": [ { "actionId": "actn_abcdefghijklmnopqrstuvwx", "revisionId": "rvsn_publishedaction" } ], "expectedDraft": { "revisionId": "rvsn_draft123", "sequence": 1 } } ``` Response example: ```json { "data": { "id": "appl_abcdefghijklmnopqrstuvwx", "workspaceId": "wksp_finance123", "name": "Operations portal", "description": "Internal operations portal", "deployment": { "status": "deploying", "url": "https://operations-portal-abcdefghijklmnopqrstuvwx.permute.site", "revisionId": "rvsn_draft123", "error": null }, "draft": { "revisionId": "rvsn_draft123", "sequence": 1 }, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### DELETE /v1/apps/{applicationId} — Delete an app Requests asynchronous deletion of the hosted app. Poll get until the app returns 404. Requires admin permission. Documentation: https://permute.ai/docs/api-reference/apps-delete Operation ID: appsDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Delete an app and its hosted deployment. Agent guidance: Requires app admin permission. Deletion is asynchronous: poll get until the app returns 404. A failed deployment needs administrator recovery. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.apps.delete('appl_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/apps/appl_abcdefghijklmnopqrstuvwx" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "appl_abcdefghijklmnopqrstuvwx", "deleted": false } } ``` ### GET /v1/resources/{resourceId}/revisions — Read resource history and version state Returns immutable revisions newest first and an opaque version token for mutations. Supports app manifests, operation definitions, write-sync definitions, database schemas, business action definitions, and dashboard source (use the resourceId from dashboards.getSource). Database and action definition access requires admin permission on that resource. Data Flow roots expose publish state and capabilities, with no graph revision history. Documentation: https://permute.ai/docs/api-reference/resource-revisions-list Operation ID: resourceRevisionsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect history and get a version token before restoring or publishing. Agent guidance: Use operation IDs for handler history and write-sync IDs for definition history. For dashboard source, use resourceId from dashboards.getSource. Check capabilities: Data Flow roots can publish but have no graph history. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.revisions.list('oper_revenue123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/resources/oper_revenue123/revisions" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "resourceId": "oper_revenue123", "version": "opaque-version-token", "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": null, "capabilities": { "history": true, "restore": true, "publish": true }, "items": [ { "id": "rvsn_code123", "parentRevisionId": null, "dependencyRevisionIds": [], "actor": { "type": "apiKey", "id": "apik_agent123" }, "message": "Saved operation source.", "createdAt": "2026-09-11T12:00:00.000Z" } ] } } ``` ### GET /v1/resources/{resourceId}/revisions/{revisionId} — Read a complete historical revision Returns the logical value of a revision belonging to this resource. Operation values contain source, instructions, source IDs, and dependency operation IDs. Write-sync values contain the directional definition. Dashboard values contain source and its data contract. App values contain settings and the complete site manifest; file bytes are stored separately. Database values contain managed table schemas. Business action values contain code, contracts, and selected sources pinned to schema revisions. Documentation: https://permute.ai/docs/api-reference/resource-revisions-get Operation ID: resourceRevisionsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect the complete saved value before choosing a revision to restore. Agent guidance: Revisions belong to one resource. A revision ID from another resource is rejected. Write-sync values contain the JSON definition; runtime enablement, record links and delivery attempts are not versioned. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.revisions.get('oper_revenue123', 'rvsn_code123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/resources/oper_revenue123/revisions/rvsn_code123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "rvsn_code123", "parentRevisionId": null, "dependencyRevisionIds": [], "actor": { "type": "apiKey", "id": "apik_agent123" }, "message": "Saved operation source.", "createdAt": "2026-09-11T12:00:00.000Z", "resourceId": "oper_revenue123", "value": { "source": "async function handler(event) { return {}; } module.exports = { handler };", "instructions": "Calculate revenue", "referencedDataSourceIds": [], "resolvedDataSourceIds": [], "dependencyOperationIds": [] } } } ``` ### POST /v1/resources/{resourceId}/revisions/{revisionId}/restore — Restore a revision as the current draft Pass the version token from the latest history read. Preserves history and does not execute or publish. App restores replace the draft manifest and settings; publish separately to roll back the live site. Database restores change only the definition draft, never customer records or live tables. Operation inputs must still be accessible and dependency connections must match the current graph. Data Flow child restores invalidate current-run eligibility. Dashboard restores validate source and restore its saved contract; dashboard source changes take effect immediately. Documentation: https://permute.ai/docs/api-reference/resource-revisions-restore Operation ID: resourceRevisionsRestore Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Recover a previous definition without running or publishing it. Agent guidance: Read history, inspect the target, then pass the returned version token. On 409, read again and reassess. Write-sync restore requires a paused sync and preserves current identity restrictions; it neither publishes nor sends records. Operation dependencies must match the current graph. Dashboard source takes effect immediately and restores its saved contract, not old data. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const history = await client.revisions.list('oper_revenue123'); const result = await client.revisions.restore('oper_revenue123', 'rvsn_code123', { expectedVersion: history.version }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/resources/oper_revenue123/revisions/rvsn_code123/restore" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "expectedVersion": "opaque-version-token" }' ``` Request example: ```json { "expectedVersion": "opaque-version-token" } ``` Response example: ```json { "data": { "resourceId": "oper_revenue123", "version": "opaque-version-token", "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": null, "capabilities": { "history": true, "restore": true, "publish": true } } } ``` ### POST /v1/resources/{resourceId}/publish — Publish the current resource draft Pass the version token from the latest history read. Database publication applies additive managed tables before moving the published ref; existing tables cannot be changed. Business actions publish independently after validating code, contracts, and selected sources against published schemas. Operation publication requires a successful run, or a completed Data Flow through its existing graph checks, and enables delivery and its configured schedule without starting a run. Publish Data Flow children through their parent. For write syncs, also pass runId for a completed preview of the current definition captured by the same API key; publication starts external delivery and new captures every 24 hours under that key. For apps, publication atomically makes the current draft live at its existing URL; no build or CloudFront rollout occurs. Dashboard source has no separate publication step. Documentation: https://permute.ai/docs/api-reference/resources-publish Operation ID: resourcesPublish Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Publish a tested operation, Data Flow or write-sync definition. Agent guidance: Read the version token after testing. For write syncs, include runId from a completed preview of the exact draft, captured by the same API key: publication starts external delivery immediately and enables captures every 24 hours. Operation and Data Flow publication enables the configured schedule without starting a run; publish children through their parent flow. Dashboard source has no separate publish action. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const history = await client.revisions.list('oper_revenue123'); const result = await client.revisions.publish('oper_revenue123', { expectedVersion: history.version }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/resources/oper_revenue123/publish" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "expectedVersion": "opaque-version-token" }' ``` Request example: ```json { "expectedVersion": "opaque-version-token" } ``` Response example: ```json { "data": { "resourceId": "oper_revenue123", "version": "opaque-version-token", "draft": { "revisionId": "rvsn_code123", "sequence": 1 }, "published": { "revisionId": "rvsn_code123", "sequence": 1 }, "capabilities": { "history": true, "restore": true, "publish": true } } } ``` ### GET /v1/write-syncs — List write syncs Lists accessible write syncs whose source and destination the key can read. Documentation: https://permute.ai/docs/api-reference/write-syncs-list Operation ID: writeSyncsList Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Find saved directional syncs in a workspace. Agent guidance: Only syncs with readable sources and destinations are returned. Authoring uses REST or the SDK; these controls are not exposed through MCP. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.list(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/write-syncs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "wsyn_contacts123", "name": "Contacts → Quo contacts", "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "version": 1, "isEnabled": false, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } ] } } ``` ### POST /v1/write-syncs — Create a write sync Creates a paused directional sync. Requires write-sync and destination write permission plus source and destination read access. Fields map destination field names to source column names. Operation input revisionId describes the configured schema; each new capture follows the current published revision. Does not send records. Documentation: https://permute.ai/docs/api-reference/write-syncs-create Operation ID: writeSyncsCreate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Save a paused sync from a published Data Table to Quo contacts. Agent guidance: Publish and run the source first. Use operations.get for its published revision and sources.get for tables and fields; configuration and preview validate the published output. Map destination fields to source columns. Requires write-sync write, destination read/write and source read access. Creation sends no records. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.create({ "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } } }); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/write-syncs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } } }' ``` Request example: ```json { "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } } } ``` Response example: ```json { "data": { "id": "wsyn_contacts123", "name": "Contacts → Quo contacts", "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "version": 1, "isEnabled": false, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### GET /v1/write-syncs/destinations — Discover writable destinations Lists connectors the key can read and write, supported destination objects, and their field contracts. Quo currently supports creating contacts only. Documentation: https://permute.ai/docs/api-reference/write-syncs-destinations Operation ID: writeSyncsDestinations Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Discover destinations and their supported field contracts. Agent guidance: Quo creates contacts only. Matching uses its last read sync, so refresh that connector before previewing if newer contacts may exist. Unchanged matches are skipped; changed linked records and ambiguous matches are blocked. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.destinations(); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/write-syncs/destinations" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "conn_quo123", "name": "Quo", "provider": "quo", "objects": [ { "object": "contacts", "scope": "", "label": "Contacts (create only)", "fields": [ { "name": "firstName", "label": "First name", "type": "string" }, { "name": "lastName", "label": "Last name", "type": "string" }, { "name": "company", "label": "Company", "type": "string" }, { "name": "role", "label": "Role", "type": "string" }, { "name": "emails", "label": "Email addresses", "type": "array" }, { "name": "phoneNumbers", "label": "Phone numbers", "type": "array" } ] } ] } ] } } ``` ### GET /v1/write-syncs/{writeSyncId} — Get a write sync Returns its saved definition, version, enablement, and whether identity fields are locked after delivery. Documentation: https://permute.ai/docs/api-reference/write-syncs-get Operation ID: writeSyncsGet Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Read the current definition and version before editing. Agent guidance: Use identityLocked to determine whether source, destination, key, matching field and managed field names are fixed after an execution request. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.get('wsyn_contacts123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/write-syncs/wsyn_contacts123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "wsyn_contacts123", "name": "Contacts → Quo contacts", "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "version": 1, "isEnabled": false, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z", "automaticEnabled": false, "identityLocked": false } } ``` ### PUT /v1/write-syncs/{writeSyncId} — Update a write sync Replaces a paused definition using expectedVersion from the latest read. Invalidates previous previews. Source, destination, stable key, matching field, and managed fields become fixed once execution has been requested. Documentation: https://permute.ai/docs/api-reference/write-syncs-update Operation ID: writeSyncsUpdate Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Replace the complete definition of a paused sync. Agent guidance: Read the latest numeric version and pass expectedVersion. Saving creates a definition revision and invalidates previous previews; review a new preview before publishing through revisions.publish with its runId and the version token from revisions.list. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.update('wsyn_contacts123', { "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "expectedVersion": 1 }); ``` Raw REST example: ```bash curl -X PUT "https://api.permute.ai/v1/write-syncs/wsyn_contacts123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{ "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "expectedVersion": 1 }' ``` Request example: ```json { "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "expectedVersion": 1 } ``` Response example: ```json { "data": { "id": "wsyn_contacts123", "name": "Contacts → Quo contacts", "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "version": 2, "isEnabled": false, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### DELETE /v1/write-syncs/{writeSyncId} — Delete a write sync Requires write-sync admin permission. Disables and soft-deletes the sync, releasing the relationship. Retains run history and record links; destination records are not deleted. In-flight writes may finish. Documentation: https://permute.ai/docs/api-reference/write-syncs-delete Operation ID: writeSyncsDelete Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Retire a sync and release its source/destination relationship. Agent guidance: Requires write-sync admin permission. Run history and record links remain, and destination records are not deleted. Requests already sent may finish. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.delete('wsyn_contacts123'); ``` Raw REST example: ```bash curl -X DELETE "https://api.permute.ai/v1/write-syncs/wsyn_contacts123" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "deleted": true } } ``` ### POST /v1/write-syncs/{writeSyncId}/preview — Test a write sync Queues a read-only preview of the full source in bounded pages. Poll getRun until previewComplete or previewError, then inspect every page. Does not write externally, even when the sync is published. Operation outputs are fixed for the capture; direct connector sources use their last synced data. Documentation: https://permute.ai/docs/api-reference/write-syncs-preview Operation ID: writeSyncsPreview Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Capture proposed changes without writing to the destination. Agent guidance: Poll getRun until previewComplete or previewError, then inspect every page before approval. Preview remains read-only on enabled syncs. Direct connector sources use their last read sync. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.preview('wsyn_contacts123'); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/write-syncs/wsyn_contacts123/preview" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "wsrn_abcdefghijklmnopqrstuvwx", "writeSyncId": "wsyn_contacts123", "definitionVersion": 1, "definitionRevisionId": "rvsn_sync123", "sourceOperation": { "runId": "oprn_contacts123", "revisionId": "rvsn_contacts123" }, "previewComplete": false, "recordCount": 0, "page": 0, "pageCount": 0, "records": [] } } ``` ### POST /v1/write-syncs/{writeSyncId}/pause — Pause a write sync Stops future writes and scheduled runs. Retains the relationship and record links. Requests already sent may finish. To resume, capture and approve a new preview. Documentation: https://permute.ai/docs/api-reference/write-syncs-pause Operation ID: writeSyncsPause Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Stop scheduled runs and further writes. Agent guidance: Already-sent requests may finish. Pausing preserves links and the relationship. To resume, capture and approve a new preview. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.pause('wsyn_contacts123'); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/write-syncs/wsyn_contacts123/pause" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "wsyn_contacts123", "name": "Contacts → Quo contacts", "definition": { "source": { "resourceId": "oper_contacts123", "object": "contacts", "scope": "" }, "destination": { "resourceId": "conn_quo123", "object": "contacts", "scope": "" }, "input": { "type": "operation", "operationId": "oper_contacts123", "revisionId": "rvsn_contacts123", "table": "contacts" }, "keyColumn": "email", "fields": { "emails": "email", "firstName": "first_name", "lastName": "last_name" } }, "version": 1, "isEnabled": false, "createdAt": "2026-09-30T12:00:00.000Z", "updatedAt": "2026-09-30T12:00:00.000Z" } } ``` ### GET /v1/write-syncs/{writeSyncId}/runs — List write sync runs Returns the latest 20 captures, newest first, including tests and real syncs. Execution identifies delivery; automatic records recurring approval. Publishing promotes the selected preview into a real run. Documentation: https://permute.ai/docs/api-reference/write-syncs-list-runs Operation ID: writeSyncsListRuns Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Inspect the latest 20 captures, newest first. Agent guidance: Tests and delivered runs share this list. Check execution for delivery and automatic for recurring approval; publishing promotes the selected preview into a real run. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.listRuns('wsyn_contacts123'); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/write-syncs/wsyn_contacts123/runs" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "items": [ { "id": "wsrn_abcdefghijklmnopqrstuvwx", "previewComplete": true, "recordCount": 1, "createdAt": "2026-09-30T12:01:00.000Z" } ] } } ``` ### GET /v1/write-syncs/{writeSyncId}/runs/{runId} — Inspect write sync results Returns one zero-based page of captured values and per-record outcomes. Poll until previewComplete or an error; inspect all pageCount pages before approval. A completed execution may contain rejected or blocked records; check record statuses. Unconfirmed writes are never blindly resent. Documentation: https://permute.ai/docs/api-reference/write-syncs-get-run Operation ID: writeSyncsGetRun Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Review captured values and per-record outcomes on one zero-based page. Agent guidance: Read all pageCount pages after preview completion. After delivery, inspect statuses even when execution.completedAt is set: blocked, rejected or unknown records require attention. Unknown writes are never blindly resent. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.getRun('wsyn_contacts123', 'wsrn_abcdefghijklmnopqrstuvwx', { page: 0 }); ``` Raw REST example: ```bash curl -X GET "https://api.permute.ai/v1/write-syncs/wsyn_contacts123/runs/wsrn_abcdefghijklmnopqrstuvwx?page=0" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "id": "wsrn_abcdefghijklmnopqrstuvwx", "writeSyncId": "wsyn_contacts123", "definitionVersion": 1, "definitionRevisionId": "rvsn_sync123", "sourceOperation": { "runId": "oprn_contacts123", "revisionId": "rvsn_contacts123" }, "previewComplete": true, "recordCount": 1, "page": 0, "pageCount": 1, "records": [ { "key": "ada@example.com", "values": { "emails": "ada@example.com", "firstName": "Ada", "lastName": "Lovelace" }, "action": "create", "status": "pending" } ] } } ``` ### POST /v1/write-syncs/{writeSyncId}/runs/{runId}/execute — Execute a write sync now Sends a completed preview captured by this same API key. Requires the current definition to be enabled. Uses captured values, does not refresh the source, and does not change the daily schedule. Poll getRun for results; retrying never resends unconfirmed writes. Documentation: https://permute.ai/docs/api-reference/write-syncs-execute Operation ID: writeSyncsExecute Scope: workspace Authentication: apiKey Stability: beta SDK supported: yes MCP promoted: no When to use: Send an completed capture while the current definition is enabled. Agent guidance: Use the API key that captured the run. Execution sends its captured values without refreshing sources or changing the daily schedule. Poll getRun and inspect every page for outcomes. Preferred SDK example: ```typescript import { PermuteClient } from '@permute/sdk'; const orgClient = new PermuteClient({ apiKey: process.env.PERMUTE_API_KEY!, }); const client = orgClient.withWorkspace(process.env.PERMUTE_WORKSPACE_ID!); const result = await client.writeSyncs.execute('wsyn_contacts123', 'wsrn_abcdefghijklmnopqrstuvwx'); ``` Raw REST example: ```bash curl -X POST "https://api.permute.ai/v1/write-syncs/wsyn_contacts123/runs/wsrn_abcdefghijklmnopqrstuvwx/execute" \ -H "Authorization: Bearer $PERMUTE_API_KEY" \ -H "X-Workspace-Id: $PERMUTE_WORKSPACE_ID" ``` Response example: ```json { "data": { "runId": "wsrn_abcdefghijklmnopqrstuvwx", "queued": true } } ```