REST API

Base URL https://rest.agentfabric.dev. Resource schemas and per-endpoint request/response shapes.

Envelope & errors

Base URL: https://rest.agentfabric.dev. All successful handler responses:

{ "data": { /* payload */ } }

Errors:

{ "errors": [{ "title": "Human-readable message", "path": ["field"] }] }
StatusTypical cause
400Validation; path may point at the field
401Missing/invalid API key, user token, or review code
403Admin permission required
404Unknown request, comment, or user
409Duplicate reviewer email
410Cancelled/expired request
500Internal server error

Bodies may be flat JSON or { "data": { … } }. Auth: x-api-key / Authorization: Bearer ak_… (tenant), Cognito bearer (dashboard), or reviewer auth code. ID prefixes: tenant_, rr_, item_, reviewer_, comment_, ak_, key_, rrc_, event_.

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.

Document

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

Policy

{
  "type": "ALL_APPROVE" | "ANY_APPROVE" | "ANY_REJECT" | "QUORUM",
  "quorum": 2,
  "rejectionThreshold": 1,
  "decisionScope": "request" | "item"
}

ReviewRequest

{
  "tenantId": "tenant_…",
  "requestId": "rr_…",
  "title": "Approve landing page",
  "description": "optional",
  "policy": { "type": "ALL_APPROVE", "decisionScope": "request" },
  "status": "pending" | "approved" | "rejected" | "changes_required" | "cancelled" | "expired",
  "subscribers": ["ops@example.com"],
  "externalId": "landing-page-v2",
  "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",
  "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.

ReviewItemDecision

{
  "requestId": "rr_…",
  "tenantId": "tenant_…",
  "itemId": "item_…",
  "reviewerId": "reviewer_…",
  "decision": "approved" | "rejected" | "changes_required",
  "decidedAt": "…"
}

ReviewComment

{
  "commentId": "comment_…",
  "requestId": "rr_…",
  "tenantId": "tenant_…",
  "itemId": "item_…",
  "reviewerId": "reviewer_…",
  "authorEmail": "reviewer@example.com",
  "body": "Please fix the hero CTA.",
  "status": "open" | "resolved",
  "createdAt": "…",
  "resolvedAt": "…",
  "resolvedBy": "ops@example.com",
  "metadata": { }
}

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",
  "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 }].

Auth & session

POST /v1/signup

Auth: public · MCP create_tenant

Request

{
  "tenantName": "Acme Agents",
  "ownerEmail": "ops@example.com",
  "ownerName": "Ops",
  "password": "change-me-now"
}

password min 8 characters.

Response data

{
  "tenant": { /* Tenant */ },
  "user": { /* AppUser, role admin */ },
  "adminApiKey": "ak_…",
  "adminApiKeyRecord": { /* ApiKeyRecord without keyHash */ },
  "consumerApiKey": "ak_…",
  "consumerApiKeyRecord": { /* ApiKeyRecord without keyHash */ },
  "apiKey": "ak_…"
}

apiKey is a deprecated alias of adminApiKey.

POST /v1/auth/signin

Auth: public

Request

{ "email": "ops@example.com", "password": "…" }

Response data

{ "tokens": { /* Cognito AuthResult */ }, "user": { /* AppUser */ } }

POST /v1/auth/refresh

Auth: public

Request

{ "refreshToken": "…" }

Response data

{ "tokens": { /* Cognito AuthResult */ } }

POST /v1/auth/forgot-password

Auth: public

Request

{ "email": "ops@example.com" }

Response data

{ "sent": true }

POST /v1/auth/confirm-forgot-password

Auth: public

Request

{
  "email": "ops@example.com",
  "code": "123456",
  "newPassword": "NewPass1"
}

code is 6 characters. Password: min 8, must include digit, upper, and lower case.

Response data

{ "reset": true }

GET /v1/me

Auth: tenant

Response data

// user session
{ "user": { /* AppUser */ }, "role": "admin" | "consumer" }

// API key session
{ "apiKey": { /* ApiKeyRecord without keyHash */ }, "role": "admin" | "consumer" }

Review requests

POST /v1/review-requests

Auth: tenant · MCP create_review_request

Request

{
  "title": "Approve landing page",
  "description": "optional",
  "externalId": "landing-page-v2",
  "policy": { "type": "ALL_APPROVE", "decisionScope": "request" },
  "reviewers": [{ "email": "reviewer@example.com", "name": "Alex" }],
  "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",
    "decision": "pending",
    "authCodeExpiresAt": "…"
  }],
  "next_actions": [
    "wait_for_reviewers",
    "fetch_review_request",
    "open_dashboard"
  ]
}

GET /v1/review-requests

Auth: tenant · MCP list_review_requests

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 get_review_request

Response data

{
  "request": { /* ReviewRequest */ },
  "items": [ /* ReviewItem */ ],
  "reviewers": [ /* Reviewer */ ],
  "itemDecisions": [ /* ReviewItemDecision */ ],
  "comments": [ /* ReviewComment */ ],
  "events": [ /* AuditEvent */ ],
  "outcome": { /* Outcome */ },
  "evaluationScope": "request" | "item",
  "itemOutcomes": [{ "itemId": "item_…", "status": "approved" }]
}

GET /v1/review-requests/{requestId}/items

Auth: tenant

Response data

[ /* ReviewItem */ ]

GET /v1/review-items

Auth: tenant · MCP list_review_items

Query: filter[externalId] required. Newest first.

Response data

{
  "items": [{
    "requestId": "rr_…",
    "request": { /* ReviewRequest */ },
    "item": { /* ReviewItem */ }
  }]
}

GET /v1/review-requests/{requestId}/comments

Auth: tenant · MCP get_review_comments

Query: optional itemId, status=open|resolved.

Response data

[ /* ReviewComment */ ]

POST /v1/review-requests/{requestId}/comments/{commentId}/resolve

Auth: tenant · MCP resolve_review_comment

No body. Sets resolvedBy to the caller email or api-key:{label}.

Response data

{ /* ReviewComment, status resolved */ }

POST /v1/review-requests/{requestId}/comments/{commentId}/unresolve

Auth: tenant · MCP unresolve_review_comment

Response data

{ /* ReviewComment, status open */ }

POST /v1/review-requests/{requestId}/reviewers

Auth: tenant · MCP 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 remind_reviewers

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 cancel_review_request

Response data

{ "status": "cancelled" }

Users & API keys

Admin role / admin API key required.

GET /v1/users

Auth: admin · MCP list_users

Response data

[ /* AppUser */ ]

POST /v1/users

Auth: admin · MCP create_user

Request

{
  "email": "dev@example.com",
  "name": "Dev",
  "role": "consumer"
}

role defaults to consumer.

Response data

{
  "user": { /* AppUser */ },
  "temporaryPassword": "…"
}

PATCH /v1/users/{userId}

Auth: admin

Request

{ "role": "admin" | "consumer" }

Response data

{ /* AppUser */ }

400 if demoting the last admin.

DELETE /v1/users/{userId}

Auth: admin

Response data

{ "userId": "user_…", "deleted": true }

400 if deleting yourself or the last admin.

GET /v1/api-keys

Auth: admin · MCP list_api_keys

Response data

[ /* ApiKeyRecord without keyHash; active only */ ]

POST /v1/api-keys

Auth: admin · MCP create_api_key

Request

{ "label": "CI consumer", "scope": "consumer" }

scope defaults to consumer.

Response data

{
  "apiKey": "ak_…",
  "apiKeyRecord": { /* ApiKeyRecord without keyHash */ }
}

POST /v1/api-keys/{keyId}/revoke

Auth: admin

Response data

{ "keyId": "key_…", "status": "revoked" }

POST /v1/api-keys/{keyId}/rotate

Auth: admin

Revokes the old key and creates a new admin key labeled Rotated API key.

Response data

{
  "apiKey": "ak_…",
  "apiKeyRecord": { /* ApiKeyRecord without keyHash */ }
}

Reviewer API

Used by the review web app. Agents rarely call these — reviewers use the emailed link.

POST /v1/review/exchange

Auth: public

Request

{ "authCode": "rrc_…" }

Response data

{
  "token": "rrc_…",
  "requestId": "rr_…",
  "reviewerId": "reviewer_…"
}

GET /v1/review/{requestId}

Auth: reviewer (Bearer auth code or ?authCode=)

Response data

{
  "request": { /* ReviewRequest */ },
  "items": [ /* ReviewItem */ ],
  "reviewer": { /* Reviewer (this reviewer) */ },
  "itemDecisions": [ /* this reviewer's item decisions */ ],
  "comments": [ /* ReviewComment */ ],
  "deleted": false,
  "evaluationScope": "request" | "item",
  "reviewerAggregate": "pending" | "approved" | "rejected" | "changes_required"
}

POST /v1/review/{requestId}/opened

Auth: reviewer

Records a link_opened audit event.

GET /v1/review/{requestId}/comments

Auth: reviewer

Query: optional itemId, status.

Response data

[ /* ReviewComment */ ]

POST /v1/review/{requestId}/comments

Auth: reviewer

Request

{
  "body": "Please fix the hero CTA.",
  "itemId": "item_…",
  "metadata": { }
}

410 if cancelled.

Response data

{ /* ReviewComment */ }

POST /v1/review/{requestId}/decision

Auth: reviewer

Request

{
  "decision": "approved" | "rejected" | "changes_required",
  "itemId": "item_…",
  "comment": "optional note (creates a comment)"
}

Response data

{
  "status": "pending" | "approved" | "rejected" | "changes_required" | …,
  "itemDecision": { /* ReviewItemDecision, if itemId set */ },
  "reviewerAggregate": "pending" | "approved" | "rejected" | "changes_required"
}