version_conflict
HTTP 412 retryable: false REST and MCP
The version or revision you sent does not match the resource's current one, so your update was computed against state that is no longer there.
Why it happens
Updates to policies and webhook endpoints are optimistically concurrent: you send the version you read, and the write only proceeds if it still matches under a row lock. This is not a general-purpose staleness warning, it is targeted at the settings whose silent overwrite would be dangerous. A policy edit that clobbers a block rule someone added a minute ago would quietly widen who an agent can email. The approval path uses the same idea with a content hash instead of a counter: a draft whose payload hash no longer matches the one the approval was granted against cannot be released under that approval.
Every condition below was read in the implementation at the file and line given. They are the branches that raise this code, not a summary of what the name suggests.
- expected_version on an inbox policy update does not equal the inbox-scoped policy's current version, where an absent policy counts as version 0.
packages/domain/src/customer-controls.ts:185 - expected_revision on a webhook control action does not equal the endpoint's current revision.
packages/domain/src/customer-controls.ts:276 - The stored draft version's payload hash no longer matches the hash recorded on the approval.
packages/domain/src/services.ts:2051
How to fix it
- Re-read the resource, re-apply your change to the version you just read, and send the new version number. Do not increment the number you were holding.
- Show the current state to whoever is editing before saving again. The message says to refresh and review, and for a sending policy that review is the point.
- For an approval, request a fresh one after editing the draft. The approval is bound to the exact content that was shown to the approver.
- Treat a missing policy as version 0. An inbox with no inbox-scoped policy of its own is at version 0, not version 1.
Why it works this way
Binding an approval to a payload hash rather than to a draft id is the substantive choice here. It means approving a draft approves that text, and editing after approval invalidates it, which is what makes the human decision meaningful rather than a rubber stamp on a mutable object.
Messages you will see
The message field is written for a human reading a log and is not a contract
value. Branch on code instead. These are the exact strings this code is sent
with today, listed so that a search for one of them lands here.
Where it appears
These are the documented surfaces that raise it. The REST base is https://api.emailforagents.ai/v1, and the remote MCP endpoint is https://mcp.emailforagents.ai/mcp.
PUT /projects/{project_id}/inboxes/{inbox_id}/policyPOST /projects/{project_id}/webhooks/{webhook_id}/control
The response
Read request_id off the body or the x-request-id header before
doing anything else. It is the only handle on one specific attempt, and for a 503 it is
the only handle at all, because the underlying detail stays server-side.
HTTP/1.1 412
content-type: application/json
x-request-id: req_...
{
"error": {
"code": "version_conflict",
"message": "These settings changed. Refresh and review the current policy before saving.",
"request_id": "req_...",
"retryable": false
}
}
Verified against packages/domain/src/customer-controls.ts:185. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one
place a code is mapped to a status.