Format contents PUT 409 format_content_sha_required: send the sha
PUT on a Format file that already exists needs its blob sha. Read the sha with GET, retry, and tell the three 409 codes apart from the package If-Match guard.

409 format_content_sha_required means you sent a PUT for a path that already exists and left out sha. Read the file with GET, keep the sha in the reply, and send it back with the new content. Omitting sha is only valid for creating a new file. This is deliberate: it is how two agents editing the same file cannot silently overwrite each other.
Which 409 did I get?
The Contents API for a Format package answers three different 409s, and each is a different fix. All of them mean nothing was committed.
| error.code | Cause | What to do |
|---|---|---|
| format_content_sha_required | The path exists and you sent no sha | GET the file, then retry with its sha |
| format_content_sha_mismatch | Your sha is stale; someone committed first | Re-read the file and retry |
| format_package_sha_mismatch | Your If-Match package sha is stale; details.package_sha has the current one | Re-read, then retry in one round trip |
How do I get the right sha?
Every file row from the Contents API carries a sha, the git blob sha of the file as stored. A GET on one path returns the file base64-encoded with its sha, and GET …/contents?recursive=1 returns every file with its body and sha in one call, so a single read is enough to start editing. A PUT replaces the whole file: send the entire new body, base64-encoded, not a patch.
After a successful write the reply carries commit.tree.sha, the Format's new package identity, and the file's new sha under content. Use that new sha for your next edit to the same file rather than reading again.
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
const headers = {
"x-api-key": process.env.SUME_API_KEY!,
"content-type": "application/json",
};
const url = `${base}/formats/acme/product-promo/contents/references/plan.md`;
const current = await fetch(url, { headers });
const sha = current.ok ? (await current.json()).data.sha : undefined;
const put = await fetch(url, {
method: "PUT",
headers,
body: JSON.stringify({
message: "tighten gate 3",
content: Buffer.from("# Plan\n").toString("base64"),
...(sha ? { sha } : {}), // omit only when the file is new
}),
});
console.log(put.status, (await put.json()).error?.code);Why not just retry the same PUT?
Retrying the identical request returns the identical 409. The three codes are answers about the state of the package, not transient faults. A stale sha is retried only after a fresh read, and the loop should stop after a couple of attempts, because a loop that keeps losing the race means someone else is actively editing the same file.
For several files at once, PUT at the package root takes a files list and lands them as one commit, or not at all if any entry's sha is stale. Paths the body does not name are kept. For agents editing different files of one Format, add If-Match with the package sha so the second write learns the package moved.
What does a write need besides the sha?
The key needs the right scope and must belong to the workspace that owns the Format. A 404 format_not_found covers an unknown Format and one outside the key's workspace, and a 404 format_content_not_found means the Format exists but holds nothing at that path. Other writes can be refused with 400 when the resulting package breaks a rule, such as skill_path_invalid or skill_limit_exceeded; nothing is committed in that case either.
There is also a 503 format_git_unavailable: package history could not take the commit, so nothing was saved. That one is safe to retry as is. Each of these is a different shape of failure from the sha 409s, which is why branching on error.code rather than only on the status keeps your editing loop honest.
Sources
Related posts
More in Developers
- Format 404 format_not_found: five causes, including a pending grant
A Sume 404 format_not_found can mean a typo, an archived Format, the wrong workspace, a team handle you are not in, or a shared Format whose grant is pending.
- 409 previous_run_not_terminal: continue a Format run after it ends
Continuing a Format run with previous_run_id while the first is still running returns 409. Wait for terminal, then continue; a failed create frees its key.
- studio_agent_upstream_unavailable 503: retry, the run keeps going
A Sume 503 studio_agent_upstream_unavailable is a Sume-side outage: retry create with the same Idempotency-Key, and keep polling a run you already hold.
- GET format-runs result 409 run_not_completed: poll status first
Reading result_url while a Format run is in flight returns 409 run_not_completed with details.status. Poll status_url, then read the receipt once terminal.
Written by Sume