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:
-
Queue the pending review.
Call
POST /api/marturia/v1/oversight/pendingwith 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 areview_url. -
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). -
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. - 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.
-
Your webhook fires. Marturia POSTs to your
callback_urlwith 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:
| Action | Meaning |
|---|---|
approve | Reviewer accepted the agent’s recommendation as-is. |
override | Reviewer changed the decision — final_outcome.decision must differ from the agent’s recommendation (server-enforced). |
reject | Reviewer blocked the action entirely. Nothing executes. |
interrupt | Reviewer used the “stop button” on a running multi-step agent. |
no_action | Reviewer 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
- Single-use: the moment a reviewer
successfully submits an action,
share_consumed_atis set. Further submission attempts return 410 Gone. - Time-bounded: the URL expires after
expires_in_hours(default 7 days, max 30 days). Calling/shareagain on the same pending generates a new token + invalidates the old one — useful when the original URL was lost or the reviewer is replaced. - Read-only after consumption: the URL still loads after the reviewer acts, but renders a “Review already completed” state with the public verify URL for the recorded receipt.
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
| Endpoint | Limit | Why |
|---|---|---|
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)
- Public reviewer URLs with signed tokens. External reviewers without dashboard accounts. v2.
- Embeddable widget. Drop-in
<script>for your app to host the review UI inline. v2. - Long-poll on GET. v1 is short-poll (recommend 5s interval).
- Bulk actions. Each item gets its own rationale per Article 14. Not in scope.
- Callback retry UI. v1 logs failures; replay via dashboard is v2.
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