Source

HTTP API

The CLI and the MCP tools are clients of this API. Anything they do, you can do with plain HTTP.

Basics

  • Base URL: https://api.heyquilt.com. Bodies and answers are JSON.
  • Send Authorization: Bearer <token>. The token is a person's sign-in (the one quilt login keeps in ~/.quilt/account.json) or an agent's qa_ access key. Hosted agents send Authorization: QuiltPass <pass>.
  • <q> is a quilt's slug or id. <s> is a square's id or its short handle, such as sq:77aa01bc. <rev> is any revision.
  • A write that names expect_head is refused with head_moved if the square moved. Without it, the server uses the square's current head.
  • Commits, merges, rebases, cherry-picks, resolves, folds and splits can be retried safely: the same call on the same square, with the same expect_head and body, within 24 hours gets the first answer again.
$ curl -s "https://api.heyquilt.com/v1/quilts/my-app/log?limit=1" \
    -H "Authorization: Bearer $QUILT_TOKEN"

Routes

Quilts

MethodPathWhat it does
GET/v1/quiltsThe quilts you are a member of: id, slug, name, owner and your level.
POST/v1/quiltsMakes a quilt: { slug, name? }. Answers 201 with the quilt. main starts at an empty root commit.
GET/v1/quilts/<q>One quilt: its lines (name, tip, patch_count, frozen), its members, and you.
POST/v1/quilts/<q>/membersAdds a member or changes their level: { member_id, level: read | write | admin }. Leave level out to remove them. Needs admin.

Squares

MethodPathWhat it does
GET/v1/quilts/<q>/squaresSquares, with ?state=open | merged | abandoned | folded, ?mine=1 and ?task=<id>.
POST/v1/quilts/<q>/squaresOpens a square: { title, line?, base?, task_id? }. base is any revision; the default is the line's tip. 201.
GET/v1/quilts/<q>/squares/<s>One square, with merged_under_you and would_conflict.
POST/v1/quilts/<q>/squares/<s>/commitCommits: { expect_head, message, changes }. 201 with the commit and the square.
POST/v1/quilts/<q>/squares/<s>/mergeMerges another square or revision into this square as a two-parent commit: { from, expect_head?, message? }.
POST/v1/quilts/<q>/squares/<s>/rebaseMoves the square onto its line's tip, or { onto } any revision.
POST/v1/quilts/<q>/squares/<s>/cherry-pickApplies one commit's change to the square: { from, expect_head? }.
POST/v1/quilts/<q>/squares/<s>/resolveSettles conflicts: { entries: [{ path, choose }], expect_head? }.
POST/v1/quilts/<q>/squares/<s>/handoffGives the square to another member: { to, note?, expect_head? }.
POST/v1/quilts/<q>/squares/<s>/abandonAbandons the square: { expect_head? }.
POST/v1/quilts/<q>/squares/<s>/reopenReopens an abandoned square; the caller becomes its holder.
POST/v1/quilts/<q>/squares/<s>/foldFolds this square into another one: { into, message?, expect_head? }.
POST/v1/quilts/<q>/squares/<s>/splitSplits it by paths: { by: "paths", groups: [[paths], …], titles?, expect_head? }. 201.

Lines and patches

MethodPathWhat it does
POST/v1/quilts/<q>/lines/<line>/mergeMerges a square into the line: { square, expect_head?, message?, on_conflict?, dry_run?, expect_tip? }. 201 merged, 200 dry run, 422 refused.
GET/v1/quilts/<q>/lines/<line>/patchesThe line's patches, newest first, with author, first line, file count and square times. ?limit= (default 100, at most 1000) and ?before=<patch_no>.
POST/v1/quilts/<q>/patches/<n>/revertOpens a square that undoes patch n: { line?, message? }. 201.
POST/v1/quilts/<q>/revertOpens a square that undoes any revision: { rev, line?, message? }. 201.
POST/v1/quilts/<q>/lines/<line>/checksReports a check on the line: { check, status: red | green | flaky }. Answers with the line's merge window.
GET/v1/quilts/<q>/lines/<line>/windowThe merge window: window, window_max, used in the last hour, and the red checks.
POST/v1/quilts/<q>/lines/<line>/windowSets the line's largest window: { merge_window_max } (5 or more). Needs admin.

History and files

MethodPathWhat it does
GET/v1/quilts/<q>/logCommits, newest first: ?rev= (default main, or a range a..b), ?paths=a,b, ?limit= (default 20, at most 1000).
GET/v1/quilts/<q>/show/<rev>One commit with its square and its diff: ?format=unified | stat | names | none, ?paths=.
GET/v1/quilts/<q>/diffThe difference between two revisions: ?from=, ?to=, ?paths=, ?format=. Without from: against the parent, or for a square against its base.
GET/v1/quilts/<q>/tree/<rev>The files at a revision: path, mode, blob and size. ?paths= limits it to folders.
GET/v1/quilts/<q>/blob/<rev>/<path>One file, base64 in content. A conflicted file comes with its markers: ?format=diff | snapshot | git.
GET/v1/quilts/<q>/blame/<rev>/<path>Each line with the commit, patch, author and time that last changed it.

Events

MethodPathWhat it does
GET/v1/quilts/<q>/eventsYour events, oldest first: ?after=<cursor>, ?kinds=a,b, ?limit= (default 100). The answer's after is the next cursor.
POST/v1/quilts/<q>/webhookPOSTs your events to a URL: { url, secret?, events?, bearer? }. { url: null } removes it.

Transfer

MethodPathWhat it does
POST/v1/quilts/<q>/haveWhich of these object ids the server is missing: { ids } → { missing }.
POST/v1/quilts/<q>/packsUploads a pack of objects (raw bytes, up to 65 MiB). 201.
POST/v1/quilts/<q>/blobsFile contents by id: { ids } → { blobs: { id: base64 } }. Up to 2,000 ids, and about 32 MiB, a call.
POST/v1/quilts/<q>/import/commitsThe git importer's commits, in order, at most 500 a call. Only for a quilt made with { import: true }. Needs admin.
GET/v1/quilts/<q>/export/gitA line's patches with everything a git commit is made from: ?line=, ?after=<patch_no>, ?limit= (default 200).

Request bodies

Commit

Each change puts a file or deletes one. Put the bytes inline as base64 content, or name a blob you uploaded first with /have and /packs. mode is file (the default), exec or symlink. Paths under .quilt/ are refused.

POST /v1/quilts/my-app/squares/sq:77aa01bc/commit
{
  "expect_head": "0c1d2e3f4a5b…",
  "message": "Rate limit the sign-in route",
  "changes": [
    { "path": "src/limits.js", "op": "put", "content": "ZXhwb3J0IGNvbnN0IExJTUlUID0gMTAK" },
    { "path": "src/old-limits.js", "op": "delete" }
  ]
}

Putting a conflicted file without its conflict tag line settles that conflict.

Merge into a line

POST /v1/quilts/my-app/lines/main/merge
{ "square": "sq:77aa01bc", "message": "Rate limit sign-in", "dry_run": false }

A merge answers { ok: true, merged: { line, patch_no, commit, seq }, retries, combined, fast, warnings, square }. A refused one answers 422 with refused, the square's new head and terms: each conflicted path, its kind and its sides. on_conflict: "leave" keeps the square as it was.

Resolve

choose is ours, theirs, base, suggestion, delete, a side number, or { "blob": "<id>" } for content you uploaded.

Answers

Every write answers with the server's record. A square, as GET /squares/<s> returns it:

{
  "id": "019a2b7e-5c40-7d21-8e3f-0d3e77aa01bc",
  "short": "sq:77aa01bc",
  "title": "Rate limit sign-in",
  "state": "open",
  "line": "main",
  "base": "4d5bb0f2aacd…",
  "head": "0c1d2e3f4a5b…",
  "holder": "agent:5c1e…",
  "opened_by": "agent:5c1e…",
  "writers": [],
  "behind_by": 1,
  "conflicted_paths": 0,
  "opened_at": 1761398000000,
  "updated_at": 1761399500000,
  "merged_under_you": [
    { "patch_no": 784, "commit": "5f4e3d2c1b0a…", "author": "person:7f3e…", "message": "Fix sign-in", "paths": ["src/auth.js"] }
  ],
  "would_conflict": [
    { "path": "src/auth.js", "kind": "content", "patch_no": 784, "author": "person:7f3e…" }
  ]
}

Times are milliseconds since 1970. behind_by counts the patches on the line since the square's base. A merged square adds merged; a folded one adds folded_into.

A commit, as POST /squares/<s>/commit returns it:

{
  "ok": true,
  "seq": 5121,
  "square": { "id": "019a2b7e-…", "short": "sq:77aa01bc", "state": "open", "head": "1e2f3a4b5c6d…", "…": "…" },
  "result": {
    "id": "1e2f3a4b5c6d…",
    "short": "1e2f3a4b5c6d",
    "seq": 5121,
    "square_id": "019a2b7e-…",
    "square": "sq:77aa01bc",
    "parents": ["0c1d2e3f4a5b…"],
    "author": { "member_id": "agent:5c1e…", "kind": "agent", "name": "builder", "tool": "claude" },
    "on_behalf_of": "person:7f3e…",
    "message": "Rate limit the sign-in route",
    "authored_at": 1761399600000,
    "at": 1761399600000,
    "conflicted": 0,
    "stack": {},
    "changes": [
      { "path": "src/limits.js", "op": "put", "blob": "8a7b6c5d…", "mode": "file", "size": 412 },
      { "path": "src/old-limits.js", "op": "delete" }
    ]
  }
}

A commit on a line also has patch: { line, patch_no }. In /log each commit has files (a count) in place of changes.

Errors

A refusal is { ok: false, error, code } with an HTTP status, plus fields that help, such as head or retry_after. The codes you will meet most:

StatuscodeMeans
400bad_requestThe request is not right; error says why.
403forbidden, not_holderYou need a higher level, or the square is held by someone else.
404not_foundNo such quilt (or you are not a member), square, revision or file.
409head_movedThe square moved on since you looked. The answer has its head; look again and retry.
409tip_movedThe line kept moving through five attempts. Call again.
409square_merging, square_closed, square_state, slug_takenThe square is not open, or the slug is taken.
422conflictA merge was refused, or the square still holds conflicts.
422pack_unsealedA blob or record the request names was not uploaded first.
429merge_window, rate_limitedToo many merges or requests. retry_after says how many seconds to wait.

Limits

LimitAn agent's tokenA person's token
Requests (uploads and blob reads not counted)600 a minute3,000 a minute
Commits60 a minute, 2,000 a day300 a minute, 10,000 a day
Merges into a line (dry runs not counted)30 an hour150 an hour
  • Each line takes at most 200 merges an hour, and each quilt 200 an hour across its lines. A red check halves a line's window, down to 5.
  • A JSON body can be up to 96 MiB; a pack up to 65 MiB.