Reference
API reference
The platform REST API (/api/v1) and the Weave server API (/api/weave/{repoId}/…): auth, requests, responses, error shapes and codes.
Weave has two HTTP APIs:
| API | Base URL | For |
|---|---|---|
| Platform | https://weave.directory/api/v1 | Organizations, repositories, tokens, agents, service accounts, webhooks, repository lifecycle |
| Weave server | https://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#
| Token | Platform /api/v1 | Weave server |
|---|---|---|
Personal access token (weave_…) | Yes | Yes |
Service token (wvs_…) | Yes, except POST /api/v1/tokens and service-account management | Yes |
Agent token (wva_…) | No (403 forbidden), except worker agents on workstream remove / restore | Yes |
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": "…" } }
| Status | Codes |
|---|---|
| 400 | bad_request |
| 401 | unauthorized |
| 403 | forbidden, insufficient_scope, route_not_allowed, two_factor_required, email_unverified, expired (export download link) |
| 404 | not_found |
| 409 | conflict, agent_disabled, service_account_disabled, webhook_archived, unpromoted_stitches |
| 429 | rate_limited (with Retry-After) |
| 500 | internal |
| 501 | unsupported (repository lifecycle, import), not_supported (workstream remove / restore) — the Weave server is too old |
| 502 | weave_unavailable, bad_gateway (attestation keys) |
| 503 | unavailable (attestation keys) |
Weave server: { "type": "error", "code": "…", "message": "…", "retryable"?: true, "error_id"?: "…" }
| Status | Code | Notes |
|---|---|---|
| 400 | bad_request | |
| 401 | unauthenticated | |
| 403 | forbidden | No access, read-only caller, not the owner, not your action, confined route |
| 403 | path_out_of_scope | With paths and pathScope |
| 404 | not_found | Also every route of a deleted repository |
| 409 | conflict | Archived workstream or repository, name taken, raced review; also revert_conflict, attestations_required |
| 422 | secret_detected | With findings: [{path, line, kind}] |
| 429 | rate_limited | With Retry-After |
| 500 | internal | With error_id |
| 503 | unavailable | retryable: 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.
{ "name": "Acme", "slug": "acme" }
201:
{ "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.
{ "name": "shop", "description": "Our web shop", "private": true, "workstreams": ["preprod"], "template": "empty" }
| Field | Default | Notes |
|---|---|---|
name | required | ^[A-Za-z0-9._-]{1,100}$, not ., .. or .weave |
private | true | |
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:
{ "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.
{ "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 + path | Scope | Body | Response |
|---|---|---|---|
POST /api/v1/repos/{id}/copy | org: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}/transfer | repo:admin and org:admin; admin of both orgs | {target_org, name?} | {id, name, org, slug, from, revoked_agent_tokens} |
POST /api/v1/repos/{id}/archive | repo:admin | — | {id, archived: true, archived_at, archived_by} |
POST /api/v1/repos/{id}/unarchive | repo:admin | — | {id, archived: false, …} |
GET /api/v1/repos/{id}/export | repo: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
workstreamsalways 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 + path | Scope | Response |
|---|---|---|
DELETE /api/v1/repos/{id}/workstreams/{ws}?force=true|false | repo:write (PAT, service or worker agent token) | {workstream, archived: true, archivedAt, archivedBy} |
POST /api/v1/repos/{id}/workstreams/{ws}/restore | repo: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 + path | Scope | Body | Response |
|---|---|---|---|
GET /api/v1/tokens | any | — | {tokens: [{id, name, scopes, repositoryIds, expiresAt, lastUsed, createdAt}]} |
POST /api/v1/tokens | tokens: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.
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 + path | Scope | Body | Response |
|---|---|---|---|
GET /api/v1/orgs/{slug}/agents | org:admin | — | {agents: [Agent]} |
POST /api/v1/orgs/{slug}/agents | org: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}/tokens | tokens: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 + path | Scope | Body | Response |
|---|---|---|---|
GET /api/v1/orgs/{slug}/service-accounts | org:admin | — | {serviceAccounts: [ServiceAccount]} |
POST /api/v1/orgs/{slug}/service-accounts | org: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}/tokens | tokens:write | — | {tokens: [ServiceToken]} |
POST /api/v1/orgs/{slug}/service-accounts/{id}/tokens | tokens: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 + path | Body | Response |
|---|---|---|
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 + path | Access | Body / query | Response |
|---|---|---|---|
GET /workstreams[?archived=true] | read | — | {type: "workstreams", workstreams: [Workstream]} |
POST /workstreams | write | {slug, parent?, settings?: {ownership?, inherit?}} | 200 {type: "workstream", workstream} |
GET /workstreams/{ws} | read | — | {type: "workstream", workstream} |
GET /workstreams/{ws}/head | read | — | {type: "head", workstream, head, seq} |
PATCH /workstreams/{ws}/settings | admin, 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}/restore | creator or admin | — | {type: "workstream", workstream} |
POST /workstreams/{from}/promote | write | {to, changesets: [id], with_dependencies?} | {type: "promote", promoted: [{source_changeset, outcome}], required, change_requests} |
POST /workstreams/{ws}/refresh | write | — | {type: "refreshed", head, inherited, change_requests} |
POST /workstreams/{ws}/seed | write 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}/revert | write | {changeset, why} or {changesets: [id], why} | {type: "reverted", outcome, change_requests, …} |
GET /workstream-summaries | read | — | 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).
{ "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}:
{ "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:
{ "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.
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.
Other stitch-related routes#
| Method + path | Does |
|---|---|
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 /rpc | Any protocol request as JSON ({"type": "…", …}) |
Weave server: claims and ownership#
| Method + path | Body / query | Response |
|---|---|---|
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.
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 + path | Body | Notes |
|---|---|---|
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 + path | Body / query | Response |
|---|---|---|
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 + path | Body / query | Response |
|---|---|---|
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.
Weave server: search#
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 + path | Body / 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}/supersede | as 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 + path | Access |
|---|---|
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 + path | Access |
|---|---|
GET /audit/verification[?deep=true] | read (deep: admin) |
GET /audit/export?from=&to=&after=&limit=&format=json|ndjson | admin |
POST /purges {blob | text, reason}, GET /purges[?state=], GET /purges/{id}, POST /purges/{id}/approve, POST /purges/{id}/cancel, GET /purges/{id}/manifest | admin (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 + path | Body |
|---|---|
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}/push | PATCH: {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.