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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.12.0"
".": "0.13.0"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 29
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-876b3cc61d59eee7f50b21239e174146afb6176473af1f7c6ac6ce8900e40b9c.yml
openapi_spec_hash: f0acdda9bda9e4733d298cf9082b58e7
config_hash: 0b48961f5f0b4dca3a9a4342cd42bd51
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/formbricks/hub-1ecd882ecd3b1995b6712bb140c7b9e6c6002963b311031d49146101af445dd1.yml
openapi_spec_hash: 529cbb1bf065a81cbf310160c330dbe6
config_hash: a4efd21029f0fb9bf9e9abd69b06ac84
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.13.0 (2026-08-19)

Full Changelog: [v0.12.0...v0.13.0](https://github.com/formbricks/hub-typescript/compare/v0.12.0...v0.13.0)

### Features

* add a records-scoped tenant purge endpoint (ENG-2129) ([a6b9afa](https://github.com/formbricks/hub-typescript/commit/a6b9afa33356b7f582c11c0f56d6d242153ee0da))

## 0.12.0 (2026-08-12)

Full Changelog: [v0.11.0...v0.12.0](https://github.com/formbricks/hub-typescript/compare/v0.11.0...v0.12.0)
Expand Down
2 changes: 2 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ Methods:
Types:

- <code><a href="./src/resources/tenants/tenants.ts">TenantDeleteDataResponse</a></code>
- <code><a href="./src/resources/tenants/tenants.ts">TenantPurgeFeedbackRecordsResponse</a></code>

Methods:

- <code title="delete /v1/tenants/{tenant_id}/data">client.tenants.<a href="./src/resources/tenants/tenants.ts">deleteData</a>(tenantID) -> TenantDeleteDataResponse</code>
- <code title="delete /v1/tenants/{tenant_id}/feedback-records">client.tenants.<a href="./src/resources/tenants/tenants.ts">purgeFeedbackRecords</a>(tenantID) -> TenantPurgeFeedbackRecordsResponse</code>

## Settings

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formbricks/hub",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official TypeScript library for the Formbricks Hub API",
"author": "Formbricks Hub <hola@formbricks.com>",
"types": "dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "@formbricks/hub-mcp",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official MCP Server for the Formbricks Hub API",
"author": {
"name": "Formbricks Hub",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formbricks/hub-mcp",
"version": "0.12.0",
"version": "0.13.0",
"description": "The official MCP Server for the Formbricks Hub API",
"author": "Formbricks Hub <hola@formbricks.com>",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-server/src/code-tool-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const fuse = new Fuse(
'client.webhooks.retrieve',
'client.webhooks.update',
'client.tenants.deleteData',
'client.tenants.purgeFeedbackRecords',
'client.tenants.settings.retrieve',
'client.tenants.settings.update',
'client.taxonomy.listFields',
Expand Down
25 changes: 25 additions & 0 deletions packages/mcp-server/src/local-docs-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,31 @@ const EMBEDDED_METHODS: MethodEntry[] = [
},
},
},
{
name: 'purge_feedback_records',
endpoint: '/v1/tenants/{tenant_id}/feedback-records',
httpMethod: 'delete',
summary: 'Purge all feedback records for a tenant',
description:
'Permanently deletes every feedback record for the specified tenant_id, everything derived from\nthose records — embeddings and the enrichment stored on each record (sentiment, emotions,\ntranslations) — and the taxonomy built on them: runs, clusters, nodes, cluster memberships,\nactive-run pointers and node events.\n\nThis is intended for emptying a dataset that stays in use, so it removes the tenant\'s DATA but\nnever its CONFIGURATION: webhooks and tenant settings are left untouched. That is the difference\nfrom `DELETE /v1/tenants/{tenant_id}/data`, which additionally deletes both and is meant for\noffboarding a deprovisioned tenant.\n\nThe taxonomy is removed rather than preserved because it describes records that no longer exist:\nits per-node counts are derived from memberships and would all read zero, while a run\'s own stored\ncounters (`record_count`, `cluster_count`, a cluster\'s `size`) are historical and would keep\nadvertising the old numbers. A new taxonomy can be generated once the dataset has enough feedback\nagain; manual node renames and removals are per-run and do not survive a regeneration in any case.\n\nThe tenant is a required path segment, so it cannot be omitted the way a filter on a collection\ndelete could be — dropping it routes elsewhere rather than widening the operation to every tenant.\n\nAsynchronous: the request schedules the purge and returns 202 immediately, because the deletion is\nunbounded and can outlive a request. There is therefore no deleted count in the response. Poll\n`GET /v1/feedback-records/count?tenant_id=...` to observe progress.\n\nThe purge removes only the records that existed when it started — it takes a high-water mark up\nfront — so feedback ingested while it runs is never deleted. For a dataset that is no longer\nreceiving feedback the count reaching zero means the purge is complete; for one still ingesting,\na nonzero count is those newer records. Note this means the count alone cannot distinguish\n"finished" from "failed" on an active dataset.\n\nIdempotent and safe to repeat. Requesting a purge while one is already running for the same tenant\njoins the running purge rather than queueing a second one, and still returns 202. A purge requested\nafter an earlier one finished starts a new run.\n\nRecords are deleted in committed batches, so a purge interrupted by a restart or a timeout keeps\nthe progress it made and resumes on retry; the taxonomy is removed in a final step once the records\nare gone. While each step runs, the tenant\'s write lock is held exclusively and Hub-owned writes\nfor that tenant are rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released\nbetween steps, and writes for other tenants are never affected. This does not apply to the request below — scheduling a purge takes no lock, so\nthis endpoint does not return 409. A batch that cannot acquire the lock is retried by the job\nqueue without any caller action, up to a bounded number of attempts.\n\nNo webhook events are published for a purge, and no webhooks are deleted. Enrichment jobs already\nqueued for purged records no-op when they run, since the record they reference is gone.\n',
stainlessPath: '(resource) tenants > (method) purge_feedback_records',
qualified: 'client.tenants.purgeFeedbackRecords',
params: ['tenant_id: string;'],
response: "{ status: 'accepted'; tenant_id: string; message?: string; }",
markdown:
"## purge_feedback_records\n\n`client.tenants.purgeFeedbackRecords(tenant_id: string): { status: 'accepted'; tenant_id: string; message?: string; }`\n\n**delete** `/v1/tenants/{tenant_id}/feedback-records`\n\nPermanently deletes every feedback record for the specified tenant_id, everything derived from\nthose records — embeddings and the enrichment stored on each record (sentiment, emotions,\ntranslations) — and the taxonomy built on them: runs, clusters, nodes, cluster memberships,\nactive-run pointers and node events.\n\nThis is intended for emptying a dataset that stays in use, so it removes the tenant's DATA but\nnever its CONFIGURATION: webhooks and tenant settings are left untouched. That is the difference\nfrom `DELETE /v1/tenants/{tenant_id}/data`, which additionally deletes both and is meant for\noffboarding a deprovisioned tenant.\n\nThe taxonomy is removed rather than preserved because it describes records that no longer exist:\nits per-node counts are derived from memberships and would all read zero, while a run's own stored\ncounters (`record_count`, `cluster_count`, a cluster's `size`) are historical and would keep\nadvertising the old numbers. A new taxonomy can be generated once the dataset has enough feedback\nagain; manual node renames and removals are per-run and do not survive a regeneration in any case.\n\nThe tenant is a required path segment, so it cannot be omitted the way a filter on a collection\ndelete could be — dropping it routes elsewhere rather than widening the operation to every tenant.\n\nAsynchronous: the request schedules the purge and returns 202 immediately, because the deletion is\nunbounded and can outlive a request. There is therefore no deleted count in the response. Poll\n`GET /v1/feedback-records/count?tenant_id=...` to observe progress.\n\nThe purge removes only the records that existed when it started — it takes a high-water mark up\nfront — so feedback ingested while it runs is never deleted. For a dataset that is no longer\nreceiving feedback the count reaching zero means the purge is complete; for one still ingesting,\na nonzero count is those newer records. Note this means the count alone cannot distinguish\n\"finished\" from \"failed\" on an active dataset.\n\nIdempotent and safe to repeat. Requesting a purge while one is already running for the same tenant\njoins the running purge rather than queueing a second one, and still returns 202. A purge requested\nafter an earlier one finished starts a new run.\n\nRecords are deleted in committed batches, so a purge interrupted by a restart or a timeout keeps\nthe progress it made and resumes on retry; the taxonomy is removed in a final step once the records\nare gone. While each step runs, the tenant's write lock is held exclusively and Hub-owned writes\nfor that tenant are rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released\nbetween steps, and writes for other tenants are never affected. This does not apply to the request below — scheduling a purge takes no lock, so\nthis endpoint does not return 409. A batch that cannot acquire the lock is retried by the job\nqueue without any caller action, up to a bounded number of attempts.\n\nNo webhook events are published for a purge, and no webhooks are deleted. Enrichment jobs already\nqueued for purged records no-op when they run, since the record they reference is gone.\n\n\n### Parameters\n\n- `tenant_id: string`\n\n### Returns\n\n- `{ status: 'accepted'; tenant_id: string; message?: string; }`\n\n - `status: 'accepted'`\n - `tenant_id: string`\n - `message?: string`\n\n### Example\n\n```typescript\nimport FormbricksHub from '@formbricks/hub';\n\nconst client = new FormbricksHub();\n\nconst response = await client.tenants.purgeFeedbackRecords('org-123');\n\nconsole.log(response);\n```",
perLanguage: {
typescript: {
method: 'client.tenants.purgeFeedbackRecords',
example:
"import FormbricksHub from '@formbricks/hub';\n\nconst client = new FormbricksHub({\n apiKey: process.env['HUB_API_KEY'], // This is the default and can be omitted\n});\n\nconst response = await client.tenants.purgeFeedbackRecords('org-123');\n\nconsole.log(response.tenant_id);",
},
http: {
example:
'curl http://localhost:8080/v1/tenants/$TENANT_ID/feedback-records \\\n -X DELETE \\\n -H "Authorization: Bearer $HUB_API_KEY"',
},
},
},
{
name: 'retrieve',
endpoint: '/v1/tenants/{tenant_id}/settings',
Expand Down
6 changes: 6 additions & 0 deletions packages/mcp-server/src/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ export const sdkMethods: SdkMethod[] = [
httpMethod: 'delete',
httpPath: '/v1/tenants/{tenant_id}/data',
},
{
clientCallName: 'client.tenants.purgeFeedbackRecords',
fullyQualifiedName: 'tenants.purgeFeedbackRecords',
httpMethod: 'delete',
httpPath: '/v1/tenants/{tenant_id}/feedback-records',
},
{
clientCallName: 'client.tenants.settings.retrieve',
fullyQualifiedName: 'tenants.settings.retrieve',
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const newMcpServer = async ({
new McpServer(
{
name: 'formbricks_hub_api',
version: '0.12.0',
version: '0.13.0',
},
{
instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }),
Expand Down
12 changes: 10 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ import {
TaxonomyListFieldsParams,
TaxonomyListFieldsResponse,
} from './resources/taxonomy/taxonomy';
import { TenantDeleteDataResponse, Tenants } from './resources/tenants/tenants';
import {
TenantDeleteDataResponse,
TenantPurgeFeedbackRecordsResponse,
Tenants,
} from './resources/tenants/tenants';
import { type Fetch } from './internal/builtin-types';
import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
import { FinalRequestOptions, RequestOptions } from './internal/request-options';
Expand Down Expand Up @@ -837,7 +841,11 @@ export declare namespace FormbricksHub {
type WebhookListParams as WebhookListParams,
};

export { Tenants as Tenants, type TenantDeleteDataResponse as TenantDeleteDataResponse };
export {
Tenants as Tenants,
type TenantDeleteDataResponse as TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse,
};

export {
Taxonomy as Taxonomy,
Expand Down
6 changes: 5 additions & 1 deletion src/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export {
type TaxonomyListFieldsResponse,
type TaxonomyListFieldsParams,
} from './taxonomy/taxonomy';
export { Tenants, type TenantDeleteDataResponse } from './tenants/tenants';
export {
Tenants,
type TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse,
} from './tenants/tenants';
export {
Webhooks,
type WebhookCreateResponse,
Expand Down
2 changes: 1 addition & 1 deletion src/resources/tenants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export {
type SettingUpdateResponse,
type SettingUpdateParams,
} from './settings';
export { Tenants, type TenantDeleteDataResponse } from './tenants';
export { Tenants, type TenantDeleteDataResponse, type TenantPurgeFeedbackRecordsResponse } from './tenants';
94 changes: 93 additions & 1 deletion src/resources/tenants/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,76 @@ export class Tenants extends APIResource {
deleteData(tenantID: string, options?: RequestOptions): APIPromise<TenantDeleteDataResponse> {
return this._client.delete(path`/v1/tenants/${tenantID}/data`, options);
}

/**
* Permanently deletes every feedback record for the specified tenant_id,
* everything derived from those records — embeddings and the enrichment stored on
* each record (sentiment, emotions, translations) — and the taxonomy built on
* them: runs, clusters, nodes, cluster memberships, active-run pointers and node
* events.
*
* This is intended for emptying a dataset that stays in use, so it removes the
* tenant's DATA but never its CONFIGURATION: webhooks and tenant settings are left
* untouched. That is the difference from `DELETE /v1/tenants/{tenant_id}/data`,
* which additionally deletes both and is meant for offboarding a deprovisioned
* tenant.
*
* The taxonomy is removed rather than preserved because it describes records that
* no longer exist: its per-node counts are derived from memberships and would all
* read zero, while a run's own stored counters (`record_count`, `cluster_count`, a
* cluster's `size`) are historical and would keep advertising the old numbers. A
* new taxonomy can be generated once the dataset has enough feedback again; manual
* node renames and removals are per-run and do not survive a regeneration in any
* case.
*
* The tenant is a required path segment, so it cannot be omitted the way a filter
* on a collection delete could be — dropping it routes elsewhere rather than
* widening the operation to every tenant.
*
* Asynchronous: the request schedules the purge and returns 202 immediately,
* because the deletion is unbounded and can outlive a request. There is therefore
* no deleted count in the response. Poll
* `GET /v1/feedback-records/count?tenant_id=...` to observe progress.
*
* The purge removes only the records that existed when it started — it takes a
* high-water mark up front — so feedback ingested while it runs is never deleted.
* For a dataset that is no longer receiving feedback the count reaching zero means
* the purge is complete; for one still ingesting, a nonzero count is those newer
* records. Note this means the count alone cannot distinguish "finished" from
* "failed" on an active dataset.
*
* Idempotent and safe to repeat. Requesting a purge while one is already running
* for the same tenant joins the running purge rather than queueing a second one,
* and still returns 202. A purge requested after an earlier one finished starts a
* new run.
*
* Records are deleted in committed batches, so a purge interrupted by a restart or
* a timeout keeps the progress it made and resumes on retry; the taxonomy is
* removed in a final step once the records are gone. While each step runs, the
* tenant's write lock is held exclusively and Hub-owned writes for that tenant are
* rejected with HTTP 409 (code `tenant_write_conflict`); the lock is released
* between steps, and writes for other tenants are never affected. This does not
* apply to the request below — scheduling a purge takes no lock, so this endpoint
* does not return 409. A batch that cannot acquire the lock is retried by the job
* queue without any caller action, up to a bounded number of attempts.
*
* No webhook events are published for a purge, and no webhooks are deleted.
* Enrichment jobs already queued for purged records no-op when they run, since the
* record they reference is gone.
*
* @example
* ```ts
* const response = await client.tenants.purgeFeedbackRecords(
* 'org-123',
* );
* ```
*/
purgeFeedbackRecords(
tenantID: string,
options?: RequestOptions,
): APIPromise<TenantPurgeFeedbackRecordsResponse> {
return this._client.delete(path`/v1/tenants/${tenantID}/feedback-records`, options);
}
}

export interface TenantDeleteDataResponse {
Expand Down Expand Up @@ -100,10 +170,32 @@ export interface TenantDeleteDataResponse {
tenant_id: string;
}

export interface TenantPurgeFeedbackRecordsResponse {
/**
* Always `accepted`. The purge runs in the background, so this reports that the
* work was scheduled, not that it finished — poll `GET /v1/feedback-records/count`
* for the tenant to observe completion.
*/
status: 'accepted';

/**
* Tenant ID whose feedback records are being purged
*/
tenant_id: string;

/**
* Human-readable confirmation
*/
message?: string;
}

Tenants.Settings = Settings;

export declare namespace Tenants {
export { type TenantDeleteDataResponse as TenantDeleteDataResponse };
export {
type TenantDeleteDataResponse as TenantDeleteDataResponse,
type TenantPurgeFeedbackRecordsResponse as TenantPurgeFeedbackRecordsResponse,
};

export {
Settings as Settings,
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.12.0'; // x-release-please-version
export const VERSION = '0.13.0'; // x-release-please-version
12 changes: 12 additions & 0 deletions tests/api-resources/tenants/tenants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,16 @@ describe('resource tenants', () => {
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});

// Mock server tests are disabled
test.skip('purgeFeedbackRecords', async () => {
const responsePromise = client.tenants.purgeFeedbackRecords('org-123');
const rawResponse = await responsePromise.asResponse();
expect(rawResponse).toBeInstanceOf(Response);
const response = await responsePromise;
expect(response).not.toBeInstanceOf(Response);
const dataAndResponse = await responsePromise.withResponse();
expect(dataAndResponse.data).toBe(response);
expect(dataAndResponse.response).toBe(rawResponse);
});
});
Loading