Async Jobs
The sync endpoint (POST /v1/chat) waits for the AI to finish before responding. For long operations or background processing, use the async endpoint instead.
For batch processing where speed dominates, consider model_tier: "turbo" in your request body — the fastest tier, optimised for high-volume workflows. For high-stakes documents in your batch, use pro or max instead. See Model Selection for the full matrix.
When to use async
Use sync (/v1/chat) | Use async (/v1/chat/async) |
|---|
| Quick edits and questions | Complex multi-step operations |
| Interactive chat UI | Background processing |
| Simple integrations | SSE streaming for progress |
| HITL approval workflows |
Async flow
1. Start the job
curl -X POST https://api.superdocs.app/v1/chat/async \
-H "Authorization: Bearer sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Restructure this entire document into 5 sections",
"session_id": "my-session",
"document_html": "..."
}'
Response:
{
"job_id": "job_abc123",
"session_id": "my-session",
"status": "pending",
"message": "Chat request queued for processing"
}
2. Poll for status
curl https://api.superdocs.app/v1/jobs/job_abc123 \
-H "Authorization: Bearer sk_YOUR_API_KEY"
3. Get the result
When status is "completed", the result field contains the AI response and document changes:
{
"job_id": "job_abc123",
"status": "completed",
"result": {
"response": "I've restructured the document into 5 sections...",
"session_id": "my-session",
"document_changes": {
"updated_html": "<div data-chunk-id=\"...\">...</div>",
"version_id": "...",
"changes_summary": "Document updated by AI"
},
"usage": {
"monthly_used": 44,
"monthly_limit": 500,
"monthly_remaining": 456,
"was_billable": true,
"subscription_tier": "free"
}
}
}
Job statuses
| Status | Meaning | What to do |
|---|
pending | Queued, waiting to start | Keep polling |
in_progress | AI is processing | Keep polling |
awaiting_approval | The job is paused for your input — check metadata.awaiting_kind | Change review (HITL): read metadata.pending_changes, call /approve. Continue prompt (large edit): read metadata.continue_prompt, call /continue — see Continuing a large edit |
completed | Finished | Read result |
failed | Error occurred | Read error field |
cancelled | Cancelled by you | No action needed |
Revert vs. in-flight jobs. Revert returns 409 while a chat job on the same session is in pending, in_progress, or awaiting_approval. Wait for the job to land in completed/failed/cancelled, or cancel it explicitly via POST /v1/jobs/{job_id}/cancel, before retrying revert.
Continuing a large edit
A very large edit can be too big to finish in a single turn. When that happens the AI applies as much as it can, keeps that work, and pauses with status: "awaiting_approval" and metadata.awaiting_kind: "continue_prompt" — asking whether to continue with the rest. This is distinct from HITL change review (which instead sets metadata.pending_changes) and can occur in either approval mode.
The pause carries a metadata.continue_prompt object:
{
"status": "awaiting_approval",
"metadata": {
"awaiting_kind": "continue_prompt",
"continue_prompt": {
"message": "I've updated 500 of 864 sections so far. 364 remain. Want me to continue with the rest?",
"done": 500,
"total": 864,
"remaining": 364
}
}
}
Resume — or stop — by calling /continue:
curl -X POST https://api.superdocs.app/v1/chat/{session_id}/continue \
-H "Authorization: Bearer $SUPERDOCS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"job_id": "job_abc123", "continue": true}'
continue: true — resume; the job returns to in_progress and finishes the rest.
continue: false — stop here; everything applied so far is kept.
The job may pause again for the next segment, so handle continue_prompt in your polling loop just like repeated HITL rounds — keep going until status is completed. If you never respond, the job is cleaned up after 1 hour, like any other awaiting_approval job.
Latency expectations and timeout strategy
Knowing how long an operation should take helps you decide when to surface a “still processing” hint and when to treat a job as broken.
Typical latency by operation type
| Operation | Expected duration | Surface “still processing” after | Treat as failed after |
|---|
| Single-section edit | < 10 seconds | 30 seconds | 2 minutes |
| Multi-section edit (3–5 sections) | 30–90 seconds | 2 minutes | 5 minutes |
| Full-document operation (10+ sections) | 60–180 seconds | 3 minutes | 10 minutes |
| Complex operation (merge, restructure, batch) | 120–300 seconds | 4 minutes | 15 minutes |
| Very large document (hundreds of sections) | 5–25 minutes | 8 minutes | 30 minutes (server cap) |
These are medians for typical documents (5–20 sections). Larger documents, embedded images, or operations using model_tier: "max" with thinking_depth: "deep" may exceed these ranges. The model_tier and thinking_depth you choose materially affect latency — see Model Selection. The server enforces a hard 30-minute wall-clock cap on every chat turn (sync and async); requests that exceed it return a graceful 504 with a suggestion to split the work across smaller turns.
Polling strategy that surfaces progress
import time
import requests
MAX_WAIT = 300 # 5 minutes — adjust per operation type
WARN_AFTER = 60 # surface "still processing" after 60 seconds
POLL_INTERVAL = 2 # seconds between status checks
def wait_for_job(job_id: str, headers: dict) -> dict:
start = time.time()
last_warn = 0
while True:
r = requests.get(f"https://api.superdocs.app/v1/jobs/{job_id}", headers=headers)
r.raise_for_status()
job = r.json()
elapsed = time.time() - start
if job["status"] == "completed":
return job["result"]
if job["status"] == "failed":
raise RuntimeError(job.get("error", "Unknown error"))
if job["status"] == "cancelled":
raise RuntimeError("Job was cancelled")
if elapsed > MAX_WAIT:
raise TimeoutError(f"Job did not complete within {MAX_WAIT}s")
# Surface a "still processing" hint to the user every 30s after WARN_AFTER
if elapsed > WARN_AFTER and elapsed - last_warn >= 30:
print(f" Still processing... ({int(elapsed)}s elapsed)")
last_warn = elapsed
time.sleep(POLL_INTERVAL)
For real-time progress (instead of polling silence), open the SSE stream in parallel and surface every intermediate event to the user — see Streaming → Rendering intermediate events.
Job retention
All jobs — pending, in-progress, awaiting-approval, or terminal — are automatically removed 1 hour after creation. For user-facing workflows that may run longer (or where the user steps away), keep MAX_WAIT well under 1 hour and persist the job_id so you can resume polling later.
Job types
The job_type field on every job row tells you which lifecycle to expect.
job_type | Triggered by | Lifecycle |
|---|
chat | POST /v1/chat/async | Standard chat turn. Supports awaiting_approval for HITL. SSE stream available at /v1/chat/{session_id}/stream?job_id=.... |
attachment_processing | Background attachment ingestion | Internal; usually completes in seconds. |
large_export | POST /v1/documents/export/email-request | Background export of an oversize document. No SSE; poll /v1/jobs/{job_id}. On completed, result.download_url carries a 7-day signed link and an email has been sent to the recipient. 24-hour SLA. See Email fallback for very large documents. |
large_export jobs are fire-and-forget — the recipient receives an email when the render finishes, so polling is optional. Use it when you want to surface progress in your UI rather than letting the user check their inbox.
Cancel a job
Only pending and in_progress jobs can be cancelled:
curl -X POST https://api.superdocs.app/v1/jobs/job_abc123/cancel \
-H "Authorization: Bearer sk_YOUR_API_KEY"
List all jobs
curl https://api.superdocs.app/v1/jobs \
-H "Authorization: Bearer sk_YOUR_API_KEY"
Polling example
import time
import requests
API_KEY = "sk_YOUR_API_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Start async job
response = requests.post("https://api.superdocs.app/v1/chat/async",
headers={**HEADERS, "Content-Type": "application/json"},
json={"message": "Summarize this document", "session_id": "my-session", "document_html": "..."}
)
job_id = response.json()["job_id"]
# Poll until complete
while True:
job = requests.get(f"https://api.superdocs.app/v1/jobs/{job_id}", headers=HEADERS).json()
if job["status"] == "completed":
print("AI response:", job["result"]["response"])
break
elif job["status"] == "failed":
print("Error:", job["error"])
break
elif job["status"] == "awaiting_approval":
meta = job.get("metadata", {})
if meta.get("awaiting_kind") == "continue_prompt":
# Large edit paused mid-way — resume it (send continue=false to stop instead).
print(meta["continue_prompt"]["message"])
requests.post("https://api.superdocs.app/v1/chat/my-session/continue",
headers={**HEADERS, "Content-Type": "application/json"},
json={"job_id": job_id, "continue": True})
# keep polling — the job returns to in_progress and may pause again
else:
# HITL change review — inspect the changes, then call /approve.
print("Changes need approval:", meta.get("pending_changes"))
break
time.sleep(2)
Jobs are automatically cleaned up 1 hour after completion, failure, or cancellation. Jobs in awaiting_approval status are also cleaned up after 1 hour — approve or deny changes within this window.