Resources
Shapes returned inside data (and nested on detail responses).
Tenant
{
"tenantId": "tenant_…",
"name": "Acme Agents",
"ownerEmail": "ops@example.com",
"ownerName": "Ops",
"status": "active" | "suspended",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
AppUser
{
"tenantId": "tenant_…",
"userId": "user_…",
"email": "ops@example.com",
"name": "Ops",
"role": "admin" | "consumer",
"cognitoSub": "…",
"status": "active" | "disabled",
"createdAt": "…",
"updatedAt": "…"
}
ApiKeyRecord
{
"tenantId": "tenant_…",
"keyId": "key_…",
"label": "CI consumer",
"scope": "admin" | "consumer",
"status": "active" | "revoked",
"createdAt": "…",
"lastUsedAt": "…"
}
Plaintext secret ak_… is returned only on create/signup/rotate — never as
keyHash.
WebhookEndpoint
{
"endpointId": "whk_…",
"url": "https://agent.example.com/hooks/agentfabric",
"description": "Content agent",
"events": ["review_request.decided"],
"secretHint": "whsec_1a2b…",
"status": "active" | "disabled",
"createdAt": "…",
"lastSucceededAt": "…",
"lastFailedAt": "…",
"lastFailureReason": "Endpoint responded 503"
}
No events means every event. The signing secret whsec_… is returned only on
create — see Webhooks.
Document
{
"type": "url" | "image" | "pdf" | "text" | "html",
"url": "https://…",
"title": "optional",
"content": "inline body…",
"metadata": { }
}
Policy
{
"type": "ALL_APPROVE" | "ANY_APPROVE" | "ANY_REJECT" | "QUORUM" | "ADVISORY",
"quorum": 2,
"rejectionThreshold": 1,
"decisionScope": "request" | "item"
}
ADVISORY collects feedback and gates nothing: it resolves to advised once every
reviewer has answered, and no answer — not even rejected — blocks it.
rejectionThreshold is ignored there.
ReviewRequest
{
"tenantId": "tenant_…",
"requestId": "rr_…",
"title": "Approve landing page",
"description": "optional",
"policy": { "type": "ALL_APPROVE", "decisionScope": "request" },
"status": "pending" | "approved" | "rejected" | "changes_required" | "advised" | "cancelled"
| "expired" | "precheck_running" | "precheck_failed",
"subscribers": ["ops@example.com"],
"externalId": "landing-page-v2",
"domain": "landing-pages",
"smartPrecheck": { "enabled": true, "instructions": "…", "blockOn": "major" },
"metadata": { "env": "staging" },
"createdAt": "…",
"updatedAt": "…"
}
ReviewItem
{
"itemId": "item_…",
"title": "Preview",
"document": { "type": "url", "url": "https://example.com/page" },
"status": "pending" | "approved" | "rejected" | "changes_required" | "cancelled" | "expired",
"order": 0,
"externalId": "preview-asset-1",
"metadata": { "branch": "main" }
}
Reviewer
{
"reviewerId": "reviewer_…",
"email": "reviewer@example.com",
"name": "Alex",
"channel": "email" | "none",
"decision": "pending" | "approved" | "rejected" | "changes_required",
"authCodeExpiresAt": "…",
"openedAt": "…",
"decidedAt": "…"
}
Detail responses may also include authCodeHash. Plaintext authCode
(rrc_…) is omitted in production create/add responses.
channel decides who delivers the review link, and is email when absent — see
Reviewer delivery channels.
ReviewItemDecision
{
"requestId": "rr_…",
"tenantId": "tenant_…",
"itemId": "item_…",
"reviewerId": "reviewer_…",
"decision": "approved" | "rejected" | "changes_required",
"decidedAt": "…"
}
{
"commentId": "comment_…",
"requestId": "rr_…",
"tenantId": "tenant_…",
"itemId": "item_…",
"reviewerId": "reviewer_…",
"authorEmail": "reviewer@example.com",
"authorRole": "reviewer" | "owner",
"threadId": "comment_…",
"parentCommentId": "comment_…",
"body": "Please fix the hero CTA.",
"status": "open" | "resolved",
"createdAt": "…",
"resolvedAt": "…",
"resolvedBy": "ops@example.com",
"groupId": "fg_…",
"context": { },
"metadata": { }
}
threadId points at the comment that started the thread and equals commentId on
that root; parentCommentId is set on replies. status, context and
groupId live on the root, so resolve and group by it.
ReviewNotice
An activity digest waiting to be emailed. Returned as notices on the request detail.
{
"audience": "subscribers" | "reviewer",
"reviewerId": "reviewer_…",
"notifyAfter": "2026-07-09T10:30:00.000Z",
"firstPendingAt": "…",
"lastActivityAt": "…",
"pendingComments": 3,
"pendingDecisions": 1,
"notifiedAt": "…"
}
notifyAfter is when the email goes out, and it is absent once nothing is owed. Each new
comment pushes it out, so one quiet period covers a whole burst.
ReviewViewContext
What the reviewer had on screen when they wrote the comment.
{
"itemId": "item_…",
"documentType": "url" | "image" | "pdf" | "text" | "html",
"documentUrl": "https://example.com/page",
"contentHash": "sha256 of inline content",
"viewport": { "width": 1440, "height": 900, "dpr": 2 },
"scroll": { "x": 0, "y": 1200 },
"selectionText": "the exact text they highlighted",
"userAgent": "…",
"capturedAt": "…",
"snapshot": {
"status": "pending" | "ready" | "failed" | "skipped",
"url": "presigned png url, present when ready",
"width": 1440,
"height": 900,
"capturedAt": "…"
}
}
Check
{
"checkId": "chk_…",
"status": "queued" | "running" | "passed" | "failed" | "error",
"domain": "landing-pages",
"title": "Pre-flight check",
"description": "…",
"items": [ /* ReviewItem */ ],
"instructionsUsed": "…",
"model": "grok-4.5",
"blockOn": "any_finding" | "major" | "critical",
"shareUrl": "https://cloud.browser-use.com/share/…",
"groupsConsidered": ["fg_…"],
"findings": [ /* Finding, same shape as on Precheck */ ],
"summary": "…",
"metadata": { },
"startedAt": "…",
"finishedAt": "…",
"updatedAt": "…"
}
The same gate as Precheck, run on its own: no review request, no
reviewers, nothing withheld.
Precheck
{
"precheckId": "pchk_…",
"requestId": "rr_…",
"attempt": 1,
"status": "queued" | "running" | "passed" | "failed" | "error",
"domain": "landing-pages",
"instructionsUsed": "…",
"blockOn": "any_finding" | "major" | "critical",
"shareUrl": "https://cloud.browser-use.com/share/…",
"groupsConsidered": ["fg_…"],
"summary": "The pricing table still contradicts the ad copy.",
"findings": [
{
"findingId": "find_…",
"severity": "critical" | "major" | "minor",
"title": "Price mismatch",
"detail": "The page shows 19 EUR while the ad promises 9 EUR.",
"itemId": "item_…",
"groupId": "fg_…",
"suggestedFix": "Update the pricing table to 9 EUR.",
"evidenceUrl": "presigned png url"
}
],
"startedAt": "…",
"finishedAt": "…"
}
FeedbackGroup
{
"groupId": "fg_…",
"domain": "landing-pages",
"title": "Pricing table contradicts the ad copy",
"summary": "Integrated content of every comment in this group.",
"rule": "The price on the page must match the price promised in the ad copy.",
"severity": "critical" | "major" | "minor",
"memberCount": 4,
"status": "active" | "muted" | "archived",
"mergedInto": "fg_…",
"firstSeenAt": "…",
"lastSeenAt": "…",
"version": 4
}
PrecheckConfig
{
"domain": "landing-pages",
"instructions": "Walk the full page and submit the signup form.",
"model": "optional agent model override",
"blockOn": "any_finding" | "major" | "critical",
"enabledByDefault": false,
"learnFromFeedback": true,
"maxGroups": 60,
"updatedAt": "…"
}
learnFromFeedback off keeps enforcing the rules already learned in this domain but stops new
reviewer feedback from becoming a rule.
AuditEvent
{
"eventId": "event_…",
"tenantId": "tenant_…",
"requestId": "rr_…",
"itemId": "item_…",
"reviewerId": "reviewer_…",
"type": "request_created" | "reviewer_added" | "reviewer_reminded" | "link_opened"
| "item_viewed" | "commented" | "comment_resolved" | "comment_unresolved"
| "approved" | "rejected" | "changes_required" | "policy_resolved" | "cancelled"
| "precheck_queued" | "precheck_passed" | "precheck_failed" | "precheck_error" | "precheck_overridden",
"createdAt": "…",
"metadata": { }
}
Outcome
{
"status": "pending",
"summary": "0/2 approved · 2 awaiting",
"tally": {
"approved": 0,
"rejected": 0,
"changes_required": 0,
"pending": 2
}
}
Presentation fields on list/detail:
outcome, evaluationScope ("request" | "item"),
optional itemOutcomes: [{ itemId, status }].
Review requests
POST /v1/review-requests
Auth: tenant · MCP reviews (create)
Request
{
"title": "Approve landing page",
"description": "optional",
"externalId": "landing-page-v2",
"domain": "landing-pages",
"smartPrecheck": { "enabled": true, "instructions": "…", "blockOn": "major" },
"policy": { "type": "ALL_APPROVE", "decisionScope": "request" },
"reviewers": [{ "email": "reviewer@example.com", "name": "Alex", "channel": "email" }],
"subscribers": ["ops@example.com"],
"items": [{
"id": "optional-client-id",
"title": "Preview",
"externalId": "preview-asset-1",
"document": { "type": "url", "url": "https://example.com/page" },
"metadata": { "branch": "main" }
}],
"metadata": { "env": "staging" }
}
Response data
{
"request": { /* ReviewRequest, status pending */ },
"items": [ /* ReviewItem */ ],
"reviewers": [{
"reviewerId": "reviewer_…",
"email": "reviewer@example.com",
"name": "Alex",
"channel": "email",
"decision": "pending",
"authCodeExpiresAt": "…"
}],
"reviewerLinks": [ /* only reviewers you have to deliver to yourself; see Channels */ ],
"next_actions": [
"wait_for_reviewers",
"fetch_review_request",
"open_dashboard"
]
}
With smartPrecheck.enabled (or enabledByDefault on the domain config), no
email is sent: the request comes back as precheck_running with a queued
precheck and next_actions: ["wait_for_precheck", "fetch_precheck"]. See
Smart precheck.
Reviewer delivery channels
reviewers[].channel decides who delivers the review link. It defaults to
email, which is the behaviour every existing request already has.
email AgentFabric emails the invitation, the reminders and the activity digests.
none AgentFabric mints the link and sends nothing. You deliver it — your product,
your chat, your agent — and reminder/digest emails are skipped for that reviewer.
A none reviewer's link comes back in reviewerLinks on create,
POST …/reviewers and POST …/remind:
{
"reviewerId": "reviewer_…",
"email": "agent@example.com",
"channel": "none",
"reviewUrl": "https://review.agentfabric.dev/review?authCode=rrc_…",
"sent": false
}
The list is empty when every reviewer is on email, so an empty list means nothing is owed. Since no
digest email goes out for that reviewer, the
review_comment.created webhook is how you learn about their
replies. Treat reviewUrl as a credential: it authenticates as that reviewer.
GET /v1/review-requests
Auth: tenant · MCP reviews (list)
Query: optional filter[externalId]. Newest first.
Response data
[
{
/* ReviewRequest fields… */
"outcome": { /* Outcome */ },
"evaluationScope": "request" | "item",
"itemOutcomes": [{ "itemId": "item_…", "status": "pending" }]
}
]
Cancelled/expired rows omit presentation fields.
GET /v1/review-requests/{requestId}
Auth: tenant · MCP reviews (get)
Response data
{
"request": { /* ReviewRequest */ },
"items": [ /* ReviewItem */ ],
"reviewers": [ /* Reviewer */ ],
"itemDecisions": [ /* ReviewItemDecision */ ],
"comments": [ /* ReviewComment */ ],
"notices": [ /* ReviewNotice: pending activity digests */ ],
"events": [ /* AuditEvent */ ],
"outcome": { /* Outcome */ },
"precheck": { /* latest Precheck, when the gate has run */ },
"evaluationScope": "request" | "item",
"itemOutcomes": [{ "itemId": "item_…", "status": "approved" }]
}
Comment snapshots and precheck evidence come back as short-lived presigned URLs.
GET /v1/review-requests/{requestId}/items
Auth: tenant
Response data
[ /* ReviewItem */ ]
GET /v1/review-items
Auth: tenant · MCP reviews (list_items)
Query: filter[externalId] required. Newest first.
Response data
{
"items": [{
"requestId": "rr_…",
"request": { /* ReviewRequest */ },
"item": { /* ReviewItem */ }
}]
}
POST /v1/review-requests/{requestId}/notify
Auth: tenant · MCP comments (notify)
Request
{ "reviewerIds": ["reviewer_…"] }
Sends pending replies immediately instead of waiting out the quiet period. Omit
reviewerIds to flush every reviewer with unsent replies. 410 if cancelled
or expired.
Response data
{
"notified": [{
"reviewerId": "reviewer_…",
"outcome": "sent" | "idle" | "claimed_by_other",
"recipients": ["reviewer@example.com"]
}]
}
POST /v1/review-requests/{requestId}/reviewers
Auth: tenant · MCP reviews (add_reviewer)
Request
{
"reviewers": [{ "email": "new@example.com", "name": "Sam" }]
}
409 if email already assigned.
Response data
{ /* added reviewers (same shape as create) */ }
POST /v1/review-requests/{requestId}/remind
Auth: tenant · MCP reviews (remind)
Request
{ "emails": ["reviewer@example.com"] }
emails optional — defaults to all pending reviewers. Regenerates auth codes.
410 if cancelled/expired.
Response data
{
"reminded": [
{ "reviewerId": "reviewer_…", "email": "reviewer@example.com", "sent": true }
]
}
POST /v1/review-requests/{requestId}/cancel
Auth: tenant · MCP reviews (cancel)
Response data
{ "status": "cancelled" }