diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 8032c17..ed21d28 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.12.0"
+ ".": "0.13.0"
}
diff --git a/.stats.yml b/.stats.yml
index 71cbc3b..2abe9c2 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b628da7..0c6072d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/api.md b/api.md
index ee5731a..ca8079d 100644
--- a/api.md
+++ b/api.md
@@ -61,10 +61,12 @@ Methods:
Types:
- TenantDeleteDataResponse
+- TenantPurgeFeedbackRecordsResponse
Methods:
- client.tenants.deleteData(tenantID) -> TenantDeleteDataResponse
+- client.tenants.purgeFeedbackRecords(tenantID) -> TenantPurgeFeedbackRecordsResponse
## Settings
diff --git a/package.json b/package.json
index fbccf89..f6b6352 100644
--- a/package.json
+++ b/package.json
@@ -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 ",
"types": "dist/index.d.ts",
diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json
index ca84895..7cd9692 100644
--- a/packages/mcp-server/manifest.json
+++ b/packages/mcp-server/manifest.json
@@ -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",
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index d55f4e1..dc14083 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -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 ",
"types": "dist/index.d.ts",
diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts
index dcfd713..dab8829 100644
--- a/packages/mcp-server/src/code-tool-worker.ts
+++ b/packages/mcp-server/src/code-tool-worker.ts
@@ -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',
diff --git a/packages/mcp-server/src/local-docs-search.ts b/packages/mcp-server/src/local-docs-search.ts
index 39df01d..1d6753e 100644
--- a/packages/mcp-server/src/local-docs-search.ts
+++ b/packages/mcp-server/src/local-docs-search.ts
@@ -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',
diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts
index aa24431..3e955c3 100644
--- a/packages/mcp-server/src/methods.ts
+++ b/packages/mcp-server/src/methods.ts
@@ -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',
diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts
index 89b2343..786b706 100644
--- a/packages/mcp-server/src/server.ts
+++ b/packages/mcp-server/src/server.ts
@@ -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 }),
diff --git a/src/client.ts b/src/client.ts
index 172e98a..dca4ff2 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -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';
@@ -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,
diff --git a/src/resources/index.ts b/src/resources/index.ts
index fc2add8..7ace492 100644
--- a/src/resources/index.ts
+++ b/src/resources/index.ts
@@ -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,
diff --git a/src/resources/tenants/index.ts b/src/resources/tenants/index.ts
index ab54cc0..65d3160 100644
--- a/src/resources/tenants/index.ts
+++ b/src/resources/tenants/index.ts
@@ -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';
diff --git a/src/resources/tenants/tenants.ts b/src/resources/tenants/tenants.ts
index d6b3698..bd72b0a 100644
--- a/src/resources/tenants/tenants.ts
+++ b/src/resources/tenants/tenants.ts
@@ -41,6 +41,76 @@ export class Tenants extends APIResource {
deleteData(tenantID: string, options?: RequestOptions): APIPromise {
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 {
+ return this._client.delete(path`/v1/tenants/${tenantID}/feedback-records`, options);
+ }
}
export interface TenantDeleteDataResponse {
@@ -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,
diff --git a/src/version.ts b/src/version.ts
index ce6b899..9d013cc 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const VERSION = '0.12.0'; // x-release-please-version
+export const VERSION = '0.13.0'; // x-release-please-version
diff --git a/tests/api-resources/tenants/tenants.test.ts b/tests/api-resources/tenants/tenants.test.ts
index 1c48fc6..5447b1e 100644
--- a/tests/api-resources/tenants/tenants.test.ts
+++ b/tests/api-resources/tenants/tenants.test.ts
@@ -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);
+ });
});