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.

4 min readSume
All posts

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.

409 codes on Format contents writes. Source: Editing a Format package, docs.sume.com/formats/contents, read 2026-10-03.
error.codeCauseWhat to do
format_content_sha_requiredThe path exists and you sent no shaGET the file, then retry with its sha
format_content_sha_mismatchYour sha is stale; someone committed firstRe-read the file and retry
format_package_sha_mismatchYour If-Match package sha is stale; details.package_sha has the current oneRe-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

All Developers posts

Written by Sume