medusajs/medusa · error · MedusaError

There are ${revokedApiKeys.length} secret keys that are alre

Error message

There are ${revokedApiKeys.length} secret keys that are already revoked.

What it means

The API Key module refuses to revoke a secret key that is already in revoked state, because revocation is a terminal state transition. The message reports how many keys in the batch were already revoked.

Source

Thrown at packages/modules/api-key/src/services/api-key-module-service.ts:563

    if (data.some((k) => !k.revoked_by)) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `You must provide a revoked_by field when revoking a key.`
      )
    }

    const revokedApiKeys = await this.apiKeyService_.list(
      {
        id: data.map((k) => k.id),
        type: ApiKeyType.SECRET,
        revoked_at: { $lt: new Date() },
      },
      {},
      sharedContext
    )

    if (revokedApiKeys.length) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        `There are ${revokedApiKeys.length} secret keys that are already revoked.`
      )
    }
  }

  // These are public keys, so there is no point hashing them.
  protected static generatePublishableKey(): TokenDTO {
    const token = "pk_" + crypto.randomBytes(32).toString("hex")

    return {
      rawToken: token,
      hashedToken: token,
      salt: "",
      redacted: redactKey(token),
    }
  }

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Fetch the key first and skip if revoked_at is already set (make revoke idempotent in your layer)
  2. Refresh the key list in the UI after revoke and disable the button
  3. Filter already-revoked ids out of the batch before calling revoke

Example fix

// before
await apiKeyModuleService.revoke(ids.map((id) => ({ id, revoked_by: userId })))
// after
const keys = await apiKeyModuleService.listApiKeys({ id: ids })
const active = keys.filter((k) => !k.revoked_at)
await apiKeyModuleService.revoke(active.map((k) => ({ id: k.id, revoked_by: userId })))
Defensive patterns

Strategy: validation

Validate before calling

const keys = await apiKeyModuleService.listApiKeys({ id: ids })
const toRevoke = keys.filter((k) => !k.revoked_at).map((k) => ({ id: k.id, revoked_by: userId }))
if (toRevoke.length) await apiKeyModuleService.revoke(toRevoke)

Type guard

const isRevoked = (k: { revoked_at: string | null }) => !!k.revoked_at

Try / catch

try { await revoke(...) } catch (e) { if (e.message.includes('already revoked')) return /* idempotent success */; throw e }

Prevention

When it happens

Trigger: Calling revokeApiKeys with the id of a publishable/secret key whose revoked_at is already set — e.g. double-clicking a revoke button, retrying a timed-out revoke request, or re-running a migration/script.

Common situations: UI double submissions, retries after network timeouts where the first revoke actually succeeded, idempotency-unaware batch jobs, or stale client state showing the key as active.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/5497be6fc865a1a0. Report an issue: GitHub.