Skip to main content

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:
Operationold_htmlnew_htmlinsert_after_chunk_idMeaning
editCurrent contentProposed replacementModify an existing section
createnullNew content to addSection to insert afterAdd a new section to the document
deleteContent being removednullRemove 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)