Skip to content

Submit a reviewer decision (approve/edit/reject/escalate)

POST
/v1/reviews/{id}/decision

The reviewer’s decision on a draft it holds (D5; §9). approve/edit → the PLATFORM performs the authenticated send using the COMPOSER’s inbox credentials (the reviewer holds review:act, NEVER mailbox:send on an inbox it doesn’t own; the credential boundary); send_path becomes reviewer_approved and terminal success is written post-delivery only. reject → the draft returns to the composer (needs_review) and hop_count is incremented. escalate → the draft goes to the human queue with an escalation_reason. revision + version are the optimistic CAS; a mismatch is 409 STALE with NO mutation (the human/composer moved the draft; the human always wins, D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline) FORCE a reject to the human regardless of intent; forced_by_breaker names the tripped breaker. An unknown action is 400; a cross-tenant id is 404; a non-reviewer is 403. $0 LLM; the reviewer agent judged; we route, send, and enforce the breakers.

id
required
string

A reviewer decision. action is approve|edit|reject|escalate. revision + version are the optimistic CAS (a mismatch is 409 STALE with NO mutation; the human always wins, D17). subject/body carry the edited content for the edit action. feedback is the reviewer’s note (reject: the rule-birth signal; escalate: the human-facing escalation reason).

object
action
required
string
Allowed values: approve edit reject escalate
revision
required

The revision you decided against (PRIMARY CAS).

integer
version

Optional row-version CAS (defense in depth).

integer
subject

Edited subject (edit action).

string
body

Edited body text (edit action).

string
feedback

Reviewer note (reject: rule-birth signal; escalate: human-facing reason).

string

Ok (sent via the composer’s creds, or returned to the human queue)

The reviewer-decision outcome. kind=sent when the platform sent with the COMPOSER’s creds (approve/edit); kind=sent_to_human when the draft returned to the human queue (reject/escalate, or a reject forced to the human by a circuit breaker). forced_by_breaker names the tripped breaker when one overrode the intent.

object
kind
required
string
Allowed values: sent sent_to_human
review
required

A review request (rr_…); the server-owned pre-send record of a message under the Review Loop. Carries intent, the current proposed draft, category, state machine fields, and (once sent) the sent body + diff.

object
id
required
string
state
required
string
Allowed values: needs_review in_review chatting stale approved sent auto_sent rejected stalled cancelled failed
mode
required
string
Allowed values: review direct
effective_mode
required
string
Allowed values: review direct
kind
required
string
Allowed values: send reply forward
from_address
string
agent_id
string
category_id
string
intent_summary
string
intent_meta
object
key
additional properties
any
revision
required
integer
version
required
integer
proposed_subject
string
proposed_body_text
string
proposed_body_html
string
proposed_to
Array<string>
proposed_cc
Array<string>
proposed_bcc
Array<string>
sent_subject
string
sent_body_text
string
diff_unified
string
sent_message_id
string
gate_outcome
string
stale_reason
string
decision_feedback
string
closed

True once this review will never move again; sent, auto_sent, cancelled OR failed. It is the definitive per-review “am I done?” answer and the poll-side companion to the terminal review events; use it after a restart when your event cursor is gone. failed is included deliberately: nothing re-approves a failed review, so treating it as open means waiting forever.

boolean
send_error

Vendor-scrubbed delivery failure, present on a failed review.

string
send_path

How the message was released, once sent.

string
Allowed values: human_reviewed reviewer_approved graduated_auto agent_direct
created_at
string format: date-time
updated_at
string format: date-time
decided_at
string format: date-time
sent_at
string format: date-time
sent
required

True iff the platform sent with the composer’s creds.

boolean
message_id

The delivered message id when sent.

string
thread_id

The thread id when sent on the reply path.

string
sent_to_human
required

True iff the draft returned to the human queue.

boolean
forced_by_breaker

The tripped circuit breaker (max_hops_reached | review_deadline_passed) when one forced the human.

string

Invalid request.

The canonical error envelope. error is a stable machine code.

object
error
required

Stable error code (e.g. unauthorized, forbidden, not_found, invalid, quota_exceeded, rate_limited).

string
Example
forbidden
message

Human-readable detail (never leaks internals).

string
Example
missing required scope

Missing or invalid credential.

The canonical error envelope. error is a stable machine code.

object
error
required

Stable error code (e.g. unauthorized, forbidden, not_found, invalid, quota_exceeded, rate_limited).

string
Example
forbidden
message

Human-readable detail (never leaks internals).

string
Example
missing required scope

Authenticated but lacking the required scope, or out of quota.

The canonical error envelope. error is a stable machine code.

object
error
required

Stable error code (e.g. unauthorized, forbidden, not_found, invalid, quota_exceeded, rate_limited).

string
Example
forbidden
message

Human-readable detail (never leaks internals).

string
Example
missing required scope

Resource not found.

The canonical error envelope. error is a stable machine code.

object
error
required

Stable error code (e.g. unauthorized, forbidden, not_found, invalid, quota_exceeded, rate_limited).

string
Example
forbidden
message

Human-readable detail (never leaks internals).

string
Example
missing required scope

A Review Loop conflict, as problem+json. Branch on code, NOT on the 409 status; the four codes demand opposite behavior. stale: the revision/version you named is no longer current (a human moved the draft) and NOTHING was mutated; errors[] carries the current state, revision and version, so re-apply your change on top and resubmit with the new parent_revision (retry, bounded to ~3). born_stale: the redraft was built against an older rule high-water; re-read the rules and resubmit, or restamp_review if nothing genuinely changed (at most one retry per high-water). wrong_state: this VERB is illegal from the current state but the draft is still live; NEVER retry the same verb; errors[] repeats an allowed_action entry per verb that IS legal right now. terminal: the review is already sent/auto_sent/cancelled; nothing will ever succeed, stop retrying, and a front_run_next review event carries the outcome. send_needs_reconciliation: a prior attempt is unconfirmed and parked for recover-by-Message-ID; do NOT resend, poll the review.

RFC-9457 problem+json error body. code is a closed machine enum clients switch on; type is a dereferenceable URI under https://extrovert.dev/problems/. Served as application/problem+json.

object
type
required
string format: uri
title
required
string
status
required
integer
detail
string
code
required

The CLOSED machine code. The Review Loop members split what used to be a single opaque conflict, because an agent must take a DIFFERENT action on each: stale (the revision/version you named is no longer current; nothing was mutated; re-read, re-apply, resubmit; retryable, bounded) and born_stale (built against an older rule high-water; re-read the rules or restamp_review; at most one retry per high-water) are the ONLY retryable 409s. wrong_state means this VERB is illegal from the current state while the draft is still live; never retry the same verb, read the state and the repeated allowed_action hints in errors[] and pick a legal one. terminal means the review is already sent/auto_sent/cancelled and nothing will EVER succeed; stop, and drain your review events for the outcome. send_needs_reconciliation means a prior send is unconfirmed and parked. Do not resend. Poll instead. unavailable (503) is the retryable fail-closed answer when a dependency could not be read; it carries Retry-After and is distinct from not_configured, which is permanent for this deployment.

string
Allowed values: bad_request unauthorized forbidden_scope not_found conflict idempotency_conflict breadth_required quota_exceeded rate_limited domain_not_allowed recipient_blocked recipient_suppressed not_configured domain_unavailable internal intent_required wrong_state terminal stale born_stale send_needs_reconciliation graduation_locked maturity_gate_unmet scope_taken unavailable
request_id
string
errors
Array<object>
object
field
string
code
string
detail
string