Concepts

How authentication, review content, policies, and workflows fit together.

Authentication

Actor How Notes
Agents / backends x-api-key: ak_… or Authorization: Bearer ak_… Scopes: admin or consumer
Dashboard users Cognito email/password → bearer access token (or x-access-token) Roles: admin or consumer
Email reviewers Personal link with auth code (rrc_…) No account; exchange via POST /v1/review/exchange

Admin can do everything consumer can. Check the current principal with GET /v1/me.

API keys & users

API keys

  • Admin — manage users and keys; also create and read review requests.
  • Consumer — create and read review requests only. Prefer this for agents.

Secrets are returned once on create/signup/rotate (ak_…). Hashed at rest. Admin endpoints:

  • POST /v1/api-keys{ "label", "scope?" } (default consumer)
  • GET /v1/api-keys — list active keys (no secret)
  • POST /v1/api-keys/{keyId}/revoke
  • POST /v1/api-keys/{keyId}/rotate — revokes and issues a new admin key

Users

  • GET /v1/users / POST /v1/users — list / invite (email, optional name, role)
  • PATCH /v1/users/{userId} — set role
  • DELETE /v1/users/{userId} — cannot delete yourself or the last admin

Documents

Each review item has a document descriptor:

{
  "type": "url" | "image" | "pdf" | "text" | "html",
  "url": "https://…",          // typical for url / image / pdf
  "title": "optional title",
  "content": "inline body…",   // typical for text / html
  "metadata": { }
}

type is required. Other fields are optional at the schema level — send what your asset needs (for example a public URL for previews, or inline content for copy).

External IDs & metadata

externalId is an optional string on requests and items (/^[A-Za-z0-9._:-]+$/, 1–128 chars. Reuse the same ID across reviews over time (same PR, asset, or deploy target). List endpoints return newest first.

GET /v1/review-requests?filter[externalId]=landing-page-v2
GET /v1/review-items?filter[externalId]=preview-asset-1

MCP equivalents: reviews with operation: "list" and external_id; reviews list_items requires external_id.

metadata is an opaque key-value bag on requests, items, comments, and documents. Stored and returned; not searchable.

Subscribers

Optional subscribers on create — email addresses notified when the request reaches a verdict (approved, rejected, or changes_required. Set at creation only.

Reviewers decide; subscribers are notified of the result. They are not the same role.

Multi-item reviews

One request can include several assets. Prefer decisionScope: "item" so reviewers approve or reject each item independently.

"policy": { "type": "ALL_APPROVE", "decisionScope": "item" },
"items": [
  { "title": "Landing page", "document": { "type": "url", "url": "https://…" } },
  { "title": "Creative PDF", "document": { "type": "pdf", "url": "https://…" } }
]

Request-level status rolls up items: pending if any pending; rejected if any rejected; changes_required if any need changes; otherwise approved. Detail/list responses include evaluationScope, outcome, and often itemOutcomes.

Policies

"policy": {
  "type": "ALL_APPROVE" | "ANY_APPROVE" | "ANY_REJECT" | "QUORUM" | "ADVISORY",
  "quorum": 2,                    // QUORUM only; defaults to reviewer count
  "rejectionThreshold": 1,        // optional early reject after N rejections
  "decisionScope": "request" | "item"   // default "request"
}
Type Behavior
ALL_APPROVE Every reviewer must approve; any rejection rejects. Pending while anyone is undecided.
ANY_APPROVE First approval wins. Otherwise pending, then rejection / changes if no approvals remain.
ANY_REJECT First rejection wins. Otherwise pending, then changes / approval.
QUORUM Approvals ≥ quorum → approved. If remaining non-rejected reviewers cannot reach quorum → rejected.
ADVISORY Feedback, no gate. Resolves to advised once every reviewer has answered, and no answer — not even a rejection — blocks it. Use it for the internal round before the work goes to whoever actually approves it.

If rejectionThreshold is set, reaching that many rejections rejects immediately for all policy types except ADVISORY, which ignores it. cancelled / expired are terminal and not overwritten by policy.

Status & outcomes

Request / item status: pending, approved, rejected, changes_required, advised, cancelled, expired, precheck_running, precheck_failed.

advised only happens under an ADVISORY policy: the round finished and the feedback is in, without anything having been approved or blocked.

The last two only occur when the smart precheck is enabled, and are never overwritten by policy evaluation.

List and detail responses include outcome:

{
  "status": "pending",
  "summary": "0/2 approved · 2 awaiting",
  "tally": {
    "approved": 0,
    "rejected": 0,
    "changes_required": 0,
    "pending": 2
  }
}

For item-scoped reviews, outcome.tally counts items; use outcome.summary for mixed results such as 1 approved · 1 rejected. Comment status is open or resolved.

Create a review request

POST /v1/review-requests · MCP reviews (create)

Body fields

Field Required Description
title yes Request title
description no Longer context for reviewers
items yes (≥1) Assets to review — see below
reviewers yes (≥1) { email, name? }
subscribers no Emails notified on verdict
policy yes See Policies
externalId no Your correlation ID
domain no Groups requests that share a feedback structure, e.g. ads vs landing-pages. Defaults to default.
smartPrecheck no { enabled, instructions?, model?, blockOn? } — see Smart precheck
metadata no Opaque bag

Item fields

Field Required Description
title yes Item title
document yes See Documents
id no Client-supplied item id
externalId no Item correlation ID
metadata no Opaque bag

Example

curl -X POST "https://rest.agentfabric.dev/v1/review-requests" \
  -H "content-type: application/json" \
  -H "x-api-key: $AGENTFABRIC_API_KEY" \
  -d '{
    "title": "Approve landing page",
    "description": "Draft for next week’s launch",
    "externalId": "landing-page-v2",
    "policy": { "type": "ALL_APPROVE", "decisionScope": "request" },
    "reviewers": [{ "email": "reviewer@example.com", "name": "Alex" }],
    "subscribers": ["ops@example.com"],
    "items": [
      {
        "title": "Preview",
        "externalId": "preview-asset-1",
        "document": { "type": "url", "url": "https://example.com/page" },
        "metadata": { "branch": "main" }
      }
    ],
    "metadata": { "env": "staging" }
  }'

Full request/response schemas: POST /v1/review-requests and Resources.

Smart precheck

An optional quality gate. Before a single reviewer is emailed, a browser agent opens every review URL and checks it against your instructions and the rules learned from past reviewer feedback in the same domain. Reviewers only ever see work that already cleared the problems they reported before.

Enable it per request, or for a whole domain with enabledByDefault in the precheck config:

{
  "smartPrecheck": {
    "enabled": true,
    "instructions": "Walk the full landing page and submit the signup form with a test address.",
    "blockOn": "major"
  }
}

With the gate on, POST /v1/review-requests sends no email and returns:

{
  "request": { "status": "precheck_running", ... },
  "precheck": { "precheckId": "pchk_…", "attempt": 1, "status": "queued" },
  "next_actions": ["wait_for_precheck", "fetch_precheck"]
}
Outcome Request status What happens
Pass pending Reviewer invitations go out exactly as they normally would
Fail precheck_failed No email; findings explain each problem with severity, the offending item, a suggested fix and an evidence screenshot
Error precheck_failed A single critical finding explains why the gate could not run, so nothing is ever silently stuck

blockOn decides which findings block delivery: any_finding, major (default) or critical. Fix the findings and call POST /v1/review-requests/{id}/precheck/rerun, or send the invitations regardless with POST /v1/review-requests/{id}/precheck/override.

model picks the browser agent's model and defaults to grok-4.5. Use claude-opus-5 when accuracy matters more than speed, or minimax-m3 for the cheapest run. Every run is capped at $1.50 of model spend and inspects the pages at 1440×900, the same viewport as the evidence screenshots.

Feedback groups

Reviewer feedback accumulates forever, so the gate never reads raw comments. Each new comment is either merged into an existing group for its domain or starts a new one, and the group's summary and rule are rewritten to integrate it rather than growing a list. The precheck prompt therefore scales with the number of distinct problems, not the number of comments.

A group holds a title, an integrated summary, one imperative rule the checker enforces, a severity and a memberCount. When a domain exceeds maxGroups (60 by default), similar groups are merged and the absorbed ones are archived with mergedInto set. Mute, archive or rewrite any group from the Smart precheck tab in the dashboard or via PATCH /v1/feedback-groups/{groupId}.

Comments

Comments are threaded conversations, and both sides can write. Reviewers start threads from their link; agents reply with comments (create), listing and resolving whole threads. See ReviewComment and comment endpoints.

A comment also records what the reviewer had on screen in context: the item and document URL, viewport size, scroll position, any selected text, and a screenshot captured right after submission. Both the resolving agent and the smart precheck use it to see the problem the way the reviewer did.

Nobody is emailed per comment. Activity starts a 30-minute quiet period for the other side and each new comment pushes the deadline out, so a burst arrives as one digest. Reviewer comments and verdicts notify the request's subscribers — which is also how a reviewer who leaves verdicts but never finishes still reaches you — while your replies notify only the reviewer whose thread you answered. To skip the wait, call comments with operation: "notify"; reviewers do the same by finishing their review.

Reviewers

Each reviewer gets an emailed personal link {REVIEW_APP_URL}/review?authCode=… — no account. Add, remind, or cancel via the review request endpoints (or MCP reviews with add_reviewer / remind / cancel).