Artifact 89722bdd065cff7ff37e230cbf161647e354c02b5ddb5093ecc1907c7bb9a491:
- File docs/api.md — part of check-in [2eb8893d0a] at 2026-10-03 11:52:17 on branch stale-merge-verdicts — Docs and API doc for stale merge verdicts; checks-state hook on the panel (user: zeitgeist, size: 144970) [annotate] [blame] [check-ins using] [more...]
quarry API
Generated from the op registry by quarry docs — edit the ops, not this file; regenerate with make docs.
Every op also appears in the OpenAPI document (quarry spec). Query inputs ride the query string; the same names work as JSON.
account.add
POST /api/v1/accounts — command, admin
Create a forge account. The password is hashed with argon2id and never stored or returned in plaintext.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
password |
string | required,max=1024 |
admin |
boolean | — |
Output fields:
name(string)admin(boolean)
account.agent
POST /api/v1/accounts/agent — command, token
Mark an account as machine-operated and name the person answerable for it; an empty operator clears the mark. An account may always declare ITSELF an agent. Only a site admin may clear the mark or mark somebody else, because declaring is never the dangerous direction and hiding always is.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
operator |
string | max=64 |
Output fields:
name(string)
account.caps
POST /api/v1/r/{repo}/people/{account} — command, token
Set an account's capability string on a repository — the whole string, not a delta, because a role IS its capability string (RFC 0004). Where account.grant takes one of four preset roles, this takes any letters the forge understands, which is what the settings editor needs. An EMPTY caps removes the account's explicit string; the repository's categories still apply. Refused by RFC 0008's guard where it applies. Raises account.added, account.caps or account.removed (RFC 0005); changed is the sentence those carry. Gated on a, not site admin: who holds what on a repository is that repository's own business.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
account |
string | required,max=64 |
caps |
string | max=64 |
Output fields:
account(string)repo(string)caps(string)changed(string)warning(string)
account.claim
POST /api/v1/accounts/claim — command, anonymous
Redeem an invite: set the account's password from its one-time link. The link is spent; sign in as usual after this.
Input:
| field | type | rules |
|---|---|---|
token |
string | required,min=32,max=128 |
password |
string | required,min=8,max=200 |
Output fields:
name(string)kind(string)
account.delete
POST /api/v1/accounts/delete — command, admin
Delete an account with its grants, tokens, sessions and pending tokens, and retire the name for good. Refused while the account owns repositories: transfer them first.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
Output fields:
name(string)
account.disable
POST /api/v1/accounts/disable — command, admin
Shut an account (on=true) or reopen it. Shutting ends every session it holds at once; the account, its grants and its history stay.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
on |
boolean | — |
account.email.set
POST /api/v1/accounts/email — command, admin
Set or correct an existing account's email as an administrator. For an account that has a password the address is only PENDING: its confirmation code goes to the new address, and only the account itself, signed in, can confirm it. The confirmed address, if any, is told which admin did it and when, and a receipt is written. An invitee who has never set a password is set confirmed at once, as the invite form does.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
email |
string | required,max=200 |
Output fields:
name(string)email(string)pending(string)
account.explain
GET /api/v1/accounts/explain — query, token
Read an account's capability string on a repository as a role, the abilities that differ from it, and a sentence of what the person can do there (RFC 0008). Your own needs nothing; anybody else's needs the admin letter on that repository.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)repo(string)caps(string)effective(string)role(string)exact(boolean)plus(array of string)minus(array of string)can(string)grants(array of string)
account.grant
POST /api/v1/accounts/grant — command, token
Give an account a role on one repository: reader, developer, maintainer or admin (RFC 0008; read and write are accepted as older spellings of the first two). The role's capability string replaces any string the account held there. Who is on a repository is that repository's own business — the a letter, not site admin.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
role |
string | required,oneof=read\|write\|admin\|reader\|developer\|maintainer |
Output fields:
name(string)repo(string)role(string)warning(string)
account.invite
POST /api/v1/accounts/invite — command, admin
Invite a person: create their account with no password and mint a one-time link that lets them set one. Sent to the email when mail is set up; returned only when it was not delivered, so it can be passed on by hand.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
email |
string | max=200 |
admin |
boolean | — |
Output fields:
name(string)link(string)emailed(boolean)mailError(string)
account.list
GET /api/v1/accounts — query, admin
List forge accounts and their per-repository grants.
Output fields:
accounts(array of object {Account: object {name: string, admin: boolean, created: string, email: string, emailPending: string, disabled: string, pending: boolean, lastLogin: string, operator: string}, grants: array of object {repo: string, role: string}})count(integer)
account.peek
GET /api/v1/accounts/invite/{token} — query, anonymous
Name the account behind a live invite token, so the invite page can greet its owner. Says nothing about spent or unknown tokens beyond not found.
Input:
| field | type | rules |
|---|---|---|
token |
string | required,min=32,max=128 |
Output fields:
name(string)kind(string)
account.reset
POST /api/v1/accounts/reset — command, admin
Send an account a one-time link, at its own confirmed address, to choose a new password itself. The email names the admin who asked and when; the current password keeps working until the link is spent, and sending again supersedes any unspent link. The link is never returned: an account with no confirmed address, or a forge with no mail, is refused. Each reset leaves a receipt naming the admin and the account.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
Output fields:
name(string)emailed(boolean)
account.revoke
POST /api/v1/accounts/revoke — command, token
Remove an account's grant on one repository — that repository's own business, the a letter.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)repo(string)role(string)warning(string)
archive.get
GET /api/v1/r/{repo}/archive/{version} — query, anonymous
Download a check-in's files as a reproducible archive (tar.gz or zip). With a bucket (the repository's, else the forge's) the archive is built once, kept there, and every request is answered 302 to a presigned GET good for five minutes; without one the answer is the archive itself, as bytes with its content type and an ETag naming the check-in — not a JSON envelope. Signed-in readers build on a lane of their own, anonymous requests for an open branch's tip or a tagged check-in on the normal one, and every other anonymous request (a closed branch's tip, an untagged check-in, a pair that is not an open landing's) on the smallest; a full lane answers 503 with Retry-After.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
version |
string | max=140 |
format |
string | oneof=tar\|zip |
artifact.resolve
GET /api/v1/r/{repo}/info/{prefix} — query, anonymous
Resolve an artifact hash prefix to its kind and canonical reference — fossil's /info dispatch.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
prefix |
string | required,min=4,max=64 |
Output fields:
kind(string)ref(string)
attachment.begin
POST /api/v1/r/{repo}/attachment/begin — command, token
Begin an upload (RFC 0016 Phase 3): name the target, the file's name, size, type and SHA-256, and receive a presigned PUT — valid fifteen minutes, its content length and type signed — and a ticket for attachment.commit. Refused when the target is unknown or read-only, no bucket is bound, the size is over the binding's limit, or the type is not on the policy (text, the common images, PDF, JSON, the common audio and video; archives only when the repository allows them; executables never). An empty target with a family (ticket, forum or wiki) DEFERS the ticket for an object that does not exist yet or is about to change: the same gates apply against the family's letter, and the ticket is named its target by the attach list of forum.post, forum.reply, ticket.new, ticket.set and wiki.save, or by attachment.commit.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
target |
string | max=200 |
family |
string | max=16 |
name |
string | required,max=255 |
size |
integer | min=1 |
type |
string | required,max=128 |
sha256 |
string | required,min=64,max=64 |
Output fields:
ticket(string)url(string)method(string)headers(map)expires(string)
attachment.commit
POST /api/v1/r/{repo}/attachment/commit — command, token
Finish an upload: quarry HEADs the object and refuses if it is absent or its length differs from what attachment.begin was told, then writes the stub and the A card through zit and answers both hashes. Against a ticket on the forge tier the file is HELD instead (local is true, no artifact or blob yet) and its A card is written when the ticket is promoted. A deferred ticket (begun with no target) needs target here, checked against the family it was begun for. held is true when the caller lacks the family's moderation letter (q on tickets, l on the wiki and notes, 5 on the forum) and is not an administrator: the file is in the DAG and shown as a link until a moderator vouches for it.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
ticket |
string | required,max=64 |
target |
string | max=200 |
Output fields:
artifact(string)blob(string)name(string)size(integer)type(string)sha256(string)held(boolean)local(boolean)
attachment.get
GET /api/v1/r/{repo}/attachment/{hash} — query, anonymous
Fetch one attachment's content by its blob hash: src in attachment.list, blob in attachment.commit's reply. Not artifact, which is the hash of the attachment record, not of the file.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
Output fields:
name(string)size(integer)content(string)
attachment.head
GET /api/v1/r/{repo}/attachment/{hash}/head — query, anonymous
Describe one attachment without downloading it: its size, which of the four shapes it takes (image, text, media, other), and for text the first few lines.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
lines |
integer | min=0,max=1000 |
Output fields:
name(string)size(integer)kind(string)mimetype(string)lines(integer)text(string)truncated(boolean)external(boolean)sha256(string)held(boolean)reason(string)
attachment.held
GET /api/v1/r/{repo}/attachments/held — query, token
The review queue: uploads on this repository nobody trusted has vouched for, oldest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
holds(array of object {repo: string, artifact: string, target: string, name: string, who: string, created: string})
attachment.link
GET /api/v1/r/{repo}/attachment/{hash}/link — query, anonymous
Where an uploaded attachment's bytes are: a link on the bucket's public base when the repository is public and one is set, otherwise a presigned GET good for an hour that names the file and its type. inline is answered only for a vouched file whose type may render in a page; SVG never. A real (non-upload) attachment answers external: false and is served by quarry's own routes.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
Output fields:
external(boolean)name(string)type(string)size(integer)held(boolean)download(string)inline(string)expires(string)
attachment.list
GET /api/v1/r/{repo}/attachments/{target} — query, anonymous
List the attachments live on one target — a ticket id, a wiki page name, a technote event id, or a forum post hash.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
target |
string | required,max=200 |
Output fields:
attachments(array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean})
attachment.reject
POST /api/v1/r/{repo}/attachments/reject — command, token
Reject a held upload: the hold is cleared and the attachment removed as attachment.remove would, object deletion included when the repository says so.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
artifact |
string | required,min=4,max=64 |
reason |
string | max=500 |
Output fields:
artifact(string)target(string)name(string)removed(boolean)deleted(boolean)
attachment.remove
POST /api/v1/r/{repo}/attachments/remove — command, token
Remove an attachment: an A card with no source, fossil's own deletion form. The attach letters reach this op because removing your OWN upload is part of attaching; removing anybody else's needs the family's moderation letter (q on tickets, l on the wiki and notes, 5 on the forum) or the administrator's. Nothing leaves the DAG. When the repository's setting deleteObjects is on and the attachment was an upload, the object is deleted from the bucket too, unless another live attachment names the same bytes.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
target |
string | required,max=200 |
name |
string | required,max=255 |
Output fields:
artifact(string)target(string)name(string)removed(boolean)deleted(boolean)
attachment.vouch
POST /api/v1/r/{repo}/attachments/vouch — command, token
Vouch for a held upload: it renders from now on. Needs the family's moderation letter (q, l or 5) or the administrator's.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
artifact |
string | required,min=4,max=64 |
reason |
string | max=500 |
Output fields:
artifact(string)held(boolean)
attention.read
POST /api/v1/me/attention/read — command, token
Move the signed-in account's attention read pointer to seq (default: everything so far). Forward-only — a stale client replaying an old cursor cannot resurrect mail already dismissed.
Input:
| field | type | rules |
|---|---|---|
seq |
integer | — |
Output fields:
ok(boolean)seq(integer)
avatar.clear
POST /api/v1/avatar/clear — command, token
Take away an uploaded mark; the identicon derived from the name comes back. The object is left in the forge's storage — a removal is a decision about the page and a deletion one about the bytes (RFC 0016).
Input:
| field | type | rules |
|---|---|---|
kind |
string | required,oneof=repo\|team\|user |
name |
string | required,max=64 |
Output fields:
ok(boolean)mime(string)etag(string)
avatar.get
GET /api/v1/avatar/{kind}/{name} — query, anonymous
Read an uploaded mark's bytes, base64, so the FORGE can serve it. The page never links the bucket: quarry's privacy page promises the interface loads nothing from another host, and an image element pointed at the storage would break that for every reader of every page it appears on.
Input:
| field | type | rules |
|---|---|---|
kind |
string | required,oneof=repo\|team\|user |
name |
string | required,max=64 |
Output fields:
mime(string)etag(string)body(string)
avatar.set
POST /api/v1/avatar — command, token
Give a repository or a team an uploaded mark, replacing any it had. The image goes to the storage the FORGE supplies, not a repository's own binding: a mark is shown on listings other people read, so it must not vanish when somebody unbinds their bucket. PNG, JPEG, GIF or WebP, recognised from the bytes rather than from what the upload claimed, and at most 512 KiB.
Input:
| field | type | rules |
|---|---|---|
kind |
string | required,oneof=repo\|team\|user |
name |
string | required,max=64 |
body |
string | required,max=1400000 |
Output fields:
ok(boolean)mime(string)etag(string)
branch.alias
GET /api/v1/r/{repo}/branch/{branch}/alias — query, anonymous
Say whether a branch name is another name for a branch that resolves to something else. A fossil branch can carry several propagating symbolic names, and a rename in practice adds one without cancelling the old, so both names keep resolving forever. A pushed check-in resolves to exactly one of them, which is the name branch protection is matched against.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
branch |
string | required,max=200 |
Output fields:
branch(string)alias(string)isAlias(boolean)
branch.list
GET /api/v1/r/{repo}/branch — query, anonymous
List branches with their tip check-in, most recently active first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=5000 |
open |
boolean | — |
Output fields:
branches(array of object {Branch: object {name: string, tip: string, date: string, closed: boolean}, protected: boolean, owners: array of string})count(integer)
branch.merges
GET /api/v1/r/{repo}/branch/merges — query, anonymous
Where a branch stands relative to the repository's default branch, asked of the STORE: clean, up-to-date, unrelated or conflict. landed is the one question a propose-a-landing offer turns on — a branch whose work is already in the target has nothing to propose, and offering it opens a record that says up-to-date on arrival. Empty state means no probe could run, and landed is then false: withholding an offer because a subprocess failed would hide real stranded work behind an infrastructure fault.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
branch |
string | required,max=200 |
Output fields:
state(string)landed(boolean)target(string)tip(string)
branch.unproposed
GET /api/v1/r/{repo}/branch/unproposed — query, anonymous
Branch tips whose work nobody has proposed: an open leaf, not the default branch, with no open landing naming it, and not sitting at work a landing already turned down. The blind spot of the landing queue — work that is finished and not going anywhere, which no other surface reports.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
rows(array of object {branch: string, tip: string, date: string, target: string})count(integer)
bucket.get
GET /api/v1/r/{repo}/bucket — query, token
The bucket this repository's uploads go to (RFC 0016): endpoint, bucket, region, access key, public base, limit and the last probe. corsOk says the bucket answered a browser preflight for the forge's origin at that probe; false means the page's scripted upload is blocked until the bucket's CORS rule allows the forge, while the multipart form still works. A companion store answers its parent's binding with inherited naming the parent. The secret is never returned.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)bound(boolean)forge(boolean)note(string)endpoint(string)bucket(string)region(string)accessKey(string)publicBase(string)limit(integer)probedAt(string)probeErr(string)corsOk(boolean)inherited(string)archives(boolean)deleteObjects(boolean)
bucket.limit
GET /api/v1/r/{repo}/bucket/limit — query, token
Whether this repository takes uploads and how large one may be — the uploader's question, answered to holders of the upload letters without the binding's detail.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)bound(boolean)limit(integer)note(string)
bucket.policy
POST /api/v1/r/{repo}/bucket/policy — command, token
The binding's two switches: archives opens the upload type policy to zip, gzip and tar; deleteObjects makes a removal delete the object from the bucket as well. Both are off by default.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
archives |
boolean | — |
deleteObjects |
boolean | — |
Output fields:
repo(string)bound(boolean)forge(boolean)note(string)endpoint(string)bucket(string)region(string)accessKey(string)publicBase(string)limit(integer)probedAt(string)probeErr(string)corsOk(boolean)inherited(string)archives(boolean)deleteObjects(boolean)
bucket.probe
POST /api/v1/r/{repo}/bucket/probe — command, token
Probe the bound bucket again — a presigned PUT and DELETE, then a browser preflight from the forge's origin — and record the outcome. The answer carries probedAt and, when it failed, probeErr; corsOk is the preflight's verdict. A failed probe is an answer, not an error.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)bound(boolean)forge(boolean)note(string)endpoint(string)bucket(string)region(string)accessKey(string)publicBase(string)limit(integer)probedAt(string)probeErr(string)corsOk(boolean)inherited(string)archives(boolean)deleteObjects(boolean)
bucket.set
POST /api/v1/r/{repo}/bucket — command, token
Bind (or re-bind) the bucket this repository's uploads go to. The endpoint must be https; region defaults to auto (R2); limit is bytes and defaults to 100 MiB. The binding is probed first — a presigned PUT and DELETE of one small object under attachments/ — and refused with the probe's answer if either fails. An empty secret on a bound repository keeps the stored one. Refused on a mirror, which accepts no writes, and on a companion store, which uses its parent's bucket.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
endpoint |
string | required,max=512 |
bucket |
string | required,max=128 |
region |
string | max=64 |
accessKey |
string | required,max=256 |
secret |
string | max=512 |
publicBase |
string | max=512 |
limit |
integer | min=0 |
Output fields:
repo(string)bound(boolean)forge(boolean)note(string)endpoint(string)bucket(string)region(string)accessKey(string)publicBase(string)limit(integer)probedAt(string)probeErr(string)corsOk(boolean)inherited(string)archives(boolean)deleteObjects(boolean)
bucket.unset
POST /api/v1/r/{repo}/bucket/unset — command, token
Forget the repository's bucket binding. The objects stay in the steward's bucket and the stubs stay in the DAG; binding a bucket with the same objects makes them live again.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)unbound(boolean)
changelog.announce
POST /api/v1/changelog/announce — command, admin
Tell every account about the newest changelog entry: an inbox item and a push through the notify plane, naming the entry and linking the page. Idempotent per entry heading.
Input:
| field | type | rules |
|---|---|---|
base |
string | max=200 |
Output fields:
heading(string)recipients(integer)
changelog.get
GET /api/v1/changelog — query, anonymous
The forge's changelog, rendered, with its entry headings newest first and the build serving it.
Output fields:
version(string)html(string)entries(array of string)
chat.delete
POST /api/v1/r/{repo}/chat/{seq}/delete — command, token
Delete a chat message: own with C, any message with a.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
seq |
integer | required |
Output fields:
ok(boolean)seq(integer)
chat.edit
POST /api/v1/r/{repo}/chat/{seq}/edit — command, token
Edit a chat message posted within the last 5 minutes, own only.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
seq |
integer | required |
text |
string | required,max=4096 |
Output fields:
ok(boolean)seq(integer)
chat.get
GET /api/v1/r/{repo}/chat/{seq} — query, anonymous
One folded chat message with its direct replies; the permalink.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
seq |
integer | required |
before |
integer | min=0,max=50 |
after |
integer | min=0,max=50 |
Output fields:
message(object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})replies(array of object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})earlier(array of object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})later(array of object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})more_earlier(integer)more_later(integer)expired(boolean)
chat.history
GET /api/v1/r/{repo}/chat — query, anonymous
Recent messages in a repository's chat channel, oldest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
before |
integer | — |
after |
integer | — |
limit |
integer | min=0,max=500,default=100 |
Output fields:
messages(array of object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})count(integer)next(integer)read(integer)later(integer)
chat.post
POST /api/v1/r/{repo}/chat — command, token
Post a message to a repository's chat channel.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
text |
string | required,max=4096 |
ref |
integer | — |
Output fields:
ok(boolean)seq(integer)
chat.preview
POST /api/v1/r/{repo}/chat/preview — command, token
Render a draft message exactly as posting it would — same markdown, same mentions, same reference resolution. A preview from any other renderer is a guess.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
text |
string | required,max=4096 |
Output fields:
html(string)
chat.purge
POST /api/v1/r/{repo}/chat/{seq}/purge — command, token
Remove an already-deleted message from the timeline: erase the message and its tombstone from the stream. Own with C, any with a. Irreversible.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
seq |
integer | required |
Output fields:
ok(boolean)purged(integer)
chat.read
POST /api/v1/r/{repo}/chat/read — command, token
Mark a repository's chat channel read up to seq (default: latest).
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
seq |
integer | — |
Output fields:
ok(boolean)seq(integer)
chat.robot
POST /api/v1/r/{repo}/chat/robot — command, token
Set which events the activity robot announces in a repository's chat channel, in the webhook pattern syntax (*, a noun like checkin, or an exact kind like landing.accepted), space-separated. Empty turns the robot off.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
events |
string | max=512 |
Output fields:
repo(string)events(string)on(boolean)
chat.robot.get
GET /api/v1/r/{repo}/chat/robot — query, token
The repository's activity-robot event patterns; empty when the robot is off. Its own op rather than a field on repo.get, because it is chat configuration and only an administrator may see it.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)events(string)on(boolean)
chat.stream
GET /api/v1/r/{repo}/chat/stream — stream, anonymous
Live chat messages as Server-Sent Events. The body is text/event-stream, not JSON: each event's id: line is the message's stream sequence, and a client that reconnects with Last-Event-ID: <seq> resumes after it. A resume older than the window replays from the window's first message, preceded by an event: gap frame carrying {"from":<asked>,"first":<available>} so a renderer re-renders rather than appends. The sequence is the FORGE'S, not this channel's: every repository's chat shares one stream, so the ids a reader of one channel receives are a subset and gaps between them are ordinary rather than loss. Do not detect loss by looking for holes -- reconnecting with Last-Event-ID is the only correct way to find out what you missed.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
from |
integer | — |
Output fields:
seq(integer)kind(string)ref(integer)repo(string)user(string)text(string)origin(string)at(string)edited(boolean)deleted(boolean)html(string)refs(array of string)replies(integer)refUser(string)refLine(string)
chat.unread
GET /api/v1/me/chat/unread — query, token
Per-repository unread chat counts for the signed-in account.
Output fields:
repos(map)
check.report
POST /api/v1/r/{repo}/checks — command, anonymous
Report an external check's state onto a check-in — the commit-status contract, quarry-shaped (RFC 0009 §external-ci). Authenticated by a reporter token minted in the repository's CI settings; the token reaches this op and nothing else.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
checkin |
string | required,min=4,max=200 |
context |
string | required,max=100 |
state |
string | required,oneof=pending\|success\|failure\|error |
url |
string | max=300 |
summary |
string | max=2048 |
token |
string | required,max=128 |
Output fields:
ok(boolean)context(string)state(string)
checkin.close
POST /api/v1/r/{repo}/checkin/{hash}/close — command, token
Close a leaf, or reopen one: records fossil's closed tag on the check-in. Closing says a line of work has ended and hides the leaf from the open set. On a PROTECTED branch it takes an owner's hand, both ways: a closed leaf refuses its own branch's next check-in, so closing one is a change to the branch and not only to the check-in. Closing a check-in the store already holds closed writes nothing and succeeds, so a retried close never records a second tag.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
reopen |
boolean | — |
Output fields:
hash(string)closed(boolean)
checkin.diff
GET /api/v1/r/{repo}/checkin/{hash}/diff — query, anonymous
Diff one check-in against its primary parent.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
file |
string | max=4096 |
offset |
integer | min=0 |
maxLines |
integer | min=0 |
branch |
boolean | — |
from |
string | max=64 |
unified |
boolean | — |
Output fields:
from(string)to(string)files(array of object {status: string, name: string})structured(array of object {path: string, prevPath: string, change: string, hunks: array of object {oldStart: integer, newStart: integer, header: string, lines: array of object {kind: string, oldNum: integer, newNum: integer, tokens: array of object {class: string, text: string, mark: boolean}}}, added: integer, deleted: integer, before: integer, omitted: integer})unified(string)note(string)omittedLines(integer)nextOffset(integer)totalLines(integer)lastOffset(integer)offset(integer)
checkin.get
GET /api/v1/r/{repo}/checkin/{hash} — query, anonymous
Describe one check-in: metadata, parents, and its file set.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
files |
string | max=4 |
Output fields:
hash(string)ci(string)ciRun(string)checks(array of object {url: string, runId: string, checkin: string, pair: string, workflow: string, pipeline: string, status: string, started: string, completed: string, summary: string, branch: string, comment: string, user: string, rerunOf: string, rerunAs: string, reason: string, event: string, inheritedFrom: string})required(array of string)unmet(array of string)branch(string)date(string)user(string)comment(string)parents(array of string)files(array of object {blob: string, name: string, size: integer, perm: string, binary: boolean})discussedIn(array of object {root: string, title: string, posts: integer, created: string, updated: string, user: string, excerpt: string, hasPoll: boolean, participants: array of string, dates: array of string})landing(object {id: string, number: integer, title: string, state: string, role: string})chatRefs(object {count: integer, seq: integer, at: string})tags(array of string)commentOrig(string)hidden(boolean)closed(boolean)fork(string)context(array of object {kind: string, hash: string, ref: string, date: string, user: string, comment: string, branch: string, isLeaf: boolean, isMerge: boolean, parents: array of string, change: string, ci: string, commentOrig: string, hidden: boolean, closed: boolean, landing: string, landingTitle: string, landingApprover: string, landingNumber: integer, excerpt: string})
checkin.manifest
GET /api/v1/r/{repo}/checkin/{hash}/manifest — query, anonymous
One check-in's manifest artifact, as cards. The file list is counted, not returned — it is 99% of the bytes and the tree browser already renders it.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
Output fields:
hash(string)size(integer)cards(array of object {letter: string, text: string, note: string})files(integer)
checkin.tags
GET /api/v1/r/{repo}/checkin/{hash}/tags — query, anonymous
The full tag history of a check-in — every tag artifact that touched it, oldest first, with which rows still stand.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=4,max=64 |
Output fields:
hash(string)date(string)user(string)comment(string)commentOrig(string)branch(string)hidden(boolean)closed(boolean)events(array of object {date: string, src: string, name: string, sign: string, value: string, user: string, standing: boolean})
ci.cancel
POST /api/v1/r/{repo}/ci/runs/{runId}/cancel — command, token
End a CI run that is still going: the engine stops it and it settles as cancelled, with the cancelling admin recorded as the reason. For runs nothing will ever finish, such as one waiting on a task type no worker serves.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
runId |
string | required,max=140 |
Output fields:
ok(boolean)
ci.environment.delete
POST /api/v1/r/{repo}/ci/environments/delete — command, token
Delete a CI environment and every secret in it. Recorded in the settings log.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=64 |
Output fields:
environments(array of object {name: string, branches: array of string, secrets: array of object {name: string, updated: string, updatedBy: string}, allowed: string, starter: string})ready(boolean)
ci.environment.set
POST /api/v1/r/{repo}/ci/environments — command, token
Create a CI environment or change which branches may receive its secrets and run tokens: no branches means protected branches only (RFC 0007), the default. allowed is the letters a run token may at most carry there (default o g 8), starter the one letter the person who starts a run must hold for its jobs to receive one (RFC 0021). Name "" is the repository's own policy, for jobs naming no environment. A landing's merge never qualifies, whatever the policy. Recorded in the settings log.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | max=64 |
branches |
array of string | max=32 |
allowed |
string | — |
starter |
string | — |
Output fields:
environments(array of object {name: string, branches: array of string, secrets: array of object {name: string, updated: string, updatedBy: string}, allowed: string, starter: string})ready(boolean)
ci.environments
GET /api/v1/r/{repo}/ci/environments — query, token
A repository's CI environments (RFC 0019): each one's branch policy and the names of its secrets, never their values. The environment named "" holds the repository's own secrets.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
environments(array of object {name: string, branches: array of string, secrets: array of object {name: string, updated: string, updatedBy: string}, allowed: string, starter: string})ready(boolean)
ci.gate.ack
POST /api/v1/r/{repo}/ci/gate/ack — command, token
Acknowledge gate-bypass findings — check-ins that moved a protected branch without a forge accept. Acknowledging hides a finding from the CI page's strip; the record stays in the history. One check-in, or all=true for every open finding.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
checkin |
string | max=64 |
all |
boolean | — |
Output fields:
acked(integer)
ci.gate.list
GET /api/v1/r/{repo}/ci/gate — query, anonymous
Every gate-bypass finding for a repository, open or acknowledged, newest first, paged.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
offset |
integer | min=0 |
limit |
integer | min=0,max=200,default=50 |
Output fields:
findings(array of object {repo: string, branch: string, checkin: string, seen: string, who: string, acked: string})count(integer)total(integer)
ci.list
GET /api/v1/r/{repo}/ci — query, anonymous
List a repository's CI runs, newest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
offset |
integer | min=0 |
Output fields:
runs(array of object {url: string, runId: string, checkin: string, pair: string, workflow: string, pipeline: string, status: string, started: string, completed: string, summary: string, branch: string, comment: string, user: string, rerunOf: string, rerunAs: string, reason: string, event: string, inheritedFrom: string})count(integer)bypasses(array of object {repo: string, branch: string, checkin: string, seen: string, who: string, acked: string})
ci.pipeline.save
POST /api/v1/r/{repo}/ci/pipeline — command, token
Validate a pipeline, or commit it to a fresh branch and open a landing. Never writes to the target branch: a pipeline edit lands like any other change.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
path |
string | required,max=120 |
content |
string | required,max=262144 |
message |
string | max=500 |
validate |
boolean | — |
Output fields:
diagnostics(array of string)hash(string)branch(string)landing(string)
ci.run.tokens
GET /api/v1/r/{repo}/ci/runs/{runId}/tokens — query, anonymous
The run tokens a CI run's jobs carried (RFC 0021): each job's letters and when its token was revoked, "" while it lives. Never a value.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
runId |
string | required,max=128 |
Output fields:
tokens(array of object {job: string, letters: string, created: string, expires: string, revoked: string})
ci.schedules
GET /api/v1/r/{repo}/ci/schedules — query, anonymous
A repository's scheduled workflows (on: schedule), read from its default branch tip: each one's cron lines (five fields, UTC) and its next fire. The forge starts them; a minute missed while it was down is not run later.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
offset |
integer | min=0 |
Output fields:
schedules(array of object {pipeline: string, crons: array of string, next: object {}, error: string, lastFailure: string, lastFailureAt: object {}})
ci.secret.delete
POST /api/v1/r/{repo}/ci/secrets/delete — command, token
Delete a CI secret. Recorded in the settings log.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
environment |
string | max=64 |
name |
string | required,max=128 |
Output fields:
environments(array of object {name: string, branches: array of string, secrets: array of object {name: string, updated: string, updatedBy: string}, allowed: string, starter: string})ready(boolean)
ci.secret.set
POST /api/v1/r/{repo}/ci/secrets — command, token
Set a CI secret, in an environment or (environment "") the repository's own. The value is sealed with the forge's key before it is stored and is never returned or logged; the settings log records that it was set, by whom.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
environment |
string | max=64 |
name |
string | required,max=128 |
value |
string | required |
Output fields:
environments(array of object {name: string, branches: array of string, secrets: array of object {name: string, updated: string, updatedBy: string}, allowed: string, starter: string})ready(boolean)
ci.token.list
GET /api/v1/r/{repo}/ci/tokens — query, token
List a repository's reporter tokens: labels, creation times and hashes — never the tokens.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
tokens(array of object {hash: string, label: string, created: string, used: string})count(integer)
ci.token.new
POST /api/v1/r/{repo}/ci/tokens — command, token
Mint a reporter token for external CI on one repository. The token is returned once and never stored; only its hash is kept.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
label |
string | max=100 |
Output fields:
token(string)
ci.token.revoke
POST /api/v1/r/{repo}/ci/tokens/revoke — command, token
Revoke one reporter token by its stored hash, returning the remaining list.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,max=64 |
Output fields:
tokens(array of object {hash: string, label: string, created: string, used: string})count(integer)
ci.trigger
POST /api/v1/r/{repo}/ci/trigger — command, token
Run a repository's pipelines now, at a check-in or the tip (RFC 0009 §triggers). The re-run button; deploys and nightlies by hand.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
checkin |
string | max=140 |
landing |
string | max=64 |
Output fields:
ok(boolean)
config.export
GET /api/v1/r/{repo}/config/export/{area} — query, token
Download one configuration area (user or shun) in fossil's export format — the file config.import reads back, so a table can be backed up before it is replaced. Restoring is exact for user; for shun it only adds, since zit never un-shuns. The user area carries password hashes.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
area |
string | required,oneof=user\|shun |
config.import
POST /api/v1/r/{repo}/config/import — command, token
Apply a fossil configuration-area export file to a store — replace (import) or fold (merge), as the file's cards decide (RFC 0008 rung 8). supported=false until zit#153.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
file |
string | required,max=8388608 |
merge |
boolean | — |
Output fields:
supported(boolean)note(string)settings(array of object {name: string, value: string, versionable: boolean})
doc.get
GET /api/v1/r/{repo}/doc/{hash}/{path...} — query, anonymous
Render one file from the tree as embedded documentation, chosen by extension.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=1,max=64 |
path |
string | required,max=512 |
Output fields:
hash(string)path(string)size(integer)mimetype(string)html(string)outline(array of object {level: integer, id: string, text: string})contentType(string)content(string)
doc.list
GET /api/v1/r/{repo}/doc — query, anonymous
List the documents in a check-in's conventional documentation directories.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
version |
string | max=64 |
Output fields:
version(string)docs(array of object {path: string, size: integer, date: string, user: string})
file.get
GET /api/v1/r/{repo}/file/{hash}/{path...} — query, anonymous
Fetch one file's content at a check-in.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
hash |
string | required,min=3,max=64 |
path |
string | required,max=512 |
from |
integer | min=0 |
lines |
integer | min=0,max=5000 |
Output fields:
hash(string)path(string)size(integer)binary(boolean)content(string)html(string)renderable(boolean)plain(string)from(integer)nextFrom(integer)totalLines(integer)
file.history
GET /api/v1/r/{repo}/history/{version}/{path...} — query, anonymous
List the check-ins that changed one file, newest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
version |
string | max=100 |
path |
string | required,max=1024 |
limit |
integer | min=0,max=200,default=50 |
Output fields:
path(string)version(string)changes(array of object {hash: string, date: string, user: string, comment: string})count(integer)
fleet.bucket.get
GET /api/v1/fleet/bucket — query, admin
The storage the forge supplies to every repository that has not bound its own.
Output fields:
bound(boolean)info(object {repo: string, bound: boolean, forge: boolean, note: string, endpoint: string, bucket: string, region: string, accessKey: string, publicBase: string, limit: integer, probedAt: string, probeErr: string, corsOk: boolean, inherited: string, archives: boolean, deleteObjects: boolean})
fleet.bucket.set
POST /api/v1/fleet/bucket — command, admin
Bind the storage the forge supplies. Every repository without a bucket of its own uses it, so a forge can host attachments for everybody on it. Probed before it is written, like a repository's own binding; an empty secret keeps the stored one.
Input:
| field | type | rules |
|---|---|---|
endpoint |
string | required,max=512 |
bucket |
string | required,max=200 |
region |
string | max=64 |
accessKey |
string | required,max=200 |
secret |
string | max=400 |
publicBase |
string | max=512 |
limit |
integer | — |
Output fields:
bound(boolean)info(object {repo: string, bound: boolean, forge: boolean, note: string, endpoint: string, bucket: string, region: string, accessKey: string, publicBase: string, limit: integer, probedAt: string, probeErr: string, corsOk: boolean, inherited: string, archives: boolean, deleteObjects: boolean})
fleet.bucket.unset
POST /api/v1/fleet/bucket/unset — command, admin
Stop supplying storage. Repositories with a bucket of their own are unaffected; the rest lose attachments until they bind one. Nothing is deleted: the objects stay where they are and the stubs stay in the DAG.
Output fields:
bound(boolean)info(object {repo: string, bound: boolean, forge: boolean, note: string, endpoint: string, bucket: string, region: string, accessKey: string, publicBase: string, limit: integer, probedAt: string, probeErr: string, corsOk: boolean, inherited: string, archives: boolean, deleteObjects: boolean})
fleet.exception.clear
POST /api/v1/fleet/exceptions/clear — command, admin
Discard one error signature and its count. The next occurrence is a first sighting.
Input:
| field | type | rules |
|---|---|---|
sig |
string | — |
Output fields:
cleared(boolean)
fleet.status
GET /api/v1/fleet — query, admin
Report every served store's ingest health.
Output fields:
repos(array of object {name: string, parent: string, code: string, checkins: integer, indexing: boolean, health: object {lastOk: string, roundSeconds: number, lastCount: integer, total: integer, fallbacks: integer, gaps: integer, lastErr: string, lastErrAt: string, failures: integer, stalled: boolean, retryAt: string}, bucket: string, bucketProbedAt: string, bucketErr: string})count(integer)unhealthy(integer)errors(array of object {message: string, surface: string, path: string, count: integer, first: object {}, last: object {}, repo: string, trace: string, why: string, fix: string, fields: array of object {key: string, value: string, private: boolean}, build: string, req: string, rate: array of integer, group: string})errorTotal(integer)errorDistinct(integer)
fleet.sync
POST /api/v1/fleet/sync — command, admin
Run an ingest round now — one store, or every store when repo is empty.
Input:
| field | type | rules |
|---|---|---|
repo |
string | max=64 |
Output fields:
results(array of object {repo: string, artifacts: integer, err: string})failed(integer)
forum.get
GET /api/v1/r/{repo}/forum/{root} — query, anonymous
Fetch one forum thread: every post, rendered, in reading order.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
root |
string | required,min=4,max=64 |
Output fields:
root(string)title(string)excerpt(string)posts(array of object {hash: string, parent: string, date: string, user: string, html: string, edits: integer, edited: string, score: integer, yourVote: integer, hidden: boolean, hiddenReason: string, poll: object {present: boolean, question: string, options: array of string, counts: array of integer, percents: array of integer, total: integer, yourChoice: integer, closed: boolean, closes: string}, attachments: array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean}})yourSub(string)chatRefs(object {count: integer, seq: integer, at: string})
forum.history
GET /api/v1/r/{repo}/forum/{root}/history/{post} — query, anonymous
Every version of one forum post, oldest first — the edits a thread page folds into its newest version.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
root |
string | required,min=4,max=64 |
post |
string | required,min=4,max=64 |
Output fields:
root(string)title(string)versions(array of object {hash: string, parent: string, date: string, user: string, html: string, edits: integer, edited: string, score: integer, yourVote: integer, hidden: boolean, hiddenReason: string, poll: object {present: boolean, question: string, options: array of string, counts: array of integer, percents: array of integer, total: integer, yourChoice: integer, closed: boolean, closes: string}, attachments: array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean}})
forum.list
GET /api/v1/r/{repo}/forum — query, anonymous
List a repository's forum threads, most-recently-active first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
sort |
string | max=8 |
limit |
integer | min=0,max=200,default=50 |
before |
string | max=96 |
offset |
integer | min=0,max=100000 |
Output fields:
threads(array of object {ForumThread: object {root: string, title: string, posts: integer, created: string, updated: string, user: string, excerpt: string, hasPoll: boolean, participants: array of string, dates: array of string}, score: integer, category: string, spark: array of integer, read: boolean})held(array of object {id: integer, kind: string, root: string, title: string, at: string, state: string, reason: string, body: string})moderated(boolean)yourPostsHeld(boolean)announcements(array of object {ForumThread: object {root: string, title: string, posts: integer, created: string, updated: string, user: string, excerpt: string, hasPoll: boolean, participants: array of string, dates: array of string}, score: integer, category: string, spark: array of integer, read: boolean})count(integer)total(integer)next(string)
forum.post
POST /api/v1/r/{repo}/forum — command, token
Open a new forum thread. attach lists deferred upload tickets to commit against the new post; refusals come back in attachErrors.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
title |
string | required,max=200 |
body |
string | required,max=1048576 |
mimetype |
string | max=64 |
attach |
array of string | max=20 |
Output fields:
hash(string)staged(boolean)attachErrors(array of string)
forum.reply
POST /api/v1/r/{repo}/forum/{root} — command, token
Reply to a forum post within its thread. attach lists deferred upload tickets to commit against the reply; refusals come back in attachErrors.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
root |
string | required,min=4,max=64 |
to |
string | max=64 |
body |
string | required,max=1048576 |
mimetype |
string | max=64 |
attach |
array of string | max=20 |
Output fields:
hash(string)staged(boolean)attachErrors(array of string)
hook.deliveries
GET /api/v1/r/{repo}/hook/{sub}/deliveries — query, token
What one webhook endpoint actually received: recent attempts with their status, latency and error. The events page says what quarry RAISED; this says what arrived, and the two differ exactly when something is broken.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
sub |
integer | required,min=1 |
limit |
integer | min=0,max=100,default=10 |
Output fields:
deliveries(array of object {id: integer, sub: integer, kind: string, at: string, status: integer, ms: integer, err: string, attempt: integer})count(integer)
hook.ping
POST /api/v1/r/{repo}/hook/{id}/ping — command, token
Send a signed test event to one endpoint. ID names the SUBSCRIPTION. Proves an endpoint is reachable and its signature check works, without waiting for something real to happen.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
integer | required,min=1 |
Output fields:
sent(boolean)status(integer)ms(integer)err(string)
hook.redeliver
POST /api/v1/r/{repo}/hook/delivery/{id}/redeliver — command, token
Send a recorded delivery again, byte for byte. ID names the DELIVERY. The stored body is replayed rather than the event rebuilt, so the signature is the one the endpoint saw before.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
integer | required,min=1 |
Output fields:
sent(boolean)status(integer)ms(integer)err(string)
inbox.count
GET /api/v1/me/inbox/count — query, token
How much is waiting for the signed-in account: the unread notify inbox PLUS the local attention events after the account's read pointer that notify holds no copy of. When notify is configured every person-addressed event is written to BOTH halves, so the two are paired on the idempotency key quarry set on each and a paired event is counted once — under notify's read state, which is the one inbox.list renders. The pairing is read off a bounded page of each half; a local backlog deeper than that page is counted whole rather than silently dropped. A forge with no notify companion counts only the attention stream.
Output fields:
unread(integer)
inbox.list
GET /api/v1/me/inbox — query, token
One page of everything addressed to the signed-in account, newest first, merging the notify inbox with quarry's own attention stream. Each row carries a reason (mention, reply, landing, activity) derived from the workflow that sent it, and an id that inbox.mark takes back — a notify message id, or local:<seq> for an attention event. An event quarry raised while notify was configured exists in both halves; the two are paired on the idempotency key each carries and render as ONE row, notify's, whose id names both halves so a single mark reaches both. before is the opaque next cursor from a previous page; it addresses BOTH sources at once, so a row the page limit cut off is served by the next page rather than skipped.
Input:
| field | type | rules |
|---|---|---|
before |
string | max=1024 |
limit |
integer | min=0,max=200 |
Output fields:
rows(array of object {id: string, source: string, reason: string, repo: string, from: string, summary: string, link: string, at: string, read: boolean, seq: integer})next(string)read(integer)
inbox.mark
POST /api/v1/me/inbox/mark — command, token
Mark inbox rows read, or done (archived — off the list). Takes the ids inbox.list gave, or all for the whole inbox. Ids reach notify; local:<seq> ids advance the account's attention read pointer, the same forward-only pointer attention.read moves, so clearing the inbox clears the badge whichever half a row came from.
Input:
| field | type | rules |
|---|---|---|
ids |
array of string | max=500 |
all |
boolean | — |
as |
string | oneof=read\|done,max=8 |
Output fields:
ok(boolean)seq(integer)
inbox.recent
GET /api/v1/me/inbox/recent — query, token
What was recently addressed to the signed-in account, newest first, with their read pointer. This is the local attention window — the same events me.attention streams — so an inbox is a page a forge with no notify companion still renders correctly.
Input:
| field | type | rules |
|---|---|---|
limit |
integer | min=0,max=200 |
Output fields:
events(array of object {seq: integer, kind: string, repo: string, from: string, summary: string, link: string, at: string})read(integer)
landing.accept
POST /api/v1/r/{repo}/landing/{id}/accept — command, token
Land the branch on its target and settle the record. Writes a merge check-in, so it needs the check-in capability rather than the forum one. Merges exactly the branch tip its checks judged: if the branch moved since, it is refused with code branch_moved and fields expected (the checked tip) and current (the tip now), and nothing is written. The checks judge the branch merged into the target: if the target moved since, the verdict is stale (unless only this landing's own strata landed beneath it), and on a target with required checks the accept is refused with code stale_verdict and fields tested (the target tip the checks merged into), current (the target tip now) and behind (the target's check-ins since, capped at 50), and nothing is written; Re-run (ci.trigger with landing) tests the merge again, and a branch owner or an admin may override with a stated reason. On a target with no required checks it lands, and the merge message names the tip tested.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
message |
string | max=65536 |
to |
string | max=64 |
override |
string | max=2048 |
file |
string | max=400 |
side |
string | max=3 |
line |
integer | — |
at |
string | max=64 |
Output fields:
hash(string)state(string)
landing.auto
POST /api/v1/r/{repo}/landing/{id}/auto — command, token
Arm or disarm auto-land: an armed open landing accepts itself once every required check on its target is green. The deferred accept is the ordinary one, written as the arming account.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
on |
boolean | — |
Output fields:
autoBy(string)autoWaiting(string)autoWaitingAt(string)
landing.comment
POST /api/v1/r/{repo}/landing/{id}/comment — command, token
Add a remark to a landing's discussion. An ordinary forum reply.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
message |
string | max=65536 |
to |
string | max=64 |
override |
string | max=2048 |
file |
string | max=400 |
side |
string | max=3 |
line |
integer | — |
at |
string | max=64 |
Output fields:
hash(string)state(string)
landing.get
GET /api/v1/r/{repo}/landing/{id} — query, anonymous
One landing: the record, its discussion, and whether the caller may accept it. id is the landing's hash, a prefix of at least four, or its number: !12, or 12, which is read as a number first (/api/v1/r/{repo}/landing/12, and the page at /r/{repo}/landing/12). Numbers are the forge's, one sequence per repository apart from the tickets'.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=1,max=64 |
Output fields:
landing(object {id: string, number: integer, branch: string, target: string, title: string, author: string, opened: string, updated: string, state: string, settled_by: string, replies: integer, superseded_by: string, claimed_by: string, ci: string, stale: boolean, stack: string, position: integer, depth: integer, broken: string})checks(array of object {url: string, runId: string, checkin: string, pair: string, workflow: string, pipeline: string, status: string, started: string, completed: string, summary: string, branch: string, comment: string, user: string, rerunOf: string, rerunAs: string, reason: string, event: string, inheritedFrom: string})required(array of string)neverRan(array of string)checksHidden(boolean)premerge(boolean)testedAgainst(string)targetMovedBy(integer)stale(boolean)behind(integer)landsOn(string)unmet(array of string)mayOverride(boolean)autoBy(string)autoWaiting(string)autoWaitingAt(string)posts(array of object {transition: string, updated: string, retarget: string, titleIsBody: boolean, root: boolean, line: object {file: string, side: string, line: integer, at: string}})attachments(array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean})chatRefs(object {count: integer, seq: integer, at: string})canAccept(boolean)mergeable(string)conflicts(array of string)merge(object {checkin: string, date: string, user: string, comment: string})mergeCi(string)mergeCiRun(string)stack(object {base: string, top: string, ci: string, segments: array of object {id: string, number: integer, title: string, branch: string, target: string, state: string, position: integer, landable: boolean, ci: string, preflight: string, preflightConflicts: array of string}, broken: string, preflight: string, stopsAt: integer, landTo: string, landCount: integer})mayLand(boolean)mayLandReason(string)protected(boolean)owners(array of string)commits(array of object {hash: string, date: string, user: string, comment: string})
landing.land
POST /api/v1/r/{repo}/landing/{id}/land — command, token
Land a strata bottom-up, up to and including the named rung. Each rung is an ordinary accept; zit retargets the next; the first rung that will not merge stops the walk and is reported, with everything below it landed.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
message |
string | max=65536 |
Output fields:
landed(array of string)stopped(string)reason(string)remaining(integer)
landing.list
GET /api/v1/r/{repo}/landing — query, anonymous
Landings in this repository: branches proposed to land on a target, newest activity first. A landing is a forum thread, so it reads on the forum capability.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
state |
string | oneof=\|open\|accepted\|rejected\|all,max=16 |
limit |
integer | min=0,max=200,default=50 |
Output fields:
landings(array of object {id: string, number: integer, branch: string, target: string, title: string, author: string, opened: string, updated: string, state: string, settled_by: string, replies: integer, superseded_by: string, claimed_by: string, ci: string, stale: boolean, stack: string, position: integer, depth: integer, broken: string})open(integer)count(integer)
landing.open
POST /api/v1/r/{repo}/landing — command, token
Propose a branch to land on a target. Mints a forum thread — no new artifact kind — so a stock fossil clone reads it as an ordinary discussion.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
branch |
string | required,max=120 |
target |
string | max=120 |
message |
string | required,max=65536 |
Output fields:
id(string)number(integer)
landing.queue
GET /api/v1/me/landings — query, token
Every open landing, across all repositories, that the signed-in account may accept: it holds the check-in capability on the repository, and the target is not a protected branch it does not own. Mirrors that refuse local writes are left out, as are landings superseded by a newer one on the same branch. Newest activity first.
Output fields:
landings(array of object {repo: string, LandingSummary: object {id: string, number: integer, branch: string, target: string, title: string, author: string, opened: string, updated: string, state: string, settled_by: string, replies: integer, superseded_by: string, claimed_by: string, ci: string, stale: boolean, stack: string, position: integer, depth: integer, broken: string}})count(integer)
landing.reject
POST /api/v1/r/{repo}/landing/{id}/reject — command, token
Settle a landing without merging. A decision, so it takes the check-in capability like accept; a remark needs only the forum letter.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
message |
string | max=65536 |
to |
string | max=64 |
override |
string | max=2048 |
file |
string | max=400 |
side |
string | max=3 |
line |
integer | — |
at |
string | max=64 |
Output fields:
hash(string)state(string)
landing.retarget
POST /api/v1/r/{repo}/landing/{id}/retarget — command, token
Point an open record at a different target branch. The recovery for a stack whose base landed on its own.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,max=64 |
to |
string | required,max=120 |
Output fields:
id(string)target(string)
landing.update
POST /api/v1/r/{repo}/landing/{id}/update — command, token
Bring the target into the proposed branch, on the store, as a merge check-in (zit#152). Refused when the merge would not be clean; nothing is written. Writes to the PROPOSED branch, so it needs the check-in capability but not ownership of a protected target: membership of the target does not change.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=64 |
message |
string | max=65536 |
to |
string | max=64 |
override |
string | max=2048 |
file |
string | max=400 |
side |
string | max=3 |
line |
integer | — |
at |
string | max=64 |
Output fields:
hash(string)state(string)
markup.preview
POST /api/v1/r/{repo}/markup-preview — command, token
Render markdown exactly as saving it would — the composer's preview IS the production renderer, so what you see is what readers get.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
body |
string | max=1048576 |
Output fields:
html(string)
me.account
GET /api/v1/me — query, token
The signed-in account's own identity: its login, whether it is a site admin, and when it was created.
Output fields:
name(string)admin(boolean)created(string)repos(array of object {repo: string, role: string, exact: boolean, plus: array of string, minus: array of string})teams(array of string)passwordAuth(boolean)
me.attention
GET /api/v1/me/attention — stream, token
Everything addressed to the signed-in account, as Server-Sent Events. The body is text/event-stream, not JSON: each event's id: line is its stream sequence, and a client that reconnects with Last-Event-ID: <seq> resumes after it. A resume older than the window replays from the window's first event, preceded by an event: gap frame carrying {"from":<asked>,"first":<available>}. The first frame is event: live, sent once the tail is attached; nothing addressed to the account after it is missed. The sequence is the FORGE'S, not this account's: ids are assigned across every subscriber, so the ones you receive are a subset and gaps between them are ordinary rather than loss. Do not detect loss by looking for holes -- reconnecting with Last-Event-ID is the only correct way to find out what you missed.
Input:
| field | type | rules |
|---|---|---|
from |
integer | — |
Output fields:
seq(integer)kind(string)repo(string)from(string)summary(string)link(string)at(string)
me.email
GET /api/v1/me/email — query, token
The signed-in account's email state: the confirmed address (empty if none) and one waiting for confirmation (empty if none).
Output fields:
email(string)pending(string)
me.email.clear
POST /api/v1/me/email/clear — command, token
Clear the signed-in account's email — confirmed and pending both.
me.email.confirm
POST /api/v1/me/email/confirm — command, token
Promote the signed-in account's pending email to confirmed, spending the six-digit code. Refuses a code that is wrong, spent, or expired; five wrong tries against one code burn it outright, and either way the account's only way forward is a fresh me.email.set.
Input:
| field | type | rules |
|---|---|---|
code |
string | required,min=6,max=6 |
Output fields:
email(string)
me.email.set
POST /api/v1/me/email — command, token
Set or change the signed-in account's email. Nothing is confirmed yet: a six-digit code goes to the NEW address, and the address quarry actually mails stays whatever was confirmed before (if anything) until that code is typed back in.
Input:
| field | type | rules |
|---|---|---|
email |
string | required,max=200 |
Output fields:
pending(string)sent(boolean)
me.feed
GET /api/v1/me/feed — query, token
The signed-in dashboard: the caller's granted repositories ordered by activity, the merged activity feed across them, and the caller's own recorded work.
Input:
| field | type | rules |
|---|---|---|
kind |
string | max=4 |
offset |
integer | min=0,max=1000 |
find |
string | max=64 |
repo |
string | max=64 |
Output fields:
user(string)repos(array of object {name: string, description: string, lastActivity: string, open: integer, role: string})feed(array of object {repo: string, kind: string, ref: string, hash: string, date: string, user: string, comment: string, landingNumber: integer})kinds(array of object {kind: string, count: integer})total(integer)depth(integer)mine(array of object {repo: string, kind: string, ref: string, hash: string, date: string, user: string, comment: string, landingNumber: integer})
me.passwd
POST /api/v1/me/passwd — command, token
Change the signed-in account's own password. Verifies the current one through the same verifier a login uses, refuses a new password the account store would refuse, and on success ends every OTHER session of the account — the browser making the change stays signed in — and tells the account, at its confirmed address, that its password changed.
Input:
| field | type | rules |
|---|---|---|
current |
string | required,max=1024 |
new |
string | required,min=8,max=200 |
keep |
string | max=64 |
me.prefs
GET /api/v1/me/prefs — query, token
The signed-in account's notification preferences: RFC 0011 §6's family x channel matrix ({mention, reply, landing, activity} x {inapp, push, email}) as notify holds it, plus the forge-local switches quarry owns — in-page toasts, desktop notifications, the repositories this account muted, and the browsers push was enabled in. A channel with no stored preference is ON, which is notify's own rule.
Output fields:
families(map)channels(array of string)digests(map)toasts(boolean)notify_desktop(boolean)muted(array of string)unknown(array of string)devices(array of object {endpoint_hash: string, ua: string, at: string, pending_remote_delete: boolean})push(boolean)applied(object {local: boolean, notify: boolean})
me.prefs.set
POST /api/v1/me/prefs — command, token
Update the signed-in account's preferences. Each family carries the channels that stay ON (repeat the key), so the whole matrix is posted and an absent channel is off. The two forge-local switches are EXPLICIT — toasts and notify_desktop take "on" or "off", and leaving one out changes it not at all. mute and unmute are list operations for the same reason: naming neither changes no mute. A muted repository still reaches the account's own inbox, it just stops being sent. The answer's applied says which halves landed: the forge-local one always does, the notify one does not on a companion that did not answer its preferences route.
Input:
| field | type | rules |
|---|---|---|
mention |
array of string | max=8 |
reply |
array of string | max=8 |
landing |
array of string | max=8 |
activity |
array of string | max=8 |
families |
array of string | max=8 |
toasts |
string | oneof=on\|off,max=8 |
notify_desktop |
string | oneof=on\|off,max=8 |
mute |
array of string | max=200 |
unmute |
array of string | max=200 |
digest |
array of string | max=8 |
Output fields:
families(map)channels(array of string)digests(map)toasts(boolean)notify_desktop(boolean)muted(array of string)unknown(array of string)devices(array of object {endpoint_hash: string, ua: string, at: string, pending_remote_delete: boolean})push(boolean)applied(object {local: boolean, notify: boolean})
me.push.key
GET /api/v1/me/push/key — query, token
notify's VAPID application-server public key, base64url — what a browser passes to pushManager.subscribe. Empty when this forge has no notify companion, which a page reads as "push is not available here". quarry never holds the private half.
Output fields:
publicKey(string)
me.push.subscribe
POST /api/v1/me/push — command, token
Register a browser for push. The body is the browser's own PushSubscription JSON ({endpoint, keys:{p256dh, auth}}), forwarded to notify as subscriber credentials for the webpush provider. quarry stores no endpoint and no key — only a hash, a user-agent label and a timestamp, so the preferences page can list the devices somebody may want to turn off. Also sweeps this account's unconfirmed unsubscribes: a device notify would not drop earlier is retried here, which is the one moment quarry knows the companion is reachable.
Input:
| field | type | rules |
|---|---|---|
endpoint |
string | required,max=2048 |
keys |
object {p256dh: string, auth: string} | — |
ua |
string | max=256 |
expirationTime |
number | — |
Output fields:
ok(boolean)devices(array of object {endpoint_hash: string, ua: string, at: string, pending_remote_delete: boolean})pending_remote_delete(boolean)
me.push.test
POST /api/v1/me/push/test — command, token
Push one test notification to this account's own devices. The one question a person cannot answer from the forge alone is whether push reaches their screen: everything up to the push service answers success, and an operating system that is holding notifications back says nothing to anybody. This sends a real push through the ordinary path so the answer is the notification itself, or its absence.
Output fields:
sent(boolean)devices(integer)outcome(string)delivered(integer)of(integer)why(array of string)results(array of object {endpointSha256: string, device: string, host: string, outcome: string, status: integer, reason: string})
me.push.unsubscribe
POST /api/v1/me/push/delete — command, token
Stop pushing to one browser: drops the endpoint's credential in notify and forgets the device row. A credential notify does not hold (404) and one the push service already discarded (410 Gone) are both done, since that is the end state asked for. A notify that FAILED the delete has not dropped anything, so quarry keeps the device row marked pending_remote_delete, holding the endpoint only until the delete can be confirmed, and retries on the next me.push.subscribe. Forgetting the row on an unconfirmed delete is how a phone keeps buzzing with nothing left in the interface to stop it.
Input:
| field | type | rules |
|---|---|---|
endpoint |
string | required,max=2048 |
Output fields:
ok(boolean)devices(array of object {endpoint_hash: string, ua: string, at: string, pending_remote_delete: boolean})pending_remote_delete(boolean)
me.sync.token
POST /api/v1/me/sync-token — command, token
Mint a push credential for this account — the password zit push and fossil push prove themselves with — and return it the ONE time it is readable. Minting adds a token named for the day and leaves the others working, so each session can hold its own; revoke the ones you no longer use. The credential is deliberately not the account password: it is revocable on its own, it is useless for signing in, it expires in 90 days, and it covers every repository the account may check in to, including ones created after it was minted.
Output fields:
user(string)token(string)
me.sync.token.list
GET /api/v1/me/sync-tokens — query, token
Every credential of yours: each token's name, scopes, repositories, ceiling, when it was made, when it expires, when it was last used and whether it was revoked, expiring ones first — never a value. legacy names the repositories where you hold a hand-issued push credential, which the front still answers first.
Output fields:
tokens(array of object {id: string, name: string, scopes: array of string, repos: array of string, ceiling: string, created: string, expires: string, last_used: string, revoked: string, state: string})legacy(array of string)available(boolean)
me.sync.token.revoke
POST /api/v1/me/sync-token/revoke — command, token
Withdraw push credentials. With id, that one token of yours; with repo, the hand-issued credential you hold on that repository; with neither, every push token you hold. The rest keep working. Pushes stop at once, everywhere, because nothing caches a credential.
Input:
| field | type | rules |
|---|---|---|
id |
string | max=64 |
repo |
string | max=128 |
me.sync.token.state
GET /api/v1/me/sync-token — query, token
Whether this account holds a live push credential and when the newest was minted — never its value, which is readable only at the moment it is made.
Output fields:
held(boolean)created(string)available(boolean)
meta.docs
GET /api/v1/docs — query, anonymous
The API reference, generated from this registry and rendered.
Output fields:
markdown(string)html(string)
meta.version
GET /api/v1/version — query, anonymous
Report the quarry build version, and whether the forge is ready: after a restart, repositories register one by one and ready is false until the last one has, with starting naming those still to come.
Output fields:
version(string)node(string)zit(string)ready(boolean)starting(array of string)zitMin(string)zitTooOld(boolean)
mirror.chatbridge
POST /api/v1/r/{repo}/mirror/chatbridge — command, token
Bridge a repository's chat channel to a Fossil server's chat, or detach it. The password is stored and never returned. The repository's own admin's call.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
url |
string | max=512 |
user |
string | max=64 |
password |
string | max=256 |
detach |
boolean | — |
Output fields:
bridge(object {repo: string, url: string, user: string, lastMsgId: integer, at: string, lastErr: string})detached(boolean)
mirror.detach
POST /api/v1/mirrors/detach — command, token
Detach a store from its upstream: quarry becomes the source of truth and accepts writes; the upstream is kept as provenance (detachedFrom).
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
repo(string)
mirror.list
GET /api/v1/mirrors — query, token
List configured upstream mirrors and their recent history.
Output fields:
mirrors(array of object {repo: string, url: string, every: integer, enabled: boolean, lastAt: string, lastOk: string, lastErr: string, failures: integer, pulls: array of object {at: string, ms: integer, changed: boolean, err: string, uvRan: boolean, uvChanged: integer, uvErr: string}, divergedAt: string, divergedLast: string, acceptWrites: boolean, detachedFrom: string, state: string, bridged: boolean, chatBridge: object {repo: string, url: string, user: string, lastMsgId: integer, at: string, lastErr: string}, uv: boolean, uvUser: string, uvCredential: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string})count(integer)
mirror.run
POST /api/v1/mirrors/run — command, token
Pull from upstream now — one store, or every configured mirror when repo is empty. A site admin may pull a store they hold no role on, as an operator act with a receipt to the owner.
Input:
| field | type | rules |
|---|---|---|
repo |
string | max=64 |
Output fields:
results(array of object {repo: string, changed: boolean, err: string})failed(integer)changed(integer)
mirror.set
POST /api/v1/mirrors — command, token
Configure a store's upstream and pull interval.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
url |
string | required,max=2048 |
every |
integer | min=0,max=604800,default=3600 |
withUv |
boolean | — |
uv |
boolean | — |
uvUser |
string | max=64 |
uvPassword |
string | max=256 |
Output fields:
mirror(object {repo: string, url: string, every: integer, enabled: boolean, lastAt: string, lastOk: string, lastErr: string, failures: integer, pulls: array of object {at: string, ms: integer, changed: boolean, err: string, uvRan: boolean, uvChanged: integer, uvErr: string}, divergedAt: string, divergedLast: string, acceptWrites: boolean, detachedFrom: string, state: string, bridged: boolean, chatBridge: object {repo: string, url: string, user: string, lastMsgId: integer, at: string, lastErr: string}, uv: boolean, uvUser: string, uvCredential: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string})
mirror.uv
POST /api/v1/mirrors/uv — command, token
Turn a mirror's import of its upstream's unversioned files on or off, and set the upstream login the import signs with. Reading them needs o on the upstream, never y. The password is sealed and never returned. Refused on while the repository has no bucket of its own, or is private with a public bucket base.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
on |
boolean | — |
user |
string | max=64 |
password |
string | max=256 |
Output fields:
mirror(object {repo: string, url: string, every: integer, enabled: boolean, lastAt: string, lastOk: string, lastErr: string, failures: integer, pulls: array of object {at: string, ms: integer, changed: boolean, err: string, uvRan: boolean, uvChanged: integer, uvErr: string}, divergedAt: string, divergedLast: string, acceptWrites: boolean, detachedFrom: string, state: string, bridged: boolean, chatBridge: object {repo: string, url: string, user: string, lastMsgId: integer, at: string, lastErr: string}, uv: boolean, uvUser: string, uvCredential: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string})
mirror.writes
POST /api/v1/mirrors/writes — command, token
Allow or refuse local writes (push, tickets, wiki, forum) on a mirror. Refused by default: a write landing on a tracking store forks it by accident. It is the mirror's own repository and the owner's fork to reconcile, so this is the repository's business like the rest of mirror.* — the a letter, not site admin.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
accept |
boolean | — |
Output fields:
mirror(object {repo: string, url: string, every: integer, enabled: boolean, lastAt: string, lastOk: string, lastErr: string, failures: integer, pulls: array of object {at: string, ms: integer, changed: boolean, err: string, uvRan: boolean, uvChanged: integer, uvErr: string}, divergedAt: string, divergedLast: string, acceptWrites: boolean, detachedFrom: string, state: string, bridged: boolean, chatBridge: object {repo: string, url: string, user: string, lastMsgId: integer, at: string, lastErr: string}, uv: boolean, uvUser: string, uvCredential: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string})
ops.history
GET /api/v1/ops/history — query, admin
Renders read back from the durable analytics store, newest or slowest first.
Input:
| field | type | rules |
|---|---|---|
find |
string | max=200 |
repo |
string | max=64 |
sort |
string | oneof=recent\|slowest |
days |
integer | min=0,max=90,default=14 |
limit |
integer | min=0,max=500,default=100 |
Output fields:
rows(array of object {at: string, path: string, url: string, repo: string, elapsedMs: integer, opsMs: integer, zitMs: integer, status: integer, surface: string})find(string)repo(string)sort(string)days(integer)store(object {configured: boolean, path: string, err: string, kept: string, dropped: integer})
ops.route
GET /api/v1/ops/route — query, admin
One route's recent renders, split by repository, with the store calls each one made.
Input:
| field | type | rules |
|---|---|---|
path |
string | required,max=200 |
limit |
integer | min=0,max=100,default=20 |
days |
integer | min=0,max=90,default=14 |
Output fields:
path(string)renders(array of object {at: string, url: string, repo: string, elapsedMs: integer, opsMs: integer, zitMs: integer, status: integer, calls: array of object {verb: string, repo: string, durationMs: integer}, callMs: integer})byRepo(array of object {repo: string, count: integer, p50Ms: integer, maxMs: integer})trend(array of object {day: string, n: integer, p50Ms: integer, p95Ms: integer, maxMs: integer})trendDays(integer)
ops.status
GET /api/v1/ops — query, admin
Operational analytics: slowest routes, recent renders, store subprocess load. surface (web or api) narrows the recent renders; store.dropped counts observations the history store could not record, because its writer fell behind or a write failed.
Input:
| field | type | rules |
|---|---|---|
limit |
integer | min=0,max=200,default=25 |
find |
string | max=200 |
sort |
string | oneof=p95\|p50\|max\|n\|path |
surface |
string | oneof=web\|api |
Output fields:
node(string)phase(object {name: string, repos: integer, ready: integer, working: array of string, stalled: array of string, artifacts: integer, slowestRoundSeconds: number})paths(array of object {path: string, count: integer, p50Ms: integer, p95Ms: integer, maxMs: integer})renders(array of object {at: string, path: string, url: string, repo: string, elapsedMs: integer, opsMs: integer, zitMs: integer, status: integer, surface: string})verbs(array of object {verb: string, count: integer, totalMs: integer, maxMs: integer, repos: array of object {name: string, count: integer}, causes: array of object {name: string, count: integer}})liveSubprocess(integer)peakSubprocess(integer)subprocessSeries(array of integer)renderSeries(array of integer)seriesWindowSeconds(integer)lanes(array of object {name: string, bound: integer, running: integer, admitted: integer, refused: integer, waitedMs: integer, waiting: integer, queue: integer, items: array of object {verb: string, subject: string, ageMs: integer, running: boolean}})errorTotal(integer)eventStream(object {firstSeq: integer, lastSeq: integer, msgs: integer, bytes: integer, oldestAgeMs: integer, maxAgeSeconds: integer})budgetMs(integer)store(object {configured: boolean, path: string, err: string, kept: string, dropped: integer})
people.list
GET /api/v1/people — query, anonymous
List contributors across every repository the caller may read.
Input:
| field | type | rules |
|---|---|---|
repo |
string | max=64 |
q |
string | max=100 |
offset |
integer | min=0,max=1000000 |
limit |
integer | min=0,max=500,default=100 |
Output fields:
people(array of object {user: string, events: integer, first: string, last: string, repos: array of string, appearances: array of object {repo: string, events: integer, first: string, last: string}})count(integer)total(integer)
people.mention
GET /api/v1/r/{repo}/mention — query, token
Handles with any capability on this repository, for @ completion.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
q |
string | max=64 |
Output fields:
handles(array of object {handle: string})
people.suggest
GET /api/v1/r/{repo}/people/suggest — query, token
Forge accounts for the roster's add field, with the role each already holds on this repository. Gated on the settings letter, because it answers who exists.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
q |
string | max=64 |
Output fields:
accounts(array of object {handle: string, role: string})
ref.resolve
GET /api/v1/r/{repo}/ref — query, anonymous
Resolve one reference token to the facts a preview card shows.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
ref |
string | required,max=512 |
Output fields:
kind(string)id(string)repo(string)title(string)summary(string)state(string)href(string)expired(boolean)author(string)at(string)number(integer)
release.list
GET /api/v1/r/{repo}/releases — query, anonymous
A repository's releases (RFC 0020), or with name the one release in that folder: its unversioned files under releases/<name>/, one group per folder, newest first (by version when every folder name is one, else by the newest file), the first marked newest. A group's release-manifest.json is read in zit's schema version 1 (artifacts with target, size and sha256; the source check-in; the date; omitted targets; qualification notes); a group's SHA256SUMS is named; the CHANGELOG.md section whose heading names the release is rendered beside it.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | max=400 |
Output fields:
bound(boolean)note(string)releases(array of object {name: string, newest: boolean, mtime: integer, files: array of object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string}, sums: string, manifest: object {version: string, date: string, checkin: string, tag: string, artifacts: array of object {name: string, target: string, size: integer, sha256: string, qualification: string, file: string}, source: object {name: string, target: string, size: integer, sha256: string, qualification: string, file: string}, omitted: array of object {target: string, reason: string}, qualification: array of object {target: string, notes: array of string}}, manifestErr: string, notes: object {heading: string, html: string}})
release.summary
GET /api/v1/r/{repo}/releases/summary — query, anonymous
How many releases a repository has and which is newest, ordered as release.list orders them, from the catalog alone: no bucket read, no manifest. Zero when it has none or no bucket.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
count(integer)latest(string)mtime(integer)
repo.about
GET /api/v1/r/{repo}/about — query, anonymous
The repository's About aggregate: description, homepage and topics (.quarry/repo.json), the landing page's document tabs, the latest release tag, and the contributor count. One op so the panel costs one invoke and the JSON API sees the same panel.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
doc |
string | max=512 |
Output fields:
docPath(string)docLabel(string)docHtml(string)description(string)homepage(string)topics(array of string)docs(array of object {path: string, label: string})latestTag(string)latestTagTip(string)latestTagDate(string)tagCount(integer)contributors(integer)licenseName(string)announceRoot(string)announceTitle(string)announceDate(string)noteEvent(string)noteTitle(string)noteDate(string)noteUser(string)noteExcerpt(string)noteCount(integer)problem(string)
repo.activity
GET /api/v1/r/{repo}/activity — query, anonymous
Sample a repository's recent check-ins — who, when, and which files — with merge-carried touches marked inherited.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
branch |
string | max=64 |
user |
string | max=64 |
since |
string | max=10 |
until |
string | max=10 |
Output fields:
since(string)until(string)authors(array of object {name: string, checkins: integer, files: integer, hue: integer, chroma: integer, shade: integer})paths(array of string)rows(array of object {day: integer, hour: integer, author: integer, merge: integer, files: integer, hash: string, branch: string})touches(array of object {day: integer, path: integer, author: integer, inherited: integer})window(string)
repo.activity.days
GET /api/v1/r/{repo}/activity/days — query, anonymous
Fold a repository's whole history to one entry per day — who led it, how many merges, which directory, which releases.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
branch |
string | max=64 |
Output fields:
first(string)last(string)authors(array of object {name: string, checkins: integer, files: integer, hue: integer, chroma: integer, shade: integer})days(map)paths(array of string)releases(map)
repo.add
POST /api/v1/repos — command, token
Serve a repository: an existing store at path, a new empty one (create), or a clone of clone. Cloning also records a mirror unless noMirror. Any signed-in account may call this — the creator becomes the repository's admin in the same operation, which is where ownership starts.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
path |
string | max=4096 |
description |
string | max=300 |
private |
boolean | — |
create |
boolean | — |
clone |
string | max=2048 |
readme |
boolean | — |
license |
string | max=16 |
ignore |
boolean | — |
noMirror |
boolean | — |
mirrorEvery |
integer | min=0,max=604800 |
Output fields:
name(string)path(string)mirrored(boolean)
repo.claim
POST /api/v1/r/{repo}/claim — command, token
Take the deed to a repository nobody holds, or hand one you hold to another of its admins. A site admin may hand any repository to one of its admins on request, without a role there and with a receipt to the owner, but never to themselves. The recipient must already administer the repository: that is the nearest thing to consent available until an offer they can decline exists.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
account |
string | max=64 |
Output fields:
repo(string)owner(string)namespace(string)
repo.counts
GET /api/v1/r/{repo}/counts — query, anonymous
Count a repository's content by section — what the navigation shows.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
hasCI(boolean)hasReleases(boolean)ciDeclinedBy(string)trunkCI(string)trunkTip(string)indexBehind(boolean)checkins(integer)wikiPages(integer)tickets(integer)forumThreads(integer)openLandings(integer)technotes(integer)contributors(integer)tags(integer)
repo.delete
POST /api/v1/repos/delete — command, token
Destroy a repository: its store, its index and everything keyed to it. confirm must equal name. Only repositories quarry created can be deleted. Destroying belongs to the DEED, not the role: the a letter reaches everything inside a repository and stops at ending it, so an admin collaborator cannot delete a repository they do not own. A repository nobody has claimed is gated on the letter alone, since somebody must be able to retire it.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
confirm |
string | required,max=64 |
Output fields:
name(string)
repo.forgelog
GET /api/v1/r/{repo}/forgelog — query, token
What the forge's operator did to this repository while somebody else held it. Written whenever a site admin changes a repository they do not own.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=500 |
Output fields:
acts(array of object {at: string, actor: string, op: string, repo: string, owner: string})
repo.fossil.users
GET /api/v1/r/{repo}/fossil/users — query, token
List this repository's synced fossil users: each login, its capabilities, and the quarry account an admin linked it to.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required |
Output fields:
upstream(string)users(array of object {login: string, caps: string, decoded: array of string, info: string, linked: string, suggest: boolean, held: string, pinned: boolean, syncedAt: string})
repo.fossil.users.link
POST /api/v1/r/{repo}/fossil/users/link — command, token
Link a synced fossil login to a quarry account, which then holds that login's capabilities here; an empty account unlinks and leaves the account's capabilities as they are.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required |
login |
string | required,max=128 |
account |
string | max=64 |
Output fields:
users(integer)categories(integer)warning(string)
repo.fossil.users.sync
POST /api/v1/r/{repo}/fossil/users/sync — command, token
Fetch the user table from this repository's fossil server with an admin login there, apply its categories, and keep its users as a reference list. The login is used for this one request and not stored; no account is created and no password hash is kept.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required |
url |
string | max=2048 |
login |
string | required,max=128 |
password |
string | required,max=1024 |
Output fields:
users(integer)categories(integer)warning(string)
repo.get
GET /api/v1/r/{repo}/info — query, anonymous
Describe one repository: its settings and headline counts.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
name(string)visibility(string)contributors(integer)mirroredAt(string)mirror(object {tracking: boolean, url: string, every: integer, lastOk: string, lastAt: string, lastErr: string, failures: integer, acceptWrites: boolean, state: string, divergedAt: string, divergedLast: string, uv: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string})detachedFrom(string)deletable(boolean)forum(string)homepage(string)topics(string)chat(boolean)chatSetting(boolean)uv(boolean)fossilClone(boolean)lastActive(string)description(string)private(boolean)mirrorable(boolean)checkins(integer)tipDate(string)tipComment(string)indexing(boolean)starting(boolean)
repo.imports
GET /api/v1/repos/imports — query, admin
Repositories being copied from an upstream, and ones whose copy failed.
Output fields:
imports(array of object {name: string, url: string, state: string, error: string, started: string, bytes: integer})count(integer)
repo.list
GET /api/v1/repos — query, anonymous
List the served repositories with their tip check-in.
Input:
| field | type | rules |
|---|---|---|
q |
string | max=64 |
sort |
string | oneof=active\|name\|size |
limit |
integer | min=0,max=500 |
kind |
string | oneof=\|mirror\|native |
Output fields:
repos(array of object {name: string, visibility: string, contributors: integer, mirroredAt: string, mirror: object {tracking: boolean, url: string, every: integer, lastOk: string, lastAt: string, lastErr: string, failures: integer, acceptWrites: boolean, state: string, divergedAt: string, divergedLast: string, uv: boolean, uvAt: string, uvChanged: integer, uvErr: string, uvBlocked: string}, detachedFrom: string, deletable: boolean, forum: string, homepage: string, topics: string, chat: boolean, chatSetting: boolean, uv: boolean, fossilClone: boolean, lastActive: string, description: string, private: boolean, mirrorable: boolean, checkins: integer, tipDate: string, tipComment: string, indexing: boolean, starting: boolean})count(integer)total(integer)served(integer)
repo.log
GET /api/v1/r/{repo}/settings/log — query, token
Who changed this repository's settings, which field, and what it held before. One entry per field that actually changed, newest first; a save that changed nothing records none. This is the durable record — the event stream keeps 24 hours and says only that settings changed.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | — |
Output fields:
repo(string)count(integer)changes(array of object {at: string, actor: string, field: string, was: string, now: string})
repo.reindex
POST /api/v1/r/{repo}/reindex — command, token
Wipe this repository's derived index and replay it from the store, in process, while the forge serves. The sanctioned repair for any index that disagrees with its store. The repository's admins may run it, and so may a site admin who holds nothing there, as an operator act with a receipt to the owner: a replay reads every artifact in the store and competes with every other repository for the machine, which makes it forge performance as much as repository business.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
artifacts(integer)
repo.remove
POST /api/v1/repos/remove — command, token
Stop serving a repository. The store on disk is left untouched. Belongs to the DEED like repo.delete: un-serving somebody's repository is not something a grant to help maintain it carries.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
Output fields:
name(string)
repo.update
POST /api/v1/repos/update — command, token
Set a repository's settings: description, visibility, about fields, chat and the UV tab. A field the body does not mention is LEFT ALONE, so a partial update changes only what it names — send an explicit empty string or false to clear one. private=true blanks the nobody and anonymous category strings (RFC 0004 §Private); false returns them to the fleet default.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
description |
string | — |
private |
boolean | — |
homepage |
string | — |
topics |
string | — |
chat |
boolean | — |
uv |
boolean | — |
mirrorable |
boolean | — |
Output fields:
name(string)description(string)private(boolean)homepage(string)topics(string)mirrorable(boolean)
search.all
GET /api/v1/search — query, anonymous
Full-text search across every repository the caller may read.
Input:
| field | type | rules |
|---|---|---|
q |
string | required,max=200 |
kind |
string | oneof=checkin\|wiki\|ticket\|forum\|technote\|doc\|file |
repo |
string | max=64 |
limit |
integer | min=0,max=200,default=50 |
Output fields:
results(array of object {repo: string, SearchResult: object {kind: string, ref: string, label: string, snippet: string, date: string, user: string, branch: string, status: string, hash: string}})count(integer)searched(integer)by_repo(array of object {repo: string, count: integer})
search.query
GET /api/v1/r/{repo}/search — query, anonymous
Full-text search over a repository's check-ins, wiki, and tickets.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
q |
string | required,max=200 |
kind |
string | oneof=checkin\|wiki\|ticket\|forum\|technote\|doc\|file |
limit |
integer | min=0,max=200,default=50 |
Output fields:
results(array of object {kind: string, ref: string, label: string, snippet: string, date: string, user: string, branch: string, status: string, hash: string})count(integer)
session.end
POST /api/v1/sessions/end — command, token
Sign out one session by its id — any session for an admin, only your own otherwise — or every session of an account with account=.
Input:
| field | type | rules |
|---|---|---|
id |
string | max=64 |
account |
string | max=64 |
session.list
GET /api/v1/sessions — query, token
Who is signed in, from where, since when. An admin sees every account's sessions (or one account's with account=); everyone else sees their own.
Input:
| field | type | rules |
|---|---|---|
account |
string | max=64 |
Output fields:
sessions(array of object {id: string, account: string, created: string, expires: string, lastSeen: string, ip: string, agent: string})count(integer)
settings.list
GET /api/v1/r/{repo}/settings/fossil — query, token
List the fossil settings in force for a repository's store (RFC 0008 rung 8). On a store with no working tree the reply is supported=false with a note, until zit ships settings -R on bare stores (zit#153).
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
supported(boolean)note(string)settings(array of object {name: string, value: string, versionable: boolean})
settings.set
POST /api/v1/r/{repo}/settings/fossil — command, token
Set one repository-tier fossil setting on a store, then return the refreshed list (RFC 0008 rung 8). supported=false until zit#153.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=64 |
value |
string | max=4096 |
Output fields:
supported(boolean)note(string)settings(array of object {name: string, value: string, versionable: boolean})
tag.list
GET /api/v1/r/{repo}/tag — query, anonymous
List release tags — symbolic names that are not branches.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=500,default=100 |
Output fields:
tags(array of object {name: string, tip: string, date: string})count(integer)
team.chat
GET /api/v1/teams/{name}/chat — query, token
A team's room: its own lines and every line from the repositories under it, newest first, in one time order. Nothing is copied — each message already names the repository it was posted in. For the team's members; a non-member reads a repository's room at that repository.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
cursor |
string | max=4096 |
Output fields:
team(string)messages(array of object {seq: integer, kind: string, ref: integer, repo: string, user: string, text: string, origin: string, at: string, edited: boolean, deleted: boolean, html: string, refs: array of string, replies: integer, refUser: string, refLine: string})cursor(string)rooms(array of string)
team.chat.post
POST /api/v1/teams/{name}/chat — command, token
Say something in a team's room. With repo, the line goes to THAT repository's room instead — which is what replying to one of its lines does, so an answer lands where the people watching that repository will see it. Posting to a repository needs the chat capability on it; posting to the team needs membership.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
text |
string | required,max=4096 |
repo |
string | max=64 |
ref |
integer | — |
Output fields:
ok(boolean)seq(integer)
team.chat.render
POST /api/v1/teams/{name}/chat/render — command, token
Render one of a team room's lines exactly as the room shows it. The live stream's renderer: a line that arrives must be the same markup as the same line after a reload.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | max=64 |
text |
string | required,max=4096 |
Output fields:
html(string)
team.create
POST /api/v1/teams — command, token
Make a team. Any signed-in account may call this — the creator becomes its first owner in the same operation, which is where ownership starts, and is what stops a team existing that nobody can manage. The name comes from the SAME namespace as accounts and is refused if an account holds it: a grant row names a string, and the forge must never have to guess which kind it is.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
title |
string | max=120 |
descr |
string | max=300 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.delete
POST /api/v1/teams/{name}/delete — command, token
Delete a team, its membership and its repository bindings. The repositories themselves are untouched: a team manages access to them and does not own them, so deleting one must never be able to delete anybody's code.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.get
GET /api/v1/teams/{name} — query, anonymous
One team: its text, its repositories, and its members. Members who opted out of being listed are withheld from callers who cannot manage the team; a manager sees everyone, because they are the person who has to.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
Output fields:
team(object {name: string, title: string, descr: string, created: string, createdBy: string, members: integer, repos: integer, unlisted: integer})members(array of object {account: string, caps: string, owner: boolean, listed: boolean})repos(array of object {repo: string, team: string, addedBy: string, added: string})canManage(boolean)membersShown(integer)reposShown(integer)donatable(array of string)offered(array of object {repo: string, team: string, offeredBy: string, offered: string, expires: string})losing(map)mayRemove(array of string)
team.list
GET /api/v1/teams — query, anonymous
Every team on the forge, with its member and repository counts. Public: a quarry team sits at the level other forges call an organisation, and that level is publicly listable everywhere (RFC 0018). mine narrows it to the caller's own.
Input:
| field | type | rules |
|---|---|---|
mine |
boolean | — |
account |
string | max=64 |
limit |
integer | min=0,max=200,default=50 |
offset |
integer | min=0,max=1000000 |
Output fields:
teams(array of object {name: string, title: string, descr: string, created: string, createdBy: string, members: integer, repos: integer, unlisted: integer})total(integer)
team.member
POST /api/v1/teams/{name}/member — command, token
Put an account in a team, or change what it holds there. caps is a capability string granted on every repository under the team, ON TOP of the membership floor every member carries; empty means the floor alone. owner manages the TEAM and nothing about any repository under it. A change that would leave the team with no owner is refused; a site admin is told instead. Omitting owner or listed keeps what the member already had — it is not the same as sending false — so a change to one field cannot quietly undo another.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
account |
string | required,max=64 |
caps |
string | max=64 |
owner |
boolean | — |
listed |
boolean | — |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.people
GET /api/v1/teams/{name}/people — query, anonymous
Everyone who reaches any repository under this team, and the route each came by: a team role, the membership floor alone, or a string of their own on one of the repositories. A non-member holding a grant on one of them is included — the question is who reaches these repositories, and an answer that hid them would be answering a different one. Unlisted members are withheld from callers who cannot manage the team.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
offset |
integer | min=0,max=1000000 |
Output fields:
team(object {name: string, title: string, descr: string, created: string, createdBy: string, members: integer, repos: integer, unlisted: integer})people(array of object {account: string, member: boolean, owner: boolean, caps: string, direct: array of object {repo: string, caps: string}})total(integer)owners(integer)repos(integer)canManage(boolean)inRoom(boolean)
team.repo.add
POST /api/v1/teams/{name}/repo — command, token
Put a repository under a team, moving it if another team held it — a repository is under exactly one. Gated on a on THAT REPOSITORY, never on the team: otherwise making a team would be a way to take somebody else's repository and grant yourself admin of it.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.repo.offer.accept
POST /api/v1/teams/{name}/repo/accept — command, token
Accept a repository offered to this team, moving it under the team (RFC 0018). An owner's decision: the offer carries no access until this call, and a repository that has moved elsewhere since is refused rather than taken back.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.repo.offer.decline
POST /api/v1/teams/{name}/repo/decline — command, token
Decline a repository offered to this team, or withdraw an offer you made. Leaves no state: RFC 0018 keeps no register of who declined whom, and the offer may be made again.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.repo.remove
POST /api/v1/teams/{name}/repo/remove — command, token
Take a repository out of its team. EITHER side may do this — an admin of the repository or an owner of the team — because whoever can grant must be able to revoke, or a repository admin loses control of their own access list.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
repo |
string | required,max=64 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.text
POST /api/v1/teams/{name}/text — command, token
Set a team's display name and its public sentence. The handle never changes: it is what every grant row names.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
title |
string | max=120 |
descr |
string | max=300 |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
team.tickets
GET /api/v1/teams/{name}/tickets — query, token
One ticket list across a team's repositories, with the assignee columns beside it and nobody last. ticket.across scoped to the team rather than to the caller's granted set. Each repository applies its own visibility, so a private one is absent for somebody who cannot read it — membership of the team is not what decides.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
state |
string | oneof=open\|closed\|all |
repo |
string | max=64 |
assignee |
string | max=64 |
nobody |
boolean | — |
status |
string | max=64 |
title |
string | max=128 |
order |
string | max=32 |
Output fields:
team(string)columns(array of object {assignee: string, open: integer})tickets(array of object {repo: string})open(integer)closed(integer)repos(array of string)assignable(array of string)truncated(boolean)
team.unmember
POST /api/v1/teams/{name}/unmember — command, token
Take an account out of a team. Removing somebody who is not in it is not an error. NOTE that on a private repository this removes ALL of their access: there is nothing beneath the membership floor.
Input:
| field | type | rules |
|---|---|---|
name |
string | required,max=64 |
account |
string | required,max=64 |
caps |
string | max=64 |
owner |
boolean | — |
listed |
boolean | — |
Output fields:
name(string)ok(boolean)warning(string)offered(boolean)
technote.add
POST /api/v1/r/{repo}/technote — command, token
Record a technote; passing an existing event id records an edit.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
body |
string | required,max=1048576 |
mimetype |
string | max=64 |
comment |
string | max=200 |
event |
string | max=64 |
time |
string | max=32 |
Output fields:
event(string)hash(string)
technote.get
GET /api/v1/r/{repo}/technote/{event} — query, anonymous
Fetch one technote, rendered, by event-id prefix; rev selects one edit.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
event |
string | required,max=64 |
rev |
string | max=64 |
Output fields:
event(string)hash(string)date(string)edited(string)user(string)comment(string)body(string)html(string)revisions(integer)historical(boolean)attachments(array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean})
technote.history
GET /api/v1/r/{repo}/technote/{event}/history — query, anonymous
List every recorded edit of one technote, newest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
event |
string | required,max=64 |
Output fields:
event(string)comment(string)edits(array of object {hash: string, date: string, edited: string, user: string, comment: string})count(integer)
technote.list
GET /api/v1/r/{repo}/technote — query, anonymous
List a repository's technotes at their latest edit, newest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
notes(array of object {hash: string, event: string, date: string, user: string, mimetype: string, comment: string, body: string, excerpt: string, revisions: integer})count(integer)
ticket.across
GET /api/v1/me/tickets — query, token
Every tracker the caller can reach, as one listing: the same report axes as ticket.list asked of all granted repositories at once, with repository as one more browse axis.
Input:
| field | type | rules |
|---|---|---|
limit |
integer | min=0,max=200,default=50 |
state |
string | oneof=open\|closed\|all |
repo |
string | max=64 |
type |
string | max=64 |
severity |
string | max=64 |
priority |
string | max=64 |
subsystem |
string | max=64 |
status |
string | max=64 |
author |
string | max=64 |
title |
string | max=128 |
order |
string | max=32 |
awaiting |
boolean | — |
mine |
boolean | — |
Output fields:
tickets(array of object {repo: string})count(integer)open(integer)closed(integer)repos(array of object {value: string, open: integer})subsystems(array of object {value: string, open: integer})authors(array of object {value: string, open: integer})truncated(boolean)
ticket.config
GET /api/v1/r/{repo}/ticket-config — query, anonymous
The repository's ticket vocabulary: .quarry/tickets.json at the tip of trunk, merged over fossil's defaults. List order is sort order. With templates=true, also each type's description template from .quarry/ticket-templates/<Type>.md (default.md for no type).
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
templates |
boolean | — |
Output fields:
vocab(array of object {field: string, words: array of string})source(string)problem(string)fields(array of object {name: string, tickets: integer})templates(array of object {type: string, body: string})inputs(array of object {name: string, kind: string, required: boolean, words: array of string, default: string, maxLength: integer, note: string})
ticket.discard
POST /api/v1/r/{repo}/ticket/{id}/discard — command, token
Delete a local ticket outright — the ability the artifact log can never offer, and the whole answer to tracker spam.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=40 |
Output fields:
ok(boolean)
ticket.get
GET /api/v1/r/{repo}/ticket/{id} — query, anonymous
Fetch one ticket: folded state plus change history.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=1,max=40 |
Output fields:
ticket(object {id: string, created: string, updated: string, fields: array of object {name: string, value: string}, description: string, descriptionMimetype: string, comments: array of object {hash: string, date: string, login: string, username: string, mimetype: string, body: string}, changes: array of object {hash: string, date: string, user: string, fields: array of object {name: string, value: string}}})descriptionHtml(string)comments(array of object {date: string, login: string, username: string, html: string})attachments(array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean})mentions(array of object {root: string, title: string, kind: string, state: string, branch: string, target: string, updated: string, number: integer})chatRefs(object {count: integer, seq: integer, at: string})local(boolean)promotedTo(string)number(integer)
ticket.intake
POST /api/v1/r/{repo}/ticket/intake — command, anonymous
File a local ticket (docs/local-tickets.md): stored on this forge, promotable into the artifact log by an admin, deletable until then.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
title |
string | required,max=200 |
status |
string | max=40 |
comment |
string | max=65536 |
type |
string | max=40 |
severity |
string | max=40 |
priority |
string | max=40 |
subsystem |
string | max=80 |
foundin |
string | max=80 |
fields |
map | max=24 |
idempotencyKey |
string | max=128 |
Output fields:
id(string)url(string)number(integer)
ticket.list
GET /api/v1/r/{repo}/ticket — query, anonymous
List a repository's tickets with their folded current state.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
before |
string | max=96 |
state |
string | oneof=open\|closed\|all |
type |
string | max=64 |
severity |
string | max=64 |
priority |
string | max=64 |
subsystem |
string | max=64 |
status |
string | max=64 |
author |
string | max=64 |
title |
string | max=128 |
offset |
integer | min=0,max=100000 |
order |
string | max=32 |
origin |
string | oneof=local\|fossil |
Output fields:
tickets(array of object {id: string, title: string, status: string, updated: string, created: string, type: string, severity: string, priority: string, subsystem: string, resolution: string, author: string, lastUser: string, comments: integer, assignee: string, refs: integer, local: boolean, number: integer})count(integer)open(integer)closed(integer)subsystems(array of object {value: string, open: integer})authors(array of object {value: string, open: integer})next(string)
ticket.named
GET /api/v1/r/{repo}/ticket/named/{number} — query, anonymous
The tickets a number names in this repository: this forge's own, then any another forge recorded on a ticket pushed here. Several means a name two forges both gave; choose by id.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
number |
integer | required,min=1 |
Output fields:
tickets(array of object {id: string, title: string, status: string, url: string})
ticket.new
POST /api/v1/r/{repo}/ticket — command, token
Open a new ticket. Where it lands follows the repository's own setting (docs/local-tickets.md): the forge's tier by default, the artifact log for a repository that keeps its tracker there. fossil overrides for one ticket and needs the admin letter. attach lists deferred upload tickets to commit against it; refusals come back in attachErrors.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
title |
string | required,max=200 |
fossil |
boolean | — |
status |
string | max=40 |
comment |
string | max=65536 |
type |
string | max=40 |
severity |
string | max=40 |
priority |
string | max=40 |
subsystem |
string | max=80 |
foundin |
string | max=80 |
fields |
map | max=24 |
attach |
array of string | max=20 |
idempotencyKey |
string | max=128 |
Output fields:
id(string)hash(string)url(string)number(integer)attachErrors(array of string)
ticket.numbers.record
POST /api/v1/r/{repo}/ticket/numbers/record — command, token
Write each store ticket's number into the ticket itself (a number field, one ticket change each), so a clone knows the name without asking this forge. Tickets filed or promoted here carry it already; this backfills the ones that predate numbering. Reports only, unless write=true: every change is permanent history.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
write |
boolean | — |
Output fields:
missing(integer)written(integer)tickets(array of object {id: string, number: integer})
ticket.promote
POST /api/v1/r/{repo}/ticket/{id}/promote — command, token
Promote a local ticket into the artifact log: one ticket artifact with the folded state and provenance, one more collating any discussion, and an A card for each file the ticket held on the forge tier. The local id becomes a redirect.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=40 |
Output fields:
id(string)hash(string)attached(integer)attachErrors(array of string)
ticket.set
POST /api/v1/r/{repo}/ticket/{id} — command, token
Record a ticket change; empty fields are left untouched. attach lists deferred upload tickets to commit against the ticket (alone, with no field, they are attached and nothing else is written); refusals come back in attachErrors.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
id |
string | required,min=4,max=40 |
title |
string | max=200 |
status |
string | max=40 |
resolution |
string | max=40 |
comment |
string | max=65536 |
description |
string | max=65536 |
type |
string | max=40 |
severity |
string | max=40 |
priority |
string | max=40 |
subsystem |
string | max=80 |
foundin |
string | max=80 |
fields |
map | max=24 |
attach |
array of string | max=20 |
Output fields:
hash(string)local(boolean)attachErrors(array of string)
ticket.similar
GET /api/v1/r/{repo}/ticket/similar — query, anonymous
Tickets whose titles look like this one, open ones first: the likely duplicates to check before filing. Searches both tiers (the history and this forge's local tickets); a match shares at least two significant words, or one when the title has only one or two.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
title |
string | max=200 |
limit |
integer | min=0,max=10,default=3 |
Output fields:
tickets(array of object {id: string, title: string, status: string, url: string, local: boolean})
timeline.list
GET /api/v1/r/{repo}/timeline — query, anonymous
List a repository's events — check-ins, wiki edits, ticket changes — newest first, paged by before.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
limit |
integer | min=0,max=200,default=50 |
before |
string | max=96 |
branch |
string | max=64 |
tag |
string | max=64 |
user |
string | max=64 |
y |
string | oneof=all\|ci\|w\|t\|e\|f\|l |
Output fields:
entries(array of object {kind: string, hash: string, ref: string, date: string, user: string, comment: string, branch: string, isLeaf: boolean, isMerge: boolean, parents: array of string, change: string, ci: string, commentOrig: string, hidden: boolean, closed: boolean, landing: string, landingTitle: string, landingApprover: string, landingNumber: integer, excerpt: string})defaultBranch(string)count(integer)next(string)indexBehind(boolean)
tree.list
GET /api/v1/r/{repo}/tree/{version}/{path...} — query, anonymous
List one directory of a check-in, with the directory's README rendered.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
version |
string | max=100 |
path |
string | max=1024 |
Output fields:
version(string)ref(string)path(string)entries(array of object {name: string, path: string, dir: boolean, blob: string, files: integer, size: integer, exec: boolean, link: boolean, binary: boolean, lastHash: string, lastDate: string, lastComment: string})readme(string)readmeIn(string)
uv.get
GET /api/v1/r/{repo}/uv/{name...} — query, anonymous
Where one unversioned file's bytes are: a presigned GET on the repository's bucket good for an hour, or its public base for a public repository's passive file. The type comes from the name's extension by a fixed table; HTML, SVG, script and anything else that could run in a browser is served as a download, never in place.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=500 |
Output fields:
file(object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string})url(string)expires(string)type(string)inline(boolean)
uv.list
GET /api/v1/r/{repo}/uv — query, anonymous
A repository's unversioned files (RFC 0020): each name's newest version with its fossil mtime, hash, size and who wrote it, removals included as deleted rows, and fossil's uv-hash over them. A repository with no bucket of its own has none: bound is false and note says how to get one.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
files(array of object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string})bound(boolean)note(string)limit(integer)hash(string)
uv.put
POST /api/v1/r/{repo}/uv — command, token
Publish an unversioned file, replacing the name's current version when this one is newer by fossil's rule (the larger mtime, then the larger hash); the version already held, same mtime and hash, is nothing to do and answers applied false. Fossil's name rules apply, the size is bounded by the bucket's per-file limit, and the object is written to the repository's bucket before the catalog records it with who wrote it. Needs y, which admin does not imply; refused on a mirror that takes no local writes. Raises uv.changed.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=500 |
mtime |
integer | min=0 |
content |
string | — |
hash |
string | max=64 |
Output fields:
file(object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string})applied(boolean)
uv.rebuild
POST /api/v1/r/{repo}/uv/rebuild — command, token
Rebuild the repository's unversioned catalog from its bucket: every object under unversioned/ and its metadata, removals included. The catalog rebuilds itself when it was never built or another bucket is bound; this is for a bucket changed by hand behind the forge's back.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
files(array of object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string})bound(boolean)note(string)limit(integer)hash(string)
uv.rm
POST /api/v1/r/{repo}/uv/rm — command, token
Remove an unversioned file: a tombstone object in the bucket and a deleted row in the catalog, both carrying the removal's mtime, which is what syncs the removal. The removal must be newer than the file by fossil's rule, so a removal in the same second as the put it removes is refused (a tombstone loses a tie), as it is in fossil; pass a later mtime or wait a second. Needs y, which admin does not imply. Raises uv.changed.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=500 |
mtime |
integer | min=0 |
ifMtime |
integer | min=0 |
Output fields:
file(object {name: string, mtime: integer, hash: string, size: integer, deleted: boolean, who: string, at: string})applied(boolean)
wiki.get
GET /api/v1/r/{repo}/wiki/{name} — query, anonymous
Fetch one wiki page, rendered — latest, or the revision named by rev.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=100 |
rev |
string | max=64 |
Output fields:
hash(string)title(string)date(string)user(string)mimetype(string)source(string)html(string)outline(array of object {level: integer, id: string, text: string})deleted(boolean)attachments(array of object {hash: string, name: string, target: string, src: string, date: string, user: string, mimetype: string, external: boolean, sha256: string, size: integer, held: boolean})
wiki.history
GET /api/v1/r/{repo}/wiki/{name}/history — query, anonymous
List every recorded edit of one wiki page, newest first.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=100 |
Output fields:
title(string)edits(array of object {hash: string, date: string, user: string})count(integer)
wiki.list
GET /api/v1/r/{repo}/wiki — query, anonymous
List a repository's wiki pages with their latest edit.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
Output fields:
pages(array of object {title: string, date: string, user: string, revisions: integer})count(integer)orphans(array of string)wanted(array of object {title: string, links: integer, from: array of string})home(string)
wiki.save
POST /api/v1/r/{repo}/wiki/{name} — command, token
Record a wiki page edit (creates the page on first save). attach lists deferred upload tickets to commit against the page; refusals come back in attachErrors.
Input:
| field | type | rules |
|---|---|---|
repo |
string | required,max=64 |
name |
string | required,max=100 |
body |
string | required,max=1048576 |
mimetype |
string | max=64 |
attach |
array of string | max=20 |
Output fields:
hash(string)attachErrors(array of string)