---
title: Editing a Format package
description: Read and commit the files behind your own Format over /v1, the same way you would use the GitHub Contents API.
---

Every custom [Format](/formats) is a small package of files — a `SKILL.md` entry file plus optional
notes under `references/` — and that package has real history behind it. The Contents API lets you
read those files and commit changes to them with an API key, so a coding agent can edit a Format the
way it already edits a repository.

The shape is GitHub's Contents API, on purpose. If you know
`gh api repos/{owner}/{repo}/contents/{path}`, you know this:

```
POST   /v1/formats                                   # create the Format
GET    /v1/formats/{handle}/{slug}/contents
GET    /v1/formats/{handle}/{slug}/contents/{path}
PUT    /v1/formats/{handle}/{slug}/contents          # several files, one commit
PUT    /v1/formats/{handle}/{slug}/contents/{path}
DELETE /v1/formats/{handle}/{slug}/contents/{path}
```

`{handle}/{slug}` is the same address you invoke the Format at. Reads need `formats:read`, writes
need `formats:write`, and the commit's author is always the key's owner — an `author` or `committer`
in the body is ignored.

This is authoring, not execution. Editing a package never touches a run that is already in flight:
each run reads the package it started with.

## Create a Format

`POST /v1/formats` opens a Format and its package repository, the way `POST /repos` opens one on
GitHub. It needs `formats:write`, and the Format is created in **your key's workspace** — there is no
`{handle}` in the address, so there is no way to create one in somebody else's.

```bash
export SUME_API_KEY=sume_…   # a workspace key; never commit it

curl -sS -X POST "https://api.dev.sume.com/v1/formats" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "content-type: application/json" \
  -d '{"slug":"plate-shots","title":"Plate shots","description":"Studio plate photography."}'
```

```json
{
  "data": {
    "id": "skl_…",
    "object": "format",
    "handle": "mobidoo",
    "slug": "plate-shots",
    "version": 1,
    "package_sha": "3f2b1c…",
    "contents_url": "https://api.dev.sume.com/v1/formats/mobidoo/plate-shots/contents",
    "vanity_invoke_url": "https://api.dev.sume.com/v1/formats/mobidoo/plate-shots/runs"
  }
}
```

`auto_init` (default `true`) commits a minimal valid `SKILL.md`, so the Format is readable and
writable through the endpoints below immediately — replacing that file is the intended next call.
`false` is a `400`: every Format package must contain `SKILL.md`, so there is no empty Format to
create. The body does not take package files; that is what the Contents API is for, and a Format's
files should follow one set of rules no matter who wrote them.

Keep `package_sha` and `contents_url` from the reply — the first is the `If-Match` precondition for
your next write, the second is where to send it. A slug your workspace already uses answers `409`,
never an overwrite.

The repository is opened **before** the catalog row. If package history cannot be reached the call
fails with `503 format_git_unavailable` and no Format is created, rather than one whose commits
nobody can resolve.

## Read the package

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": [
    { "type": "dir", "name": "references", "path": "references", "sha": "9c1a…", "size": 0 },
    { "type": "file", "name": "SKILL.md", "path": "SKILL.md", "sha": "0ee8…", "size": 4213 }
  ]
}
```

One path returns the file itself, base64-encoded:

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/SKILL.md" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": {
    "type": "file",
    "name": "SKILL.md",
    "path": "SKILL.md",
    "sha": "0ee8784832cd08154436f264ddeb38703dc89cd7",
    "size": 4213,
    "encoding": "base64",
    "content": "LS0tCm5hbWU6IGxpdmUtY29tbWVyY2UK…"
  }
}
```

**Keep the `sha`.** It is the git blob sha of the file as stored, and it is the precondition every
write takes. A path naming a directory (`…/contents/references`) returns that directory's entries
instead.

## Read the whole package at once

Listing the root and then fetching each path is one request per file. When what you want is the
whole package — hydrating it into an agent's context, say — ask for it in one call:

```bash
curl -sS "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents?recursive=1" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

```json
{
  "data": [
    {
      "type": "file",
      "name": "SKILL.md",
      "path": "SKILL.md",
      "sha": "0ee8784832cd08154436f264ddeb38703dc89cd7",
      "size": 4213,
      "encoding": "base64",
      "content": "LS0tCm5hbWU6IGxpdmUtY29tbWVyY2UK…"
    },
    {
      "type": "file",
      "name": "plan.md",
      "path": "references/plan.md",
      "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b",
      "size": 128,
      "encoding": "base64",
      "content": "IyBQbGFuCg=="
    }
  ]
}
```

Every row is a file with its body, sorted by `path`, and there are no `dir` rows — the directories
are the paths. `recursive=true` works too. Anything else, including leaving it off, is the plain
root listing above. The `sha` on each row is the same precondition a write takes, so one recursive
read is enough to start editing.

This only applies to the listing. `…/contents/{path}` answers the same way with or without it.

## Commit a change

`PUT` writes one whole file and produces one commit. It is a replace, not a patch — send the entire
new body, base64-encoded.

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/references/plan.md" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "message": "tighten gate 3",
        "content": "IyBQbGFuCg==",
        "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b"
      }'
```

```json
{
  "data": {
    "content": { "type": "file", "path": "references/plan.md", "sha": "5a2f…", "size": 7 },
    "commit": {
      "sha": "b41c9a…",
      "message": "tighten gate 3",
      "tree": { "sha": "77de8a…" },
      "parents": [{ "sha": "0a91f2…" }]
    },
    "version": 12
  }
}
```

`commit.tree.sha` is the Format's new package identity — the same value the Format's own record
reports — and `version` is the display counter the dashboard shows.

Send `sha` when the path already exists; omit it to create a new file. Two agents editing the *same
file* cannot silently overwrite each other, because the second one's `sha` no longer matches. For
agents editing *different* files of one Format, see [`If-Match`](#guard-the-whole-package-with-if-match).

`DELETE` takes `message` and `sha`, and answers with `content: null`:

```bash
curl -sS -X DELETE \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents/references/plan.md" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "drop the old plan", "sha": "5a2f1b3c4d5e6f708192a3b4c5d6e7f809a1b2c3"}'
```

## Commit several files at once

Editing a folder one path at a time costs one commit and one round trip per file. `PUT` at the
package root — no `{path}` — takes a `files` list and writes all of them as **one** commit, one
`version` bump, one new `package_sha`:

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "message": "edit plan + facts",
        "files": [
          { "path": "references/plan.md",  "content": "IyBQbGFuCg==", "sha": "3f7b1c0d9e2a4b6c8d0e1f2a3b4c5d6e7f809a1b" },
          { "path": "references/facts.md", "content": "IyBGYWN0cwo=", "sha": "9b1d0e2f3a4b5c6d7e8f90a1b2c3d4e5f6a7b8c9" },
          { "path": "references/new-note.md", "content": "IyBOZXcK" }
        ]
      }'
```

Each entry follows the single-file rules: `content` is the whole file base64-encoded, `sha` is
required when the path already exists and omitted to create it. `content` in the reply is the list
of files this commit wrote.

**`files` is a change set, not the package.** Paths the Format holds but this body does not name are
kept exactly as they are — editing two of twelve files never risks the other ten. That also means
deletion is not expressible here: remove a file with `DELETE …/contents/{path}`, so that dropping a
file is always something you asked for and never something you forgot to say.

If any entry's `sha` is stale, or the resulting package breaks a rule, **nothing** is committed —
the batch lands whole or not at all.

## Guard the whole package with `If-Match`

Per-file `sha` cannot express "the package has not moved since I read it". Two agents editing
*different* files each hold a `sha` that is still current, so both writes land — and the second one
planned its edit against a tree that no longer existed.

Send the package's own sha to close that gap. It is `commit.tree.sha` from your last write, and the
same value the Format record reports as `package_sha`:

```bash
curl -sS -X PUT \
  "https://api.dev.sume.com/v1/formats/mobidoo/live-commerce/contents" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "If-Match: 77de8a9b1c2d3e4f5061728394a5b6c7d8e9f001" \
  -H "Content-Type: application/json" \
  -d '{"message": "edit plan", "files": [{"path": "references/plan.md", "content": "IyBQbGFuCg==", "sha": "3f7b…"}]}'
```

If the package has moved on, the write is refused with `409 format_package_sha_mismatch`, and
`error.details.package_sha` carries the current one so you can re-read and retry in a single round
trip. The header works on `PUT …/{path}` and `DELETE …/{path}` too, and it is *additional* — per-file
`sha` checks still apply.

`If-Match` here is not an opaque ETag; it is the package sha, as a bare 40-character hex string.
Formats published before `package_sha` existed carry none, and those ignore the header rather than
answering with a `409` you could never satisfy.

## What a package may contain

The same rules the dashboard editor enforces:

- `SKILL.md` at the package root is required, and its frontmatter `name` must equal the Format's
  slug. It cannot be deleted.
- Files live at the root or one directory deep under `references/` or `agents/`. No `..`, no
  absolute paths.
- File names must match `^[A-Za-z0-9][A-Za-z0-9._-]*$` — no leading `_` or `.`.
- `.md`, `.json`, `.yaml`, `.yml`, `.txt` only.
- At most 100 MiB per file and 100 MiB per package. A Contents batch may name at most 1000 paths.

A package that breaks any of these is rejected before anything is committed. A rejected path answers
with `skill_path_invalid` and spells the whole allowlist back to you, so the first rejection is
enough to fix the name.

## Errors worth handling

| Status | `error.code` | What happened |
|---|---|---|
| `403` | `insufficient_scope` | The key lacks `formats:read` / `formats:write`, or it is a service-account key. Those cannot create or edit packages. `next_action` is `authenticate`. Scopes cannot be patched — mint a new key. Missing scopes are never `format_not_found`. |
| `409` | `skill_slug_taken` | Your workspace already has a Format with that slug. Creating never overwrites; pick another. |
| `409` | `skill_slug_reserved` | A Format by Sume holds that slug globally. Pick another. |
| `404` | `format_not_found` | Unknown Format, or one outside the calling key's workspace. A team Format needs a key created in that workspace. |
| `404` | `format_content_not_found` | The Format exists but holds nothing at that path. |
| `409` | `format_content_sha_required` | The path already exists and you sent no `sha`. Read it, then retry. |
| `409` | `format_content_sha_mismatch` | Your `sha` is stale — someone else committed first. Re-read and retry. |
| `409` | `format_package_sha_mismatch` | Your `If-Match` package sha is stale. `error.details.package_sha` holds the current one. |
| `400` | `skill_path_invalid`, `skill_frontmatter_invalid`, `skill_limit_exceeded`, … | The resulting package broke a rule above. Nothing was committed. |
| `503` | `format_git_unavailable` | Package history could not take the commit, so **nothing was saved**. Retry. |

That last one is deliberate: a write either commits or fails. There is no path where the Format
changes but the commit does not exist.

## What this is not

There is no public git endpoint, no clone URL, and no way to reach the history writer directly.
Commits happen because this API makes them. Reverting, blame, and history browsing are not part of
this surface yet.
