Article 14 Review Queue

The Review Queue gives your application two things the bare /v1/oversight endpoint doesn’t: a protocol for gating an agent decision on human review BEFORE it executes, and a hosted UI reviewers can use without you building a single page. Drop in two API calls and one webhook handler. You have a working Article-14 oversight loop.

When to use this versus /v1/oversight

You want to…Use
Record a human review event after it happened (your app already has its own review UI) POST /v1/oversight
Queue an agent decision and wait for a reviewer’s verdict before letting the agent act POST /v1/oversight/pending
Let reviewers use Marturia’s hosted UI (no front-end build required) POST /v1/oversight/pending

End-to-end flow

Your application’s agent has produced a recommendation. You want a human to sign off before it executes:

  1. Queue the pending review. Call POST /api/marturia/v1/oversight/pending with the agent recommendation, the inputs the reviewer should see, any warning signals you want surfaced, and a webhook URL Marturia will POST back to when the reviewer decides. Marturia returns a review_url.
  2. Show the reviewer the review_url. Either email them the link, or redirect them to it from your own UI. Reviewers must be authenticated dashboard users (Clerk-style flow with signed-token public links is on the v2 roadmap).
  3. The reviewer acts. They see the recommendation, the alternatives, the warnings, and the five Article-14 action buttons (approve / override / reject / interrupt / no_action). They’re required to enter a rationale. They submit.
  4. Marturia mints a signed receipt in the same cryptographic chain as your agent’s decision. The receipt records the reviewer, the action, the rationale, the context shown, the timing — everything an EU AI Act conformity assessment will want.
  5. Your webhook fires. Marturia POSTs to your callback_url with the verdict + an HMAC-SHA256 signature. Your app verifies the signature, reads the action, and either proceeds or doesn’t.

Request: create a pending review

POST /api/marturia/v1/oversight/pending
Headers:
  X-Marturia-Key: mtu_live_<your key>
  Content-Type: application/json

Body:
{
  "agent_name":              "purchase_approver",
  "agent_recommendation":    "approve",
  "agent_confidence":        0.91,
  "agent_run_id":            "run_abc123",
  "reviews_receipt_seq":     12345,             // optional: parent agent-decision receipt seq
  "input_summary":           "Invoice #4242 from Acme Lumber, $6850. Vendor history mixed.",
  "alternatives_presented": [
    {"action": "approve",       "score": 0.91},
    {"action": "dispute",       "score": 0.08},
    {"action": "manual_review", "score": 0.01}
  ],
  "warning_messages":        ["Vendor has 3 prior disputed invoices in last 12 months"],
  "ui_context": {
    "fields_shown": ["agent_recommendation", "input_data", "confidence", "alternatives"],
    "ui_version":   "your-app-v3.2"
  },
  "severity":                "high",            // low | medium | high | critical
  "reviewer_hint":           "compliance officer review preferred",
  "expires_at":              "2026-05-11T20:00:00Z",
  "callback_url":            "https://your-app.example.com/marturia/callback"
}

Response (201):

{
  "id":           42,
  "status":       "pending",
  "review_url":   "https://marturia.dev/app/oversight/42",
  "requested_at": "2026-05-10T20:14:33Z",
  "expires_at":   "2026-05-11T20:00:00Z"
}

The five Article-14 actions

The reviewer’s response is restricted to a frozen enum — the same five values the receipt primitive supports:

ActionMeaning
approveReviewer accepted the agent’s recommendation as-is.
overrideReviewer changed the decision — final_outcome.decision must differ from the agent’s recommendation (server-enforced).
rejectReviewer blocked the action entirely. Nothing executes.
interruptReviewer used the “stop button” on a running multi-step agent.
no_actionReviewer was shown the decision but the review window timed out without an explicit choice.

Polling for completion (server-to-server)

If you don’t want to handle webhooks, poll instead:

GET /api/marturia/v1/oversight/pending/<id>
Headers:
  X-Marturia-Key: mtu_live_<your key>

Response:
{
  "id":                              42,
  "status":                          "completed",            // or "pending" / "expired" / "cancelled"
  "agent_name":                      "purchase_approver",
  "agent_recommendation":            "approve",
  "severity":                        "high",
  "requested_at":                    "2026-05-10T20:14:33Z",
  "completed_at":                    "2026-05-10T20:15:11Z",
  "completed_action":                "override",
  "completed_oversight_receipt_id":  213,
  "oversight_verify_url":            "https://marturia.dev/v1/verify/12/213"
}

While the item is still pending, the completed_* fields are null. Loop on this endpoint until status transitions out of pending. Recommended poll interval: 5 seconds.

Webhook callbacks (recommended)

If you set callback_url when creating the pending item, Marturia POSTs the verdict to that URL as soon as the reviewer acts. The webhook fires on completed, expired, and cancelled terminal transitions.

Request format

POST <your callback_url>
Headers:
  Content-Type:           application/json
  X-Marturia-Signature:   sha256=<hex of HMAC-SHA256(body, secret)>
  X-Marturia-Event:       oversight.completed       // or .expired / .cancelled
  X-Marturia-Pending-Id:  42

Body:
{
  "event":                  "oversight.completed",
  "pending_id":             42,
  "status":                 "completed",
  "completed_action":       "override",
  "oversight_receipt_id":   213,
  "oversight_receipt_seq":  169,
  "oversight_verify_url":   "https://marturia.dev/v1/verify/12/213",
  "completed_at":           "2026-05-10T20:15:11Z"
}

Signature verification

The HMAC secret is the SHA-256 digest of your Marturia API key — hashlib.sha256(api_key.encode()).digest(). Compute it once from the key you stored when you minted it. Always use a constant-time comparison.

Python (FastAPI / Flask):

import hmac, hashlib

API_KEY = os.environ["MARTURIA_API_KEY"]
SECRET  = hashlib.sha256(API_KEY.encode()).digest()

@app.post("/marturia/callback")
async def marturia_callback(request: Request):
    body = await request.body()
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    header   = request.headers.get("x-marturia-signature", "").removeprefix("sha256=")
    if not hmac.compare_digest(expected, header):
        raise HTTPException(401, "bad signature")

    event = json.loads(body)
    # event["completed_action"] is the reviewer's verdict
    # event["oversight_receipt_id"] is the immutable receipt
    return {"ok": True}

Node.js (Express):

const crypto = require("crypto");

const API_KEY = process.env.MARTURIA_API_KEY;
const SECRET  = crypto.createHash("sha256").update(API_KEY).digest();

app.post("/marturia/callback", express.raw({type: "application/json"}), (req, res) => {
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const header   = (req.headers["x-marturia-signature"] || "").replace(/^sha256=/, "");
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(req.body);
  // event.completed_action is the reviewer's verdict
  res.json({ok: true});
});

Retry policy

On a non-2xx response or timeout (10s), Marturia retries with exponential backoff: 1s, 5s, then 30s. After three failures, the result is logged on the pending row (callback_last_error) and Marturia stops retrying. You can re-fire manually from the operator dashboard (replay UI is on the v2 roadmap).

Expiration semantics

Set expires_at if you want unattended pending items to auto-resolve. A nightly sweep flips them to expired and fires the webhook with status=expired. Items without an expires_at stay pending forever (your responsibility to clean up via the dashboard).

Severity hints

Set severity to one of low / medium / high / critical. It controls UI ordering (critical first) and email-notification urgency, but it does NOT gate the action enum — compliance officers can still handle a “low” item with override if they want.

External Reviewer Flow (v2)

Reviewers don’t need a Marturia dashboard account. Mint a single-use signed URL with POST /api/marturia/v1/oversight/pending/{id}/share, email the URL to your external reviewer (compliance officer, auditor, CFO, end customer), they click and act. Same chain, same crypto, same Article-14 receipt.

Mint a share URL

POST /api/marturia/v1/oversight/pending/<id>/share
Headers:
  X-Marturia-Key: mtu_live_<your key>
  Content-Type: application/json

Body (optional):
{
  "expires_in_hours": 168       # default 7 days, max 30 days
}

Response (201):
{
  "id":          42,
  "share_url":   "https://marturia.dev/review/42?t=mrv_a1b2c3d4...",
  "expires_at":  "2026-05-17T20:00:00Z",
  "consumed_at": null
}

Send share_url to your reviewer. They click → see the agent recommendation + alternatives + warnings → enter their name, email (optional), and role (optional) → pick one of the five Article-14 actions → enter rationale → submit. The receipt mints, the chain holds, and the webhook fires.

What gets recorded for an external reviewer

The reviewer’s identity is taken from the form on the public page. The recorded receipt’s reviewer field looks like:

{
  "id":                 "Alice Carter",            // their full name
  "role":               "compliance officer",      // optional
  "tenant_user_email":  "[email protected]", // optional
  "external":           true                       // provenance marker
}

The external: true field is a provenance marker so auditors can distinguish receipts signed by your authenticated dashboard users from receipts signed via a public share URL. Both are valid Article-14 evidence; the marker is for clarity during audit reviews.

Single-use + time-bounded

Token security model

The token is mrv_<32 hex> — 128 bits of entropy from secrets.token_hex(16). We store SHA-256(token), not the plaintext. The plaintext appears only in the URL we hand back at share-creation time. Lookup is a single-row SELECT on a UNIQUE partial index — no timing channel beyond the constant SHA-256 hash.

Anyone with the URL can act — same trust model as Stripe invoice links, DocuSign envelopes, and Vercel preview deploys. The customer is trusted to share the URL appropriately. The reviewer’s name + email on the public page are attestations, not third-party verified (v3 will add email-OTP for high-assurance contexts).

Rate limits

EndpointLimitWhy
POST /pending/{id}/share 30/hour/project Sharing is bursty but bounded
GET /pending/public/{id} 60/min/IP Polling-safe
POST /pending/public/{id}/complete 5/min/IP Brute-forcing 128 bits is computationally infeasible, but layered defense

What this is NOT (yet)

Smoke-test it yourself

Spin up a 5-line test webhook receiver and watch the loop run end-to-end:

# 1. Create the pending item
curl -X POST https://marturia.dev/api/marturia/v1/oversight/pending \
  -H "X-Marturia-Key: $MARTURIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_name":           "smoke_test",
    "agent_recommendation": "approve",
    "input_summary":        "Smoke test",
    "severity":             "low",
    "callback_url":         "https://webhook.site/<your-id>"
  }'

# 2. Review the item via the dashboard
# Open: https://marturia.dev/app/oversight/<id from response>

# 3. Webhook fires to your URL within 5s. Verify the signature.

See also: Article 14 oversight receipts · Offline verification · Full API reference