# API reference

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

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](https://www.weave.directory/docs/security#token-types)). 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](https://www.weave.directory/docs/agents#retrying-retryable-errors)) |

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" }
```

| 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`:

```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 + 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 `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 + 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](https://www.weave.directory/docs/workstreams#remove-and-restore-a-workstream).

## 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.

```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 + 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](https://www.weave.directory/docs/agents#register-agents-and-issue-agent-tokens).

## 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](https://www.weave.directory/docs/webhooks).

## 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](https://www.weave.directory/docs/decisions-and-history#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).

```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.

### 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`.

```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 + 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](https://www.weave.directory/docs/working-together#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](https://www.weave.directory/docs/webhooks#polling-and-replay)) |
| `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](https://www.weave.directory/docs/search#search-over-the-api).

## 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](https://www.weave.directory/docs/decisions-and-history#tags).

## 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](https://www.weave.directory/docs/security#lawful-purge).

## 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](https://www.weave.directory/docs/webhooks#deliveries-and-redelivery).
