Reference

API reference

The platform REST API (/api/v1) and the Weave server API (/api/weave/{repoId}/…): auth, requests, responses, error shapes and codes.

View as Markdown

Weave has two HTTP APIs:

APIBase URLFor
Platformhttps://weave.directory/api/v1Organizations, repositories, tokens, agents, service accounts, webhooks, repository lifecycle
Weave serverhttps://api.weave.directory/api/weave/{repoId}Workstreams, stitches, claims, ownership, suggestions, change requests, resolution points, messages, search, history

All requests and responses are JSON with Authorization: Bearer <token>. {repoId} is the platform's repository id (returned when you create a repository), never org/name. Workstream names in a path are one URL segment: encode / as %2F.

Authentication#

TokenPlatform /api/v1Weave server
Personal access token (weave_…)YesYes
Service token (wvs_…)Yes, except POST /api/v1/tokens and service-account managementYes
Agent token (wva_…)No (403 forbidden), except worker agents on workstream remove / restoreYes

Check a token with GET https://weave.directory/api/whoami (see Security & access). The server's GET /api/weave/version (no token) answers {"server": "0.4.0"}.

Errors#

Platform: { "error": { "code": "…", "message": "…" } }

StatusCodes
400bad_request
401unauthorized
403forbidden, insufficient_scope, route_not_allowed, two_factor_required, email_unverified, expired (export download link)
404not_found
409conflict, agent_disabled, service_account_disabled, webhook_archived, unpromoted_stitches
429rate_limited (with Retry-After)
500internal
501unsupported (repository lifecycle, import), not_supported (workstream remove / restore) — the Weave server is too old
502weave_unavailable, bad_gateway (attestation keys)
503unavailable (attestation keys)

Weave server: { "type": "error", "code": "…", "message": "…", "retryable"?: true, "error_id"?: "…" }

StatusCodeNotes
400bad_request
401unauthenticated
403forbiddenNo access, read-only caller, not the owner, not your action, confined route
403path_out_of_scopeWith paths and pathScope
404not_foundAlso every route of a deleted repository
409conflictArchived workstream or repository, name taken, raced review; also revert_conflict, attestations_required
422secret_detectedWith findings: [{path, line, kind}]
429rate_limitedWith Retry-After
500internalWith error_id
503unavailableretryable: true, with error_id: resend (see Retrying)

Successful stitch requests answer 200 even when the outcome is suggested or rejected: inspect outcome. Every write that appends to the operation log carries entry_id and entry_ids (and the X-Weave-Entry-Id header); POST /undo {"entry": "<id>"} takes it.

Platform: organizations#

POST /api/v1/orgs#

Create an organization; the caller becomes its owner. Scope org:admin; needs a verified email.

json
{ "name": "Acme", "slug": "acme" }

201:

json
{ "id": "clx…", "slug": "acme", "name": "Acme" }

The slug is lower-cased and must match ^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$. A taken slug: 409 conflict.

Platform: repositories#

POST /api/v1/orgs/{slug}/repos#

Create a repository. Scope org:admin; the caller must be a member of the organization; a token limited to repositoryIds can't create repositories.

json
{ "name": "shop", "description": "Our web shop", "private": true, "workstreams": ["preprod"], "template": "empty" }
FieldDefaultNotes
namerequired^[A-Za-z0-9._-]{1,100}$, not ., .. or .weave
privatetrue
workstreams["dev"]Each is created with main as its parent. An invalid name fails the request (400); a failure creating one is reported in warnings.
template"empty"

201:

json
{ "id": "cm3k…", "name": "shop", "org": "acme", "slug": "acme/shop", "private": true,
  "workstreams": ["main", "preprod"], "warnings": [] }

409 conflict: the name exists, or its storage prefix is still bound. 502 weave_unavailable: the Weave server didn't answer.

POST /api/v1/orgs/{slug}/repos/import#

Create a repository and import a git branch or a zip into main. Scope org:admin.

json
{ "name": "app", "private": true, "workstreams": ["dev"],
  "source": { "type": "git", "url": "https://github.com/acme/app.git", "ref": "main", "history": true, "maxCommits": 1000, "token": "…" } }

source.type: "zip" takes url and stripRoot. 202 with the repository fields plus import (a job; poll GET /api/weave/{id}/imports/{importId}). workstreams are created once the import is done. If the import can't start, the repository is deleted again.

DELETE /api/v1/repos/{repoId}#

Scope repo:admin, repository admin. Purges the repository on the Weave server (every row, object and search entry), then removes it from the platform. Permanent: ids are never reused and later requests answer not_found. If the purge fails, the repository is kept.

200: { "id": "cm3k…", "deleted": true }

Copy, transfer, archive, export#

Method + pathScopeBodyResponse
POST /api/v1/repos/{id}/copyorg:admin (not repository-limited); read on the source, member of the target org{target_org, name, workstreams?, at_head?, description?, private?}201 {id, name, org, slug, private, source, workstreams: [{slug, parent, head, entries}], promotions, objects, at_head}
POST /api/v1/repos/{id}/transferrepo:admin and org:admin; admin of both orgs{target_org, name?}{id, name, org, slug, from, revoked_agent_tokens}
POST /api/v1/repos/{id}/archiverepo:admin—{id, archived: true, archived_at, archived_by}
POST /api/v1/repos/{id}/unarchiverepo:admin—{id, archived: false, …}
GET /api/v1/repos/{id}/exportrepo:admin—{id, url, url_expires_at, deleted_at, bytes, sha256, format: "weave-export", version: 1, created_at}
  • Copy keeps workstream history (same changeset ids, authors, times); the parents of named workstreams always come too. at_head (64 hex) needs exactly one workstream. Not copied: the operation log (the copy starts a new one), claims, pending suggestions, open change requests, webhooks.
  • Transfer keeps the repository id. Agent tokens of the old organization that named it are revoked.
  • Archive makes the repository read-only on every transport (409 "this repository is archived (read-only); unarchive it to make changes"); reads keep working. Both calls are idempotent.
  • Export writes a new gzip tar each call and answers a 15-minute download link; the file is deleted after 7 days.

Workstreams: remove and restore#

Method + pathScopeResponse
DELETE /api/v1/repos/{id}/workstreams/{ws}?force=true|falserepo:write (PAT, service or worker agent token){workstream, archived: true, archivedAt, archivedBy}
POST /api/v1/repos/{id}/workstreams/{ws}/restorerepo:write{workstream, archived: false, archivedAt: null, archivedBy: null}

The Weave server then allows only the workstream's creator or a repository admin. main → 400; child workstreams → 409 conflict; weaved stitches not promoted → 409 unpromoted_stitches unless force=true; verifier agents → 403. See Workstreams.

Platform: tokens#

Method + pathScopeBodyResponse
GET /api/v1/tokensany—{tokens: [{id, name, scopes, repositoryIds, expiresAt, lastUsed, createdAt}]}
POST /api/v1/tokenstokens:write (person only){name, scopes, repositoryIds?, expiresInDays?}201 the token fields plus token (shown once)
DELETE /api/v1/tokens/{id}tokens:write—{id, revoked: true}
GET /api/v1/tokens/{id}/usage?since=&limit=any (your own token)—{uses: [{at, kind, tokenId, principalId, method, path, traceId}]}

scopes is required (1–10 known scopes); expiresInDays 1–365, default 30; repositoryIds ≤ 200. A repository-limited caller's new token inherits its repositories when none are given.

bash
curl -s https://weave.directory/api/v1/tokens -H "Authorization: Bearer $WEAVE_PAT" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci", "scopes": ["repo:read", "repo:write"], "repositoryIds": ["cm3k…"], "expiresInDays": 90}'

Platform: agents and agent tokens#

Method + pathScopeBodyResponse
GET /api/v1/orgs/{slug}/agentsorg:admin—{agents: [Agent]}
POST /api/v1/orgs/{slug}/agentsorg:admin{name, kind?: "worker"|"verifier", pathScope?}201 Agent
GET /api/v1/orgs/{slug}/agents/{id}org:admin—Agent
DELETE /api/v1/orgs/{slug}/agents/{id}org:admin—Agent (disabled, every token revoked; idempotent)
POST /api/v1/agents/{id}/tokenstokens:write{ttlMinutes?, repositoryIds?, scopes?}201 {token: "wva_…", id, agentId, expiresAt, scopes, repositoryIds}

Agent = {id, userId: "agent:<id>", name, kind, pathScope | null, disabled, disabledAt, createdAt, activeTokens}. The caller must be an owner or admin of the organization (with 2FA). Details: For AI agents.

Platform: service accounts#

Method + pathScopeBodyResponse
GET /api/v1/orgs/{slug}/service-accountsorg:admin—{serviceAccounts: [ServiceAccount]}
POST /api/v1/orgs/{slug}/service-accountsorg:admin{name, role?: "member"|"admin"|"owner"} (default admin; owner only by an owner)201 ServiceAccount
DELETE /api/v1/orgs/{slug}/service-accounts/{id}org:admin—ServiceAccount (disabled: true)
GET /api/v1/orgs/{slug}/service-accounts/{id}/tokenstokens:write—{tokens: [ServiceToken]}
POST /api/v1/orgs/{slug}/service-accounts/{id}/tokenstokens:write{name, scopes, repositoryIds?, allowedRoutes?, expiresInDays?} (default 90, max 365)201 ServiceToken + {token: "wvs_…", serviceAccountId}
DELETE /api/v1/orgs/{slug}/service-accounts/{id}/tokens/{tokenId}tokens:write—{revoked: true}
GET /api/v1/orgs/{slug}/service-accounts/{id}/usage?tokenId=&since=&limit=org:admin—{uses: [TokenUse]}

ServiceAccount = {id, name, organizationId, role, disabled, disabledAt, createdById, createdAt, activeTokens}. ServiceToken = {id, name, scopes, repositoryIds, allowedRoutes, expiresAt, lastUsed, createdAt}. All need a person's PAT.

Platform: webhooks#

Repository admins (scope repo:admin; people and service accounts).

Method + pathBodyResponse
GET /api/v1/repos/{id}/webhooks[?archived=true]—{webhooks: [Webhook]}
POST /api/v1/repos/{id}/webhooks{url (https), secret (≥ 16 characters), events? (default ["workstream.weaved"]), active? (true), name?}201 Webhook
GET /api/v1/repos/{id}/webhooks/{hookId}—Webhook
PATCH /api/v1/repos/{id}/webhooks/{hookId}any of {url, events, secret, active, name}Webhook (409 webhook_archived when archived)
DELETE /api/v1/repos/{id}/webhooks/{hookId}—{id, deleted: true}
POST …/webhooks/{hookId}/archive / unarchive—Webhook
POST …/webhooks/{hookId}/transfer{targetRepositoryId}Webhook on the target

Webhook = {id, repositoryId, name, url, events, active, archived, archivedAt, secretSet, createdAt, updatedAt}. The secret is never returned. Events and signing: Webhooks & events.

Platform: attestation keys#

GET|POST /api/v1/orgs/{slug}/attestation-keys and DELETE /api/v1/orgs/{slug}/attestation-keys/{keyId} (scope org:admin) manage organization-wide Ed25519 keys. POST body: {publicKey, label?, types?}. See Attestations.

Weave server: workstreams#

Method + pathAccessBody / queryResponse
GET /workstreams[?archived=true]read—{type: "workstreams", workstreams: [Workstream]}
POST /workstreamswrite{slug, parent?, settings?: {ownership?, inherit?}}200 {type: "workstream", workstream}
GET /workstreams/{ws}read—{type: "workstream", workstream}
GET /workstreams/{ws}/headread—{type: "head", workstream, head, seq}
PATCH /workstreams/{ws}/settingsadmin, or the creator{ownership?, inherit?, force?}{type: "workstream", workstream}
DELETE /workstreams/{ws}?force=creator or admin—{type: "workstream_archived", workstream, unpromoted, closed_change_requests, closed_suggestions, released_claims}
POST /workstreams/{ws}/restorecreator or admin—{type: "workstream", workstream}
POST /workstreams/{from}/promotewrite{to, changesets: [id], with_dependencies?}{type: "promote", promoted: [{source_changeset, outcome}], required, change_requests}
POST /workstreams/{ws}/refreshwrite—{type: "refreshed", head, inherited, change_requests}
POST /workstreams/{ws}/seedwrite here + read on the template{fromRepo, state?} or {fromRepo, fromWorkstream?}{type: "seeded", from_repo, state, files, outcome, skipped?}
GET /workstreams/{ws}/changes?since=&limit=read—{type: "changes", workstream, head, changes: [{entry, changeset}], more}
GET /workstreams/{ws}/history?before=&limit=&kind=&agent=&author=&source=&symbol=read—{type: "history", workstream, items, more}
GET /workstreams/{ws}/symbol-history?symbol=&limit=read—{type: "symbol_history", …}
GET /workstreams/{ws}/blame?path=&offset=&limit=read—{type: "blame", …}
POST /workstreams/{ws}/revertwrite{changeset, why} or {changesets: [id], why}{type: "reverted", outcome, change_requests, …}
GET /workstream-summariesread—Workstreams with their last stitch, pending suggestions, open change requests, behind-parent count

Workstream = {slug, parent, parent_head, synced_parent_head, head, settings: {ownership, inherit}, created_by, created_at, archived_at, archived_by}. main exists implicitly.

Weave server: stitches#

POST /workstreams/{ws}/stitches#

Submit one sealed changeset (the CLI builds and hashes changesets with the shared Rust crate; the server rejects an id that doesn't match its content).

json
{ "changeset": { "id": "…", "change_id": "…", "base_head": "…", "author": "<your user id>", "agent": null,
                 "intent": "Add cart total", "why": "…", "created_at": "…", "ops": [ … ] },
  "blobs": { "<hash>": "<base64>" }, "allowRevert": false, "allowSecret": [] }

200 {"type": "stitch", "outcome": StitchOutcome}:

json
{ "outcome": "weaved", "head": "…", "changeset": "…", "carried_forward": false, "duplicate": false,
  "suggestions": [], "change_requests": [], "notices": [] }
{ "outcome": "suggested", "head": "<unchanged>", "suggestions": [ … ] }
{ "outcome": "rejected", "reason": "…", "symbols": [ … ] }

A suggestion routed because of a clash carries clash: {code: "lost_update" | "reverts_newer_work", reason, authors, stitches}. Resending the same changeset never weaves twice (duplicate: true).

POST /workstreams/{ws}/stitches/batch#

Several changesets (1–64) woven in order under one workstream lock with one head update:

json
{ "stitches": [ { "changeset": { … }, "blobs": {}, "allowRevert": false } ], "allowSecret": [] }

200 {"type": "stitches_submitted", "workstream", "head", "outcomes": [{"submitted", "outcome"}]}. One rejected stitch doesn't fail the others, except later ones that depend on it (code: "depends_on_rejected"). Every changeset's author must be the caller (400 otherwise).

POST /stitch (whole files)#

The simplest way to stitch over HTTP: send whole files and the server diffs them into symbol operations.

bash
curl -s https://api.weave.directory/api/weave/$REPO_ID/stitch \
  -H "Authorization: Bearer $WEAVE_TOKEN" -H "Content-Type: application/json" \
  -d '{"workstream": "dev", "intent": "Fix login copy", "why": "support ticket 41",
       "files": [{"path": "src/app/page.tsx", "content": "…"}, {"path": "old.md", "content": null}],
       "moves": [{"from": "img/a.png", "to": "assets/a.png"}]}'

content: null deletes a file; moves become file moves applied before files. The response is the bare StitchOutcome ("no changes" when nothing differs). Paths under .weave/ are refused, except .weave/decisions/. Send "allowRevert": true when taking back someone's newer change is intended.

Method + pathDoes
GET /changesets/{id}A changeset
GET /states/{head}, GET /states/{head}/page?after=&max_bytes=, GET /states/{head}/file?path=A state, a page of it, one file's raw bytes
POST /blobs, GET /blobs/{hash}Upload / download one blob (raw bytes)
GET /archive?ref=<head|workstream>&format=tar.gz[&state=1]Every file of a head in one streamed tar.gz (what weave clone uses); header X-Weave-Head
GET /diff?from=&to=&level=file|symbol&path=Diff two heads by file or symbol
POST /restore{workstream, head, paths?, symbols?, intent?, why?}: a new stitch setting paths or symbols back to head
POST /undo{entry?}: undo your own most recent action, or that log entry
POST /rpcAny protocol request as JSON ({"type": "…", …})

Weave server: claims and ownership#

Method + pathBody / queryResponse
POST /claims{targets, workstream?, feature?, lease_secs?, partial?}{type: "claimed", targets, lease_secs}, {type: "claim_conflict", conflicts} or, with partial: true, {type: "partial_claim", claimed, conflicts: [{target, owner, held}], missing, lease_secs}
POST /claims/release{targets, workstream?} or {all: true}{type: "released", count}
POST /claims/renew{lease_secs?}{type: "renewed", renewed, lease_secs}
GET /claims?workstream=—{type: "claims", items: [{target, owner, scope, feature, expires_in_secs, ephemeral}]}
GET /ownership?path=&symbol=&workstream=—{type: "ownership", workstream, path, claims, symbols: [{symbol, owner, via}]}
GET /ownership?prefix=&workstream=—{type: "repo_ownership", prefix, claims}: every live claim under a file or folder
GET /impact?symbol=&depth=&ws=—{type: "impact", callers, callees, truncated} (resolved by name, depth 1–5)

Targets are path (whole file) or path::qualified_path, literal (no globs on the wire). To have a write weave directly, write as symbols[i].owner, or as anyone when owner is null.

bash
curl -s https://api.weave.directory/api/weave/$REPO_ID/claims \
  -H "Authorization: Bearer $WEAVE_TOKEN" -H "Content-Type: application/json" \
  -d '{"targets": ["app/[id]/page.tsx", "package.json::scripts.dev"], "feature": "part:p3", "partial": true}'

Weave server: suggestions and change requests#

Method + pathBodyNotes
GET /suggestions?workstream=&owner=&author=&state=—state: pending, accepted, rejected, stale
GET /suggestions/{id}—
POST /suggestions/{id}/accept{} or {as_admin: true, why}Owner; a repository admin with as_admin and a why (not agents)
POST /suggestions/{id}/reject{reason} (+ optional as_admin, why)A clash suggestion opens resolution points
POST /suggestions/{id}/decline{reason?, as_admin?, why?}reject with the reason optional
GET /change-requests?workstream=&state=open|resolved&assignee=—
GET /change-requests/{id}—
POST /change-requests/{id}/resolve{"with": "choice", "choice": "this_side"|"other_side", "intent"?, "why"?} or {"with": "stitch", "changeset", "blobs"}

Weave server: resolution points#

Method + pathBody / queryResponse
GET /resolution-points?workstream=&state=open|resolved&involving=&suggestion={type: "resolution_points", items} (oldest first)
GET /resolution-points/{id}—{type: "resolution_point", item}
POST /resolution-points/{id}/resolve{choice: "head"|"copy"|"edited", text?, why?}{type: "resolution_point", item, outcome?}

A point is {id, tag, workstream, symbol, copy?, head_side, copy_side, owner, author, callers, other_callers, suggestion, reason, fallback?, state, created_at, resolved_at?, resolver?, why?}. The owner, the current claim holder or a repository admin choose any; the author only head. See Resolution points.

Weave server: messages, notices and events#

Method + pathBody / queryResponse
POST /messages{to: ["@all"|"@ws:<ws>"|"@owner:<file>[::<symbol>]"|"<user id>"|"agent:<id>"], body, about?: {workstream?, path?, symbol?, changeset?, change_request?}, reply_to?}201 Message
GET /messages?unread=&sent=&thread=&after=&limit= (≤ 200, default 50){messages, unread}
POST /messages/read{ids?} or {all: true}{count}
GET /notices—Your dependency notices
GET /events?since=&limit=—Durable events in seq order (Webhooks & events)
GET /log?actor=&agent=&workstream=&symbol=&before=&limit=, GET /log/verify—The operation log, newest first; chain verification

Message = {id, from, to, recipients?, body, about?, reply_to?, thread, created_at, read}. Body ≤ 16 KiB, 1–50 addresses. Read access is enough.

GET /search?q=&workstream=&limit=&paths=a,b&kinds=function,method or POST /search {query, workstream?, limit?, paths?, kinds?} → {type: "code_search", workstream, head, stale?, indexing?, hits}. GET /search/index shows what is indexed; POST /search/reindex {workstream?, full?, wait?} (admin) backfills. See Search.

Decisions#

Method + pathBody / query
GET /decisions?workstream=&head=&symbol=&q=&status=&limit=&offset=
GET /decisions/{id}?workstream=&head=; a unique id prefix works
POST /decisions{workstream, title, why, on?: ["file::symbol"], alternatives?, constraints?, intent?}
POST /decisions/{id}/supersedeas POST /decisions
POST /decisions/{id}/retire{workstream, reason?, intent?}
POST /decisions/review{workstream?, head?, ids?, apply?, accept?, intent?} (read-only unless apply: true)
GET /states/{head}/symbol-shape?symbol=&kind=Hashes and a ready-made anchor for a symbol

Writes are stitches: the response is {type: "stitch", outcome, decision}.

Releases: tags, protection, attestations#

Method + pathAccess
GET /tags[?head=], POST /tags {name, head? | workstream?, message?}, GET /tags/{name}, DELETE /tags/{name}?why=read / write / read / admin
GET /protection, GET|PUT|DELETE /workstreams/{ws}/protection {requireAttestations}, GET /workstreams/{ws}/protection/check?head=read / admin
GET|POST /attestation-keys, DELETE /attestation-keys/{keyId}read / admin
GET|POST /heads/{head}/attestations {type, payload, signature, keyId}read / write
GET /api/weave/attestation-keys/{keyId} (no repository, no token)public

See Decisions & history.

Audit and purges#

Method + pathAccess
GET /audit/verification[?deep=true]read (deep: admin)
GET /audit/export?from=&to=&after=&limit=&format=json|ndjsonadmin
POST /purges {blob | text, reason}, GET /purges[?state=], GET /purges/{id}, POST /purges/{id}/approve, POST /purges/{id}/cancel, GET /purges/{id}/manifestadmin (two different admins to request and approve)

See Security & access.

Git import and mirrors#

Repository admins only. A mirror pushes one workstream to one branch of a git remote over HTTPS; every history entry becomes one git commit with the intent and why as the message. An import fills an empty workstream from a git branch (one stitch per commit, first-parent chain) or a zip.

Method + pathBody
POST /git/mirrors{url (https), workstream?: "main", branch?, continuous?: true, username?, token?, force?: false}
GET /git/mirrors, GET|PATCH|DELETE /git/mirrors/{id}, POST /git/mirrors/{id}/pushPATCH: {enabled?, continuous?, force?, token?, username?}
POST /imports{source: "git", url, ref?, history?, maxCommits?, username?, token?, workstream?, createWorkstreams?} or {source: "zip", url, stripRoot?, workstream?}, or a raw zip body (Content-Type: application/zip, ≤ 30 MB)
GET /imports, GET /imports/{id}Job status: queued → running → done | failed

continuous: false is a one-off export. Mirror tokens are stored encrypted and never returned; import tokens are used for the fetch only.

Webhook deliveries#

GET /webhooks/deliveries?status=&webhook=&event=&before=&limit= and POST /webhooks/deliveries/{id}/redeliver (repository admin). See Webhooks & events.