Skip to main content

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 questionsComplex multi-step operations
Interactive chat UIBackground processing
Simple integrationsSSE 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

StatusMeaningWhat to do
pendingQueued, waiting to startKeep polling
in_progressAI is processingKeep polling
awaiting_approvalThe job is paused for your input — check metadata.awaiting_kindChange review (HITL): read metadata.pending_changes, call /approve. Continue prompt (large edit): read metadata.continue_prompt, call /continue — see Continuing a large edit
completedFinishedRead result
failedError occurredRead error field
cancelledCancelled by youNo 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

OperationExpected durationSurface “still processing” afterTreat as failed after
Single-section edit< 10 seconds30 seconds2 minutes
Multi-section edit (3–5 sections)30–90 seconds2 minutes5 minutes
Full-document operation (10+ sections)60–180 seconds3 minutes10 minutes
Complex operation (merge, restructure, batch)120–300 seconds4 minutes15 minutes
Very large document (hundreds of sections)5–25 minutes8 minutes30 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_typeTriggered byLifecycle
chatPOST /v1/chat/asyncStandard chat turn. Supports awaiting_approval for HITL. SSE stream available at /v1/chat/{session_id}/stream?job_id=....
attachment_processingBackground attachment ingestionInternal; usually completes in seconds.
large_exportPOST /v1/documents/export/email-requestBackground 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.