Human-in-the-Loop
By default, the AI applies changes immediately. Set approval_mode to "ask_every_time" to review changes before they take effect.
HITL requires the async workflow (/v1/chat/async) because the job pauses to wait for your approval.
End-to-end workflow
1. Send a request with approval mode
curl -X POST https://api.superdocs.app/v1/chat/async \
-H "Authorization: Bearer sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Update section 3 to include GDPR compliance language",
"session_id": "contract-review",
"document_html": "...",
"approval_mode": "ask_every_time"
}'
2. Poll for approval status
curl https://api.superdocs.app/v1/jobs/JOB_ID \
-H "Authorization: Bearer sk_YOUR_API_KEY"
When status is "awaiting_approval", check metadata.pending_changes:
{
"status": "awaiting_approval",
"metadata": {
"pending_changes": [
{
"change_id": "ch_1",
"operation": "edit",
"chunk_id": "550e8400-e29b-41d4-a716-446655440000",
"old_html": "<p>Original section 3 content...</p>",
"new_html": "<p>Updated content with GDPR compliance...</p>",
"ai_explanation": "Added GDPR data processing requirements"
}
]
}
}
3. Approve or deny changes
Approve a single change:
curl -X POST https://api.superdocs.app/v1/chat/contract-review/approve \
-H "Authorization: Bearer sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_id": "JOB_ID",
"change_id": "ch_1",
"approved": true
}'
Approve all changes at once:
curl -X POST https://api.superdocs.app/v1/chat/contract-review/approve \
-H "Authorization: Bearer sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_id": "JOB_ID",
"approved": true,
"changes": [
{"change_id": "ch_1", "approved": true},
{"change_id": "ch_2", "approved": false}
]
}'
Deny with feedback:
curl -X POST https://api.superdocs.app/v1/chat/contract-review/approve \
-H "Authorization: Bearer sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"job_id": "JOB_ID",
"change_id": "ch_1",
"approved": false,
"feedback": "Keep the original language but add a GDPR reference link"
}'
4. Continue polling
After approval, the job resumes processing. Poll until status is "completed" to get the final result.
If you deny a change with feedback, the AI receives your feedback and may propose a revised change. Your polling loop should handle multiple rounds of awaiting_approval — not just one.
Understanding proposed changes
Operation types
Each proposed change has an operation field that determines what the AI wants to do and which fields are populated:
| Operation | old_html | new_html | insert_after_chunk_id | Meaning |
|---|
edit | Current content | Proposed replacement | — | Modify an existing section |
create | null | New content to add | Section to insert after | Add a new section to the document |
delete | Content being removed | null | — | Remove a section from the document |
Building a diff view
To show users what the AI wants to change, compare old_html and new_html:
- For
edit: Use a diff library (like diff-match-patch or jsdiff) to highlight additions and removals between old_html and new_html. Show a before/after view or inline diff.
- For
create: Display new_html with a visual indicator that this is a new section being added (e.g., a green border or “New section” label). The insert_after_chunk_id value corresponds to a data-chunk-id attribute on an element in the document HTML — find that element and insert the new content after it.
- For
delete: Display old_html with a visual indicator that this section will be removed (e.g., red strikethrough or “Will be deleted” label).
Always display the ai_explanation alongside the diff — it tells the user why the AI proposed the change.
Batch changes
When the AI proposes multiple changes at once, each change includes:
batch_id — Groups related changes. All changes in a batch share the same batch_id.
batch_total — How many changes are in this batch.
If you’re receiving changes via SSE, wait for batch_total events with the same batch_id before showing the review UI. You can then offer “Accept All” and “Deny All” buttons alongside individual change controls.
For polling, all changes appear together in the metadata.pending_changes array.
Alternative: SSE streaming workflow
The polling workflow above works but requires repeated API calls. For a real-time UI, use SSE to receive proposed changes as they’re generated:
// 1. Start async request with approval mode
const response = await fetch("https://api.superdocs.app/v1/chat/async", {
method: "POST",
headers: { "Authorization": "Bearer sk_YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
message: "Rewrite the liability section",
session_id: "contract-review",
document_html: "...",
approval_mode: "ask_every_time"
})
});
const { job_id } = await response.json();
// 2. Open SSE connection
const es = new EventSource(
`https://api.superdocs.app/v1/chat/contract-review/stream?job_id=${job_id}&api_key=sk_YOUR_API_KEY`
);
// 3. Collect proposed changes
const pendingChanges = [];
es.addEventListener("proposed_change", (event) => {
const data = JSON.parse(event.data);
const change = JSON.parse(data.content); // content is JSON-stringified
pendingChanges.push(change);
// Show the change to the user with a diff view
console.log(`Change ${change.change_id}: ${change.operation}`);
console.log(` Explanation: ${change.ai_explanation}`);
console.log(` Old: ${change.old_html}`);
console.log(` New: ${change.new_html}`);
// If batch, wait for all changes before showing review UI
if (change.batch_total && pendingChanges.length < change.batch_total) {
return; // Wait for more changes
}
// Show approve/deny UI to the user here
// When user decides, call the approve endpoint (see step 3 above)
});
es.addEventListener("final", (event) => {
const data = JSON.parse(event.data);
console.log("Done:", data.result.response);
// Apply data.result.document_changes.updated_html to your editor
es.close();
});
es.addEventListener("error", (event) => {
if (event.data) {
const data = JSON.parse(event.data);
console.error("Error:", data.error);
}
es.close();
});
After calling /approve, the AI resumes processing. If approved changes were applied, the SSE connection will eventually emit a final event with the updated document. If you denied with feedback, you may receive new proposed_change events as the AI tries again.
Complete Python example
import time
import requests
API_KEY = "sk_YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
BASE = "https://api.superdocs.app"
# 1. Start with approval mode
response = requests.post(f"{BASE}/v1/chat/async", headers=HEADERS, json={
"message": "Rewrite the liability section",
"session_id": "contract-review",
"document_html": "...",
"approval_mode": "ask_every_time"
})
job_id = response.json()["job_id"]
# 2. Poll and handle approvals
while True:
job = requests.get(f"{BASE}/v1/jobs/{job_id}", headers=HEADERS).json()
if job["status"] == "completed":
print("Done:", job["result"]["response"])
break
elif job["status"] == "failed":
print("Error:", job["error"])
break
elif job["status"] == "awaiting_approval":
changes = job["metadata"]["pending_changes"]
for change in changes:
print(f"\nChange {change['change_id']}:")
print(f" Operation: {change['operation']}")
print(f" Explanation: {change.get('ai_explanation', 'N/A')}")
print(f" Old: {change.get('old_html', 'N/A')[:100]}")
print(f" New: {change.get('new_html', 'N/A')[:100]}")
# Approve all changes
requests.post(f"{BASE}/v1/chat/contract-review/approve", headers=HEADERS, json={
"job_id": job_id,
"approved": True,
"changes": [{"change_id": c["change_id"], "approved": True} for c in changes]
})
print("\nApproved all changes, continuing...")
time.sleep(2)