From 6f2d8c3aa5f2bc323d18d01e5c9514b3e1d8a645 Mon Sep 17 00:00:00 2001 From: Dominic Couture Date: Fri, 2 Oct 2026 11:54:27 +0100 Subject: [PATCH 1/2] feat(backend): add deleteInvitation to the Invitations API Expose DELETE /v1/invitations/{invitation_id} as clerkClient.invitations.deleteInvitation(invitationId). Unlike revoking, deleting removes the invitation record and the stored invitation emails, which is needed to honor erasure requests from invitees who never signed up. Related to SEC-409 Co-Authored-By: Claude Fable 5.1 --- .changeset/backend-delete-invitation.md | 5 +++ .../src/api/__tests__/InvitationApi.test.ts | 35 +++++++++++++++++++ .../src/api/endpoints/InvitationApi.ts | 18 ++++++++++ 3 files changed, 58 insertions(+) create mode 100644 .changeset/backend-delete-invitation.md create mode 100644 packages/backend/src/api/__tests__/InvitationApi.test.ts diff --git a/.changeset/backend-delete-invitation.md b/.changeset/backend-delete-invitation.md new file mode 100644 index 00000000000..d2cf179c16b --- /dev/null +++ b/.changeset/backend-delete-invitation.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': minor +--- + +Add `clerkClient.invitations.deleteInvitation(invitationId)`, which permanently deletes an instance invitation and the stored copies of its invitation email. Unlike `revokeInvitation`, this removes the invitation record itself, so it can be used to honor a data erasure request from someone who was invited but never signed up. diff --git a/packages/backend/src/api/__tests__/InvitationApi.test.ts b/packages/backend/src/api/__tests__/InvitationApi.test.ts new file mode 100644 index 00000000000..f91582082bd --- /dev/null +++ b/packages/backend/src/api/__tests__/InvitationApi.test.ts @@ -0,0 +1,35 @@ +import { http, HttpResponse } from 'msw'; +import { describe, expect, it } from 'vitest'; + +import { server, validateHeaders } from '../../mock-server'; +import { createBackendApiClient } from '../factory'; + +describe('InvitationAPI', () => { + const apiClient = createBackendApiClient({ + apiUrl: 'https://api.clerk.test', + secretKey: 'deadbeef', + }); + + describe('deleteInvitation', () => { + const invitationId = 'inv_123'; + + it('deletes an invitation by ID', async () => { + server.use( + http.delete( + `https://api.clerk.test/v1/invitations/${invitationId}`, + validateHeaders(() => HttpResponse.json({ object: 'invitation', id: invitationId, deleted: true })), + ), + ); + + const response = await apiClient.invitations.deleteInvitation(invitationId); + + expect(response.object).toBe('invitation'); + expect(response.id).toBe(invitationId); + expect(response.deleted).toBe(true); + }); + + it('throws an error when the invitation ID is missing', async () => { + await expect(apiClient.invitations.deleteInvitation('')).rejects.toThrow('A valid resource ID is required.'); + }); + }); +}); diff --git a/packages/backend/src/api/endpoints/InvitationApi.ts b/packages/backend/src/api/endpoints/InvitationApi.ts index 45b4e2c9976..6fa1d0cc873 100644 --- a/packages/backend/src/api/endpoints/InvitationApi.ts +++ b/packages/backend/src/api/endpoints/InvitationApi.ts @@ -1,6 +1,7 @@ import type { ClerkPaginationRequest } from '@clerk/shared/types'; import { joinPaths } from '../../util/path'; +import type { DeletedObject } from '../resources/DeletedObject'; import type { PaginatedResourceResponse } from '../resources/Deserializer'; import type { InvitationStatus } from '../resources/Enums'; import type { Invitation } from '../resources/Invitation'; @@ -132,4 +133,21 @@ export class InvitationAPI extends AbstractAPI { path: joinPaths(basePath, invitationId, 'revoke'), }); } + + /** + * Permanently deletes the given invitation and the copies of the invitation email Clerk stored for its recipient. + * + * Unlike revoking, deleting removes the invitation record itself, which helps honor a data erasure request from someone who was invited but never signed up. Other records that contain the same email address, such as users or organization invitations, are not affected. + * + * Invitations of any status can be deleted. + * @param invitationId - The ID of the invitation to delete. + * @returns The [`DeletedObject`](https://clerk.com/docs/reference/backend/types/deleted-object) object. + */ + public async deleteInvitation(invitationId: string) { + this.requireId(invitationId); + return this.request({ + method: 'DELETE', + path: joinPaths(basePath, invitationId), + }); + } } From 40ca52a580c9dcee144eb375fd5f5d43c4a769f5 Mon Sep 17 00:00:00 2001 From: Dominic Couture Date: Fri, 2 Oct 2026 13:26:07 +0100 Subject: [PATCH 2/2] fix(backend): declare the deleteInvitation return type Co-Authored-By: Claude Fable 5.1 --- packages/backend/src/api/endpoints/InvitationApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/api/endpoints/InvitationApi.ts b/packages/backend/src/api/endpoints/InvitationApi.ts index 6fa1d0cc873..13ba0310a12 100644 --- a/packages/backend/src/api/endpoints/InvitationApi.ts +++ b/packages/backend/src/api/endpoints/InvitationApi.ts @@ -143,7 +143,7 @@ export class InvitationAPI extends AbstractAPI { * @param invitationId - The ID of the invitation to delete. * @returns The [`DeletedObject`](https://clerk.com/docs/reference/backend/types/deleted-object) object. */ - public async deleteInvitation(invitationId: string) { + public async deleteInvitation(invitationId: string): Promise { this.requireId(invitationId); return this.request({ method: 'DELETE',