mem0ai/mem0 · info · NodeOperationError

Provide at least one of User ID, Agent ID, App ID, or Run ID

Error message

Provide at least one of User ID, Agent ID, App ID, or Run ID

What it means

DELETE /api-keys/{key_id} is idempotency-unfriendly on purpose: if the targeted key already has revoked_at set, further DELETEs return 400 'API key is already revoked' instead of silently succeeding. Revocation is a one-time state transition; a second attempt is treated as a client error.

Source

Thrown at integrations/n8n-nodes-mem0/nodes/Mem0/Mem0.node.ts:587

				throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: i });
			}
		}

		return [returnData];
	}
}

function buildEntityFilters(
	ids: Record<string, string>,
	ctx: IExecuteFunctions,
	itemIndex: number,
): IDataObject {
	const clauses: IDataObject[] = Object.entries(ids)
		.filter(([, value]) => value)
		.map(([key, value]) => ({ [key]: value }));

	if (clauses.length === 0) {
		throw new NodeOperationError(
			ctx.getNode(),
			'Provide at least one of User ID, Agent ID, App ID, or Run ID',
			{ itemIndex },
		);
	}

	return clauses.length === 1 ? clauses[0] : { OR: clauses };
}

// Polls GET /v1/event/{id}/ until the memory-addition event resolves.
async function pollEvent(
	request: (m: IHttpRequestMethods, u: string) => Promise<IDataObject>,
	eventId: string,
	ctx: IExecuteFunctions,
	itemIndex: number,
): Promise<IDataObject | IDataObject[]> {
	for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
		const event = await request('GET', `/v1/event/${encodeURIComponent(eventId)}/`);

View on GitHub (pinned to 001c235229)

Solutions

  1. Treat this 400 as success in reconciliation scripts: the desired end state (revoked) is already true.
  2. Refresh the key list after revoking and disable the UI action for keys with revoked_at set.
  3. Before deleting, check the key's status via GET /api-keys and skip if already revoked.

Example fix

# before
resp = requests.delete(f"{BASE}/api-keys/{key_id}", headers=h)
resp.raise_for_status()  # raises on 'already revoked' 400

# after
resp = requests.delete(f"{BASE}/api-keys/{key_id}", headers=h)
if resp.status_code == 400 and "already revoked" in resp.json().get("detail", ""):
    pass  # already in desired state
else:
    resp.raise_for_status()
Defensive patterns

Strategy: try-catch

Validate before calling

keys = requests.get(f"{BASE}/api-keys", headers=h).json()
if any(k["id"] == key_id and k.get("revoked_at") for k in keys):
    print("already revoked; skipping")  # skip before calling DELETE

Type guard

def is_revoked(key_record: dict) -> bool:
    return key_record.get("revoked_at") is not None

Try / catch

resp = requests.delete(f"{BASE}/api-keys/{key_id}", headers=h)
if resp.status_code == 400 and "already revoked" in resp.json().get("detail", ""):
    pass  # idempotent success: desired state reached
else:
    resp.raise_for_status()

Prevention

When it happens

Trigger: Clicking 'revoke' twice in a UI without a refresh in between; retrying a DELETE that already succeeded (e.g. response lost to a network error); scripts that reconcile state by re-revoking all keys each run.

Common situations: Eventual-consistency UI showing a stale 'active' key; automation that does not track that revocation already happened; retry logic that resends non-idempotent DELETEs after timeouts.

Related errors


AI-assisted analysis of mem0ai/mem0@001c235229 (2026-08-15). Data as JSON: /api/errors/d4dfd71987c18780. Report an issue: GitHub.