{"record":{"id":"830694a0727ea483","repo":"medusajs/medusa","slug":"cannot-delete-api-keys-that-are-not-revoked-un","errorCode":null,"errorMessage":"Cannot delete api keys that are not revoked - ${unrevokedApiKeys.join(\n          \", \"\n        )}","messagePattern":"Cannot delete api keys that are not revoked - (.+?)","errorType":"validation","errorClass":"MedusaError","httpStatus":405,"severity":"error","filePath":"packages/modules/api-key/src/services/api-key-module-service.ts","lineNumber":101,"sourceCode":"  ) {\n    const apiKeyIds = Array.isArray(ids) ? ids : [ids]\n\n    const unrevokedApiKeys = (\n      await this.apiKeyService_.list(\n        {\n          id: ids,\n          $or: [\n            { revoked_at: { $eq: null } },\n            { revoked_at: { $gt: new Date() } },\n          ],\n        },\n        { select: [\"id\"] },\n        sharedContext\n      )\n    ).map((apiKey) => apiKey.id)\n\n    if (isPresent(unrevokedApiKeys)) {\n      throw new MedusaError(\n        MedusaError.Types.NOT_ALLOWED,\n        `Cannot delete api keys that are not revoked - ${unrevokedApiKeys.join(\n          \", \"\n        )}`\n      )\n    }\n\n    return await super.deleteApiKeys(apiKeyIds, sharedContext)\n  }\n\n  //@ts-expect-error\n  createApiKeys(\n    data: ApiKeyTypes.CreateApiKeyDTO[],\n    sharedContext?: Context\n  ): Promise<ApiKeyTypes.ApiKeyDTO[]>\n  //@ts-expect-error\n  createApiKeys(\n    data: ApiKeyTypes.CreateApiKeyDTO,","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/modules/api-key/src/services/api-key-module-service.ts#L83-L119","documentation":"Thrown by the api-key module when deleting API keys that are still active — Medusa requires keys to be revoked before deletion so that in-use keys are never silently removed. The error lists the offending key ids.","triggerScenarios":"Calling DELETE /admin/api-keys/:id (or apiKeyModuleService.deleteApiKeys) on a key whose revoked_at is null; batch deletes fail if any key in the list is unrevoked.","commonSituations":"Cleanup scripts deleting old keys without revoking them first; attempting to delete a publishable key still used by a live storefront.","solutions":["Revoke the key first: POST /admin/api-keys/:id/revoke (revoked_by required), then delete","For batch deletes, filter out or revoke all unrevoked keys before calling delete","Keep the two-step revoke→delete flow in key-rotation scripts"],"exampleFix":"// before\nawait apiKeyModuleService.deleteApiKeys([\"apk_active\"])\n\n// after\nawait apiKeyModuleService.revokeApiKeys({\n  data: [{ id: \"apk_active\", revoked_by: \"user_1\" }],\n})\nawait apiKeyModuleService.deleteApiKeys([\"apk_active\"])","handlingStrategy":"validation","validationCode":"// check revoked_at before deleting\nconst keys = await service.listApiKeys({ id: ids }, { select: ['id', 'revoked_at'] })\nconst unrevoked = keys.filter(k => !k.revoked_at)\nfor (const k of unrevoked) {\n  await service.revokeApiKeys({ data: [{ id: k.id, revoked_by: actorId }] })\n}\nawait service.deleteApiKeys(ids)","typeGuard":"const isRevoked = (key: { revoked_at: string | Date | null }): boolean =>\n  key.revoked_at !== null","tryCatchPattern":"try {\n  await service.deleteApiKeys(ids)\n} catch (e) {\n  if (e.type === 'not_allowed' && /not revoked/.test(e.message)) {\n    // parse listed ids, revoke them, then retry the delete\n  }\n  throw e\n}","preventionTips":["Always run revoke before delete in key-lifecycle scripts and treat it as one atomic flow","Guard deletion UI/actions with a 'revoked' state check"],"tags":["api-key","admin-api","lifecycle","not-allowed"],"backgroundTag":"delete-before-revoke","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}