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
- Fetch the key first and skip if revoked_at is already set (make revoke idempotent in your layer)
- Refresh the key list in the UI after revoke and disable the button
- 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
- Make revoke idempotent in your layer by filtering already-revoked keys
- Refresh key state in the UI after revoke and disable the action
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
- You must provide a revoked_by field when revoking a key.
- Cart ${cart.id} already has a payment collection
- Cannot revert a transaction that is already compensating.
- Cannot delete api keys that are not revoked - ${unrevokedApi
- You must provide an api key id field when revoking a key.
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/5497be6fc865a1a0.
Report an issue: GitHub.