One idempotency key has one meaning
An owner submits a task named "Review migration" with key task-create-9fc2. Later, a client bug submits "Delete old backups" with the same key. Taskboard must not treat those bodies as the same operation.
Returning the first result would tell the client that its second task exists when it does not. Running the second operation would break the promise that retries under one key do not create another task.
Compare validated input
Taskboard records a fingerprint alongside the key. On replay, the server compares the current fingerprint with the stored fingerprint. A mismatch produces a conflict response, commonly HTTP 409, and makes no new change.
if (stored.requestHash !== currentRequestHash) {
throw new ConflictError("Idempotency key reused with different input");
}
return stored.response;
The fingerprint needs a deliberate representation. Two JSON objects can have different property ordering while describing the same validated input. Hashing raw request bytes would treat those as different. A canonical representation of validated fields avoids that accidental distinction.
Validation also decides which defaults count as input. If an omitted status becomes todo, the normalized object can record todo explicitly. That keeps the comparison tied to the operation the server actually performs.
Keys are identifiers, not permission
The server still checks the session, workspace membership, and role. Knowing another actor's key must not reveal their result. A maximum key length prevents unbounded storage through a request header.
The client can recover from an accidental conflict by using a fresh key for a genuinely new operation. It should not automatically change the key after every timeout. That would bypass the duplicate protection precisely when the original outcome is uncertain.
Checkout has the same distinction. Repeating one checkout attempt uses its original key. Taskboard currently offers one paid plan, team; adding another plan would introduce a new choice and would need the same input-matching rule.
The real fingerprint sorts object keys recursively before hashing the JSON representation:
export function fingerprint(value: unknown): string {
return hashToken(JSON.stringify(canonical(value)));
}
Inspect key scoping and fingerprint checks in Read api/src/tasks.ts and Read api/src/billing.ts. The canonical representation is in Read api/src/security.ts.
A key names one operation with one validated input. Reusing the key with different input is a conflict, not a convenient way to replace the first operation.
Should a timeout make the client generate a new key?
No. A timeout leaves the first outcome uncertain. Reusing the original key lets the server find that outcome. A new key can create duplicate work.